From 6783895f14cb6cb9ab7be83c00746ae5b7d5ad02 Mon Sep 17 00:00:00 2001 From: MiaoMint <1981324730@qq.com> Date: Wed, 26 Jul 2023 17:33:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A6=96=E9=A1=B5=E6=94=B6=E8=97=8F?= =?UTF-8?q?=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +- assets/i18n/en.json | 3 +- assets/i18n/zh.json | 3 +- lib/pages/home/controller.dart | 21 +- lib/pages/home/pages/favorites_page.dart | 157 ++++++++++++++ lib/pages/home/view.dart | 15 +- lib/pages/home/widgets/home_favorites.dart | 82 ++++--- lib/pages/search/widgets/search_all_tile.dart | 191 ++++------------ .../search/widgets/search_all_tile_title.dart | 65 ------ lib/router/router.dart | 12 ++ lib/utils/database.dart | 18 +- lib/widgets/extension_item_card.dart | 19 +- lib/widgets/horizontal_list.dart | 203 ++++++++++++++++++ 13 files changed, 511 insertions(+), 288 deletions(-) create mode 100644 lib/pages/home/pages/favorites_page.dart delete mode 100644 lib/pages/search/widgets/search_all_tile_title.dart create mode 100644 lib/widgets/horizontal_list.dart diff --git a/README.md b/README.md index 97f66dae..d923ae0c 100644 --- a/README.md +++ b/README.md @@ -29,18 +29,18 @@ Miru App - [x] Windows 支持 - [x] Android 支持 - [x] ~~Linux 支持~~ -- [ ] 字幕 -- [ ] BT 种子播放 - [x] 漫画支持 - [x] 小说支持 - [x] 影视支持 -- [ ] TMDB 元数据 -- [ ] 数据同步 - [x] i18n 国际化 - [x] 扩展设置 +- [x] 影视播放进度 - [ ] 漫画小说视频设置 - [ ] 漫画小说历史记录 -- [x] 影视播放进度 +- [ ] TMDB 元数据 +- [ ] 字幕 +- [ ] BT 种子播放 +- [ ] 数据同步 - [ ] 自动搜寻字幕 ## 截图 diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 10b08feb..b54e5e1d 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -19,7 +19,8 @@ "error": "Error", "retry": "Retry", "next": "Next", - "previous": "Previous" + "previous": "Previous", + "show-all": "Show all" }, "home": { diff --git a/assets/i18n/zh.json b/assets/i18n/zh.json index f5968ad5..40357146 100644 --- a/assets/i18n/zh.json +++ b/assets/i18n/zh.json @@ -24,7 +24,8 @@ "error": "错误", "retry": "重试", "next": "下一个", - "previous": "上一个" + "previous": "上一个", + "show-all": "显示全部" }, "home": { diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 73c60240..8768704b 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -1,11 +1,13 @@ import 'package:get/get.dart'; +import 'package:miru_app/models/extension.dart'; import 'package:miru_app/models/favorite.dart'; import 'package:miru_app/models/history.dart'; import 'package:miru_app/utils/database.dart'; class HomePageController extends GetxController { final RxList resents = [].obs; - final RxList favorites = [].obs; + final RxMap> favorites = + >{}.obs; @override void onInit() { @@ -19,8 +21,19 @@ class HomePageController extends GetxController { resents.addAll( await DatabaseUtils.getHistorysByType(), ); - favorites.addAll( - await DatabaseUtils.getFavoritesByType(), - ); + favorites.addAll({ + ExtensionType.bangumi: await DatabaseUtils.getFavoritesByType( + type: ExtensionType.bangumi, + limit: 20, + ), + ExtensionType.manga: await DatabaseUtils.getFavoritesByType( + type: ExtensionType.manga, + limit: 20, + ), + ExtensionType.fikushon: await DatabaseUtils.getFavoritesByType( + type: ExtensionType.fikushon, + limit: 20, + ), + }); } } diff --git a/lib/pages/home/pages/favorites_page.dart b/lib/pages/home/pages/favorites_page.dart new file mode 100644 index 00000000..ce026cf6 --- /dev/null +++ b/lib/pages/home/pages/favorites_page.dart @@ -0,0 +1,157 @@ +import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:flutter/material.dart'; +import 'package:miru_app/models/index.dart'; +import 'package:miru_app/utils/database.dart'; +import 'package:miru_app/utils/extension.dart'; +import 'package:miru_app/utils/i18n.dart'; +import 'package:miru_app/widgets/extension_item_card.dart'; +import 'package:miru_app/widgets/platform_widget.dart'; +import 'package:miru_app/widgets/progress_ring.dart'; + +class FavoritesPage extends fluent.StatefulWidget { + const FavoritesPage({Key? key, required this.type}) : super(key: key); + final ExtensionType type; + + @override + fluent.State createState() => _FavoritesPageState(); +} + +class _FavoritesPageState extends fluent.State { + Widget _buildAndroid(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + "${ExtensionUtils.typeToString(widget.type)}${"home.favorite".i18n}", + )), + body: FutureBuilder( + future: DatabaseUtils.getFavoritesByType(type: widget.type), + builder: ((context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Text("${snapshot.error}"), + ); + } + + if (!snapshot.hasData) { + return const SizedBox( + height: 300, + child: Center( + child: ProgressRing(), + ), + ); + } + final data = snapshot.data; + + if (data != null && data.isEmpty) { + return Center( + child: Text("common.no-result".i18n), + ); + } + return LayoutBuilder( + builder: (context, constraints) => GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: constraints.maxWidth ~/ 120, + childAspectRatio: 0.7, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: data!.length, + itemBuilder: (context, index) { + final item = data[index]; + return ExtensionItemCard( + title: item.title, + url: item.url, + package: item.package, + cover: item.cover, + ); + }, + ), + ); + }), + ), + ); + } + + Widget _buildDesktop(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + flex: 2, + child: Text( + "${ExtensionUtils.typeToString(widget.type)}${"home.favorite".i18n}", + style: fluent.FluentTheme.of(context).typography.subtitle, + ), + ), + const Spacer(), + ], + ), + const SizedBox(height: 16), + Expanded( + child: FutureBuilder( + future: DatabaseUtils.getFavoritesByType(type: widget.type), + builder: ((context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Text( + snapshot.error.toString(), + ), + ); + } + + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + final data = snapshot.data; + + if (data == null) { + return const Center( + child: Text('No data'), + ); + } + + return LayoutBuilder( + builder: ((context, constraints) => GridView.builder( + padding: const EdgeInsets.all(8), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: constraints.maxWidth ~/ 160, + childAspectRatio: 0.6, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: data.length, + itemBuilder: (context, index) { + final item = data[index]; + return ExtensionItemCard( + title: item.title, + url: item.url, + package: item.package, + cover: item.cover, + ); + }, + )), + ); + }), + ), + ) + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return PlatformBuildWidget( + androidBuilder: _buildAndroid, + desktopBuilder: _buildDesktop, + ); + } +} diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index a1109377..faced389 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:miru_app/models/extension.dart'; import 'package:miru_app/pages/home/controller.dart'; import 'package:miru_app/pages/home/widgets/home_favorites.dart'; import 'package:miru_app/pages/home/widgets/home_recent.dart'; @@ -58,10 +59,20 @@ class _HomePageState extends State { ), const SizedBox(height: 16), ], - if (c.favorites.isNotEmpty) + if (c.favorites.isNotEmpty) ...[ HomeFavorites( - data: c.favorites, + type: ExtensionType.bangumi, + data: c.favorites[ExtensionType.bangumi]!, ), + HomeFavorites( + type: ExtensionType.manga, + data: c.favorites[ExtensionType.manga]!, + ), + HomeFavorites( + type: ExtensionType.fikushon, + data: c.favorites[ExtensionType.fikushon]!, + ), + ] ], ), ), diff --git a/lib/pages/home/widgets/home_favorites.dart b/lib/pages/home/widgets/home_favorites.dart index 4954519e..b7885e9e 100644 --- a/lib/pages/home/widgets/home_favorites.dart +++ b/lib/pages/home/widgets/home_favorites.dart @@ -1,15 +1,22 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:miru_app/models/extension.dart'; import 'package:miru_app/models/favorite.dart'; -import 'package:miru_app/utils/i18n.dart'; +import 'package:miru_app/pages/home/pages/favorites_page.dart'; +import 'package:miru_app/router/router.dart'; +import 'package:miru_app/utils/extension.dart'; import 'package:miru_app/widgets/extension_item_card.dart'; +import 'package:miru_app/widgets/horizontal_list.dart'; class HomeFavorites extends StatefulWidget { const HomeFavorites({ Key? key, + required this.type, required this.data, }) : super(key: key); + final ExtensionType type; final List data; @override @@ -19,52 +26,35 @@ class HomeFavorites extends StatefulWidget { class _HomeFavoritesState extends State { @override Widget build(BuildContext context) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "home.favorite".i18n, - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - // IconButton( - // icon: const Icon(FluentIcons.filter), - // onPressed: () { - // // _filterDialog(context); - // }) - ], - ), - const SizedBox(height: 16), - LayoutBuilder( - builder: (context, constraints) { - final crossAxisCount = - constraints.maxWidth ~/ (Platform.isAndroid ? 120 : 160); - final childAspectRatio = Platform.isAndroid ? 0.7 : 0.6; - return GridView.builder( - // 取消滚动 - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - childAspectRatio: childAspectRatio, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - ), - itemCount: widget.data.length, - itemBuilder: (context, index) { - return ExtensionItemCard( - key: ValueKey(widget.data[index].cover), - title: widget.data[index].title, - url: widget.data[index].url, - package: widget.data[index].package, - cover: widget.data[index].cover, - ); - }, + if (widget.data.isEmpty) { + return const SizedBox.shrink(); + } + return SizedBox( + child: HorizontalList( + title: ExtensionUtils.typeToString(widget.type), + onClickMore: () { + if (Platform.isAndroid) { + Get.to(FavoritesPage(type: widget.type)); + } else { + router.push( + Uri( + path: '/favorites', + queryParameters: {'type': widget.type.index.toString()}, + ).toString(), ); - }, - ) - ], + } + }, + itemCount: widget.data.length, + itemBuilder: (context, index) { + return ExtensionItemCard( + key: ValueKey(widget.data[index].cover), + title: widget.data[index].title, + url: widget.data[index].url, + package: widget.data[index].package, + cover: widget.data[index].cover, + ); + }, + ), ); } } diff --git a/lib/pages/search/widgets/search_all_tile.dart b/lib/pages/search/widgets/search_all_tile.dart index 299ace19..a2d72412 100644 --- a/lib/pages/search/widgets/search_all_tile.dart +++ b/lib/pages/search/widgets/search_all_tile.dart @@ -1,10 +1,10 @@ -import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'dart:io'; + import 'package:flutter/material.dart'; -import 'package:miru_app/pages/search/widgets/search_all_tile_title.dart'; import 'package:miru_app/utils/extension_runtime.dart'; import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/widgets/extension_item_card.dart'; -import 'package:miru_app/widgets/platform_widget.dart'; +import 'package:miru_app/widgets/horizontal_list.dart'; import 'package:miru_app/widgets/progress_ring.dart'; class SearchAllTile extends StatefulWidget { @@ -24,42 +24,19 @@ class SearchAllTile extends StatefulWidget { } class _SearchAllTileState extends State { - final ScrollController _controller = ScrollController(); - bool hoverTitle = false; - - _horzontalMove(bool left) { - _controller.animateTo( - _controller.offset + (left ? -500 : 500), - duration: const Duration(milliseconds: 500), - curve: Curves.ease, - ); - } - - Widget _buildAndroid(BuildContext context) { - return Column( - children: [ - Row( - children: [ - Text( - widget.runtime.extension.name, - ), - const Spacer(), - IconButton( - icon: const Icon(Icons.chevron_right), - onPressed: widget.onClickMore, - ), - ], - ), - const SizedBox(height: 8), - SizedBox( - height: 186, - child: Center( - child: FutureBuilder( - key: ValueKey(widget.kw), - future: widget.kw.isNotEmpty - ? widget.runtime.search(widget.kw, 1) - : widget.runtime.latest(1), - builder: ((context, snapshot) { + @override + Widget build(BuildContext context) { + return Center( + child: FutureBuilder( + key: ValueKey(widget.kw), + future: widget.kw.isNotEmpty + ? widget.runtime.search(widget.kw, 1) + : widget.runtime.latest(1), + builder: ((context, snapshot) { + return HorizontalList( + onClickMore: widget.onClickMore, + title: widget.runtime.extension.name, + contentBuilder: (controller) { if (snapshot.hasError) { return Text(snapshot.error.toString()); } @@ -74,118 +51,32 @@ class _SearchAllTileState extends State { return Text('common.no-result'.i18n); } - return ListView.builder( - scrollDirection: Axis.horizontal, - controller: _controller, - itemCount: data!.length, - itemBuilder: (context, index) { - return Container( - width: 128, - margin: const EdgeInsets.symmetric(horizontal: 6), - child: ExtensionItemCard( - key: ValueKey(data[index].url), - title: data[index].title, - url: data[index].url, - package: widget.runtime.extension.package, - cover: data[index].cover, - update: data[index].update, - ), - ); - }, + return SizedBox( + height: Platform.isAndroid ? 186 : 280, + child: ListView.builder( + scrollDirection: Axis.horizontal, + controller: controller, + itemCount: data!.length, + itemBuilder: ((context, index) { + return Container( + width: Platform.isAndroid ? 128 : 170, + margin: const EdgeInsets.only(right: 16), + child: ExtensionItemCard( + key: ValueKey(data[index].url), + title: data[index].title, + url: data[index].url, + package: widget.runtime.extension.package, + cover: data[index].cover, + update: data[index].update, + ), + ); + }), + ), ); - }), - )), - ), - const SizedBox(height: 16) - ], - ); - } - - Widget _buildDesktop(BuildContext context) { - return Column( - children: [ - Row( - children: [ - SearchAllTileTitle( - widget.runtime.extension.name, - onClick: widget.onClickMore, - ), - const Spacer(), - Row( - children: [ - fluent.IconButton( - icon: const Icon(fluent.FluentIcons.chevron_left), - onPressed: () { - _horzontalMove(true); - }), - const SizedBox(width: 8), - fluent.IconButton( - icon: const Icon(fluent.FluentIcons.chevron_right), - onPressed: () { - _horzontalMove(false); - }, - ) - ], - ), - ], - ), - const SizedBox(height: 8), - SizedBox( - height: 280, - child: Center( - child: FutureBuilder( - key: ValueKey(widget.kw), - future: widget.kw.isNotEmpty - ? widget.runtime.search(widget.kw, 1) - : widget.runtime.latest(1), - builder: ((context, snapshot) { - if (snapshot.hasError) { - return Text(snapshot.error.toString()); - } - - if (!snapshot.hasData) { - return const ProgressRing(); - } - - final data = snapshot.data; - - if (snapshot.data != null && snapshot.data!.isEmpty) { - return Text("common.no-result".i18n); - } - - return ListView.builder( - scrollDirection: Axis.horizontal, - controller: _controller, - itemCount: data!.length, - padding: const EdgeInsets.all(5), - itemBuilder: (context, index) { - return Container( - width: 170, - margin: const EdgeInsets.only(right: 8), - child: ExtensionItemCard( - key: ValueKey(data[index].url), - title: data[index].title, - url: data[index].url, - package: widget.runtime.extension.package, - cover: data[index].cover, - update: data[index].update, - ), - ); - }, - ); - }), - )), - ), - const SizedBox(height: 16) - ], - ); - } - - @override - Widget build(BuildContext context) { - return PlatformBuildWidget( - androidBuilder: _buildAndroid, - desktopBuilder: _buildDesktop, + }, + ); + }), + ), ); } } diff --git a/lib/pages/search/widgets/search_all_tile_title.dart b/lib/pages/search/widgets/search_all_tile_title.dart deleted file mode 100644 index 0cc42b8f..00000000 --- a/lib/pages/search/widgets/search_all_tile_title.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart' as fluent; -import 'package:flutter/material.dart'; - -class SearchAllTileTitle extends StatefulWidget { - const SearchAllTileTitle(this.text, {Key? key, required this.onClick}) - : super(key: key); - final String text; - final Function() onClick; - - @override - State createState() => _SearchAllTileTitleState(); -} - -class _SearchAllTileTitleState extends State { - bool _hoverTitle = false; - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - onHover: (event) { - setState(() { - _hoverTitle = true; - }); - }, - onExit: (event) { - setState(() { - _hoverTitle = false; - }); - }, - child: GestureDetector( - onTap: () { - widget.onClick(); - }, - child: AnimatedContainer( - padding: EdgeInsets.symmetric( - horizontal: _hoverTitle ? 20 : 10, vertical: 10), - duration: const Duration(milliseconds: 100), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8)), - color: _hoverTitle - ? const Color.fromARGB(19, 27, 26, 25) - : Colors.transparent, - ), - child: Row( - children: [ - Text( - widget.text, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 8), - const Icon( - fluent.FluentIcons.chevron_right_med, - size: 14, - ) - ], - ), - ), - ), - ); - } -} diff --git a/lib/router/router.dart b/lib/router/router.dart index 965aaed2..1484013d 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -1,10 +1,12 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:go_router/go_router.dart'; +import 'package:miru_app/models/index.dart'; import 'package:miru_app/pages/detail/view.dart'; import 'package:miru_app/pages/extension/view.dart'; import 'package:miru_app/pages/extension_repo/view.dart'; import 'package:miru_app/pages/extension_settings/view.dart'; +import 'package:miru_app/pages/home/pages/favorites_page.dart'; import 'package:miru_app/pages/home/view.dart'; import 'package:miru_app/pages/main/view.dart'; import 'package:miru_app/pages/search/pages/search_extension.dart'; @@ -31,6 +33,16 @@ final router = GoRouter( path: '/', builder: (context, state) => _animation(const HomePage()), ), + GoRoute( + path: '/favorites', + builder: (context, state) => _animation( + FavoritesPage( + type: ExtensionType.values[int.parse( + state.queryParameters['type']!, + )], + ), + ), + ), GoRoute( path: '/search', builder: (context, state) => _animation(const SearchPage()), diff --git a/lib/utils/database.dart b/lib/utils/database.dart index dc902da0..e91fbd16 100644 --- a/lib/utils/database.dart +++ b/lib/utils/database.dart @@ -58,12 +58,22 @@ class DatabaseUtils { null; } - static Future> getFavoritesByType( - {ExtensionType? type}) async { + static Future> getFavoritesByType({ + ExtensionType? type, + int? limit, + }) async { if (type == null) { - return db.favorites.where().sortByDateDesc().findAll(); + final query = db.favorites.where().sortByDateDesc(); + if (limit != null) { + return query.limit(limit).findAll(); + } + return query.findAll(); + } + final query = db.favorites.filter().typeEqualTo(type).sortByDateDesc(); + if (limit != null) { + return query.limit(limit).findAll(); } - return db.favorites.filter().typeEqualTo(type).sortByDateDesc().findAll(); + return query.findAll(); } // 历史记录 diff --git a/lib/widgets/extension_item_card.dart b/lib/widgets/extension_item_card.dart index 58985454..10fdaae5 100644 --- a/lib/widgets/extension_item_card.dart +++ b/lib/widgets/extension_item_card.dart @@ -143,20 +143,19 @@ class _ExtensionItemCardState extends State { ).toString(), ); }, - child: AnimatedScale( - scale: _isHover ? 1.03 : 1, - duration: const Duration(milliseconds: 80), - child: Container( + child: Container( decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(8)), ), clipBehavior: Clip.antiAlias, - child: CacheNetWorkImage( - widget.cover, - width: double.infinity, - ), - ), - ), + child: AnimatedScale( + scale: _isHover ? 1.05 : 1, + duration: const Duration(milliseconds: 80), + child: CacheNetWorkImage( + widget.cover, + width: double.infinity, + ), + )), ), ), const SizedBox(height: 8), diff --git a/lib/widgets/horizontal_list.dart b/lib/widgets/horizontal_list.dart new file mode 100644 index 00000000..b6397239 --- /dev/null +++ b/lib/widgets/horizontal_list.dart @@ -0,0 +1,203 @@ +import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:flutter/material.dart'; +import 'package:miru_app/utils/i18n.dart'; +import 'package:miru_app/widgets/platform_widget.dart'; + +class HorizontalList extends StatefulWidget { + const HorizontalList({ + Key? key, + required this.title, + required this.onClickMore, + this.itemCount, + this.itemBuilder, + this.contentBuilder, + }) : assert( + (itemCount != null && itemBuilder != null) || contentBuilder != null, + "itemCount and itemBuilder or contentBuilder must not be null", + ), + super(key: key); + final String title; + final void Function() onClickMore; + final int? itemCount; + final Widget? Function(BuildContext, int)? itemBuilder; + final Widget Function(ScrollController)? contentBuilder; + + @override + State createState() => _HorizontalListState(); +} + +class _HorizontalListState extends State { + final ScrollController _controller = ScrollController(); + + _horzontalMove(bool left) { + _controller.animateTo( + _controller.offset + (left ? -500 : 500), + duration: const Duration(milliseconds: 500), + curve: Curves.ease, + ); + } + + Widget _buildAndroid(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Text(widget.title), + const Spacer(), + TextButton( + onPressed: widget.onClickMore, + child: Text('common.show-all'.i18n), + ) + ], + ), + const SizedBox(height: 8), + if (widget.contentBuilder != null) + widget.contentBuilder!(_controller) + else + SizedBox( + height: 186, + child: ListView.builder( + scrollDirection: Axis.horizontal, + controller: _controller, + itemCount: widget.itemCount, + itemBuilder: ((context, index) { + return Container( + width: 128, + margin: const EdgeInsets.only(right: 16), + child: widget.itemBuilder!(context, index), + ); + }), + ), + ), + const SizedBox(height: 16), + ], + ); + } + + Widget _buildDesktop(BuildContext context) { + return Column( + children: [ + Row( + children: [ + HorizontalTitle( + widget.title, + onClick: widget.onClickMore, + ), + const Spacer(), + Row( + children: [ + fluent.IconButton( + icon: const Icon(fluent.FluentIcons.chevron_left), + onPressed: () { + _horzontalMove(true); + }), + const SizedBox(width: 8), + fluent.IconButton( + icon: const Icon(fluent.FluentIcons.chevron_right), + onPressed: () { + _horzontalMove(false); + }, + ) + ], + ), + ], + ), + const SizedBox(height: 8), + if (widget.contentBuilder != null) + widget.contentBuilder!(_controller) + else + SizedBox( + height: 280, + child: ListView.builder( + scrollDirection: Axis.horizontal, + controller: _controller, + itemCount: widget.itemCount, + itemBuilder: ((context, index) { + return Container( + width: 170, + margin: const EdgeInsets.only(right: 16), + child: widget.itemBuilder!(context, index), + ); + }), + ), + ), + const SizedBox(height: 16), + ], + ); + } + + @override + Widget build(BuildContext context) { + return PlatformBuildWidget( + androidBuilder: _buildAndroid, + desktopBuilder: _buildDesktop, + ); + } +} + +class HorizontalTitle extends StatefulWidget { + const HorizontalTitle(this.text, {Key? key, required this.onClick}) + : super(key: key); + final String text; + final Function() onClick; + + @override + State createState() => _HorizontalTitleState(); +} + +class _HorizontalTitleState extends State { + bool _hoverTitle = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onHover: (event) { + setState(() { + _hoverTitle = true; + }); + }, + onExit: (event) { + setState(() { + _hoverTitle = false; + }); + }, + child: GestureDetector( + onTap: () { + widget.onClick(); + }, + child: AnimatedContainer( + padding: EdgeInsets.symmetric( + horizontal: _hoverTitle ? 20 : 0, + vertical: 10, + ), + duration: const Duration(milliseconds: 100), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8)), + color: _hoverTitle + ? fluent.FluentTheme.of(context).brightness == Brightness.light + ? const Color.fromARGB(19, 27, 26, 25) + : const Color.fromARGB(19, 186, 186, 186) + : Colors.transparent, + ), + child: Row( + children: [ + Text( + widget.text, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + const Icon( + fluent.FluentIcons.chevron_right_med, + size: 14, + ) + ], + ), + ), + ), + ); + } +}