From 700630e3173eb0b3bf4b7522aebf211c9c6d1bd5 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 18 Oct 2024 13:07:57 +0800 Subject: [PATCH] improve favorites page --- lib/components/comic.dart | 142 ++++++++++-------- lib/foundation/comic_source/models.dart | 23 ++- lib/pages/favorites/favorites_page.dart | 5 +- .../favorites/network_favorites_page.dart | 92 +++++++++++- lib/pages/favorites/side_bar.dart | 33 +++- lib/utils/tags_translation.dart | 2 +- 6 files changed, 221 insertions(+), 76 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index f774ff6..b727272 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -21,7 +21,7 @@ class ComicTile extends StatelessWidget { final VoidCallback? onTap; void _onTap() { - if(onTap != null) { + if (onTap != null) { onTap!(); return; } @@ -192,6 +192,9 @@ class ComicTile extends StatelessWidget { badge: badge, tags: comic.tags, maxLines: 2, + enableTranslate: ComicSource.find(comic.sourceKey) + ?.enableTagsTranslate ?? + false, ), ), ], @@ -274,13 +277,15 @@ class ComicTile extends StatelessWidget { } class _ComicDescription extends StatelessWidget { - const _ComicDescription( - {required this.title, - required this.subtitle, - required this.description, - this.badge, - this.maxLines = 2, - this.tags}); + const _ComicDescription({ + required this.title, + required this.subtitle, + required this.description, + required this.enableTranslate, + this.badge, + this.maxLines = 2, + this.tags, + }); final String title; final String subtitle; @@ -288,13 +293,15 @@ class _ComicDescription extends StatelessWidget { final String? badge; final List? tags; final int maxLines; + final bool enableTranslate; @override Widget build(BuildContext context) { if (tags != null) { tags!.removeWhere((element) => element.removeAllBlank == ""); } - var enableTranslate = App.locale.languageCode == 'zh'; + var enableTranslate = + App.locale.languageCode == 'zh' && this.enableTranslate; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -334,10 +341,10 @@ class _ComicDescription extends StatelessWidget { color: s == "Unavailable" ? Theme.of(context).colorScheme.errorContainer : Theme.of(context) - .colorScheme - .secondaryContainer, + .colorScheme + .secondaryContainer, borderRadius: - const BorderRadius.all(Radius.circular(8)), + const BorderRadius.all(Radius.circular(8)), ), child: Text( enableTranslate ? TagsTranslation.translateTag(s) : s, @@ -583,6 +590,7 @@ class ComicList extends StatefulWidget { this.leadingSliver, this.trailingSliver, this.errorLeading, + this.menuBuilder, }); final Future>> Function(int page)? loadPage; @@ -595,32 +603,45 @@ class ComicList extends StatefulWidget { final Widget? errorLeading; + final List Function(Comic)? menuBuilder; + @override - State createState() => _ComicListState(); + State createState() => ComicListState(); } -class _ComicListState extends State { - int? maxPage; +class ComicListState extends State { + int? _maxPage; - Map> data = {}; + final Map> _data = {}; - int page = 1; + int _page = 1; - String? error; + String? _error; - Map loading = {}; + final Map _loading = {}; - String? nextUrl; + String? _nextUrl; - Widget buildPageSelector() { + void remove(Comic c) { + if(_data[_page] == null || !_data[_page]!.remove(c)) { + for(var page in _data.values) { + if(page.remove(c)) { + break; + } + } + } + setState(() {}); + } + + Widget _buildPageSelector() { return Row( children: [ FilledButton( - onPressed: page > 1 + onPressed: _page > 1 ? () { setState(() { - error = null; - page--; + _error = null; + _page--; }); } : null, @@ -661,10 +682,10 @@ class _ComicListState extends State { context.showMessage(message: "Invalid page".tl); } else { if (page > 0 && - (maxPage == null || page <= maxPage!)) { + (_maxPage == null || page <= _maxPage!)) { setState(() { - error = null; - this.page = page; + _error = null; + this._page = page; }); } else { context.showMessage( @@ -682,18 +703,18 @@ class _ComicListState extends State { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - child: Text("Page $page / ${maxPage ?? '?'}"), + child: Text("Page $_page / ${_maxPage ?? '?'}"), ), ), ), ), ), FilledButton( - onPressed: page < (maxPage ?? (page + 1)) + onPressed: _page < (_maxPage ?? (_page + 1)) ? () { setState(() { - error = null; - page++; + _error = null; + _page++; }); } : null, @@ -703,63 +724,63 @@ class _ComicListState extends State { ).paddingVertical(8).paddingHorizontal(16); } - Widget buildSliverPageSelector() { + Widget _buildSliverPageSelector() { return SliverToBoxAdapter( - child: buildPageSelector(), + child: _buildPageSelector(), ); } - Future loadPage(int page) async { - if (loading[page] == true) { + Future _loadPage(int page) async { + if (_loading[page] == true) { return; } - loading[page] = true; + _loading[page] = true; try { if (widget.loadPage != null) { var res = await widget.loadPage!(page); if (res.success) { if (res.data.isEmpty) { - data[page] = const []; + _data[page] = const []; setState(() { - maxPage = page; + _maxPage = page; }); } else { setState(() { - data[page] = res.data; + _data[page] = res.data; if (res.subData != null && res.subData is int) { - maxPage = res.subData; + _maxPage = res.subData; } }); } } else { setState(() { - error = res.errorMessage ?? "Unknown error".tl; + _error = res.errorMessage ?? "Unknown error".tl; }); } } else { try { - while (data[page] == null) { - await fetchNext(); + while (_data[page] == null) { + await _fetchNext(); } setState(() {}); } catch (e) { setState(() { - error = e.toString(); + _error = e.toString(); }); } } } finally { - loading[page] = false; + _loading[page] = false; } } - Future fetchNext() async { - var res = await widget.loadNext!(nextUrl); - data[data.length + 1] = res.data; + Future _fetchNext() async { + var res = await widget.loadNext!(_nextUrl); + _data[_data.length + 1] = res.data; if (res.subData['next'] == null) { - maxPage = data.length; + _maxPage = _data.length; } else { - nextUrl = res.subData['next']; + _nextUrl = res.subData['next']; } } @@ -768,18 +789,18 @@ class _ComicListState extends State { if (widget.loadPage == null && widget.loadNext == null) { throw Exception("loadPage and loadNext can't be null at the same time"); } - if (error != null) { + if (_error != null) { return Column( children: [ if (widget.errorLeading != null) widget.errorLeading!, - buildPageSelector(), + _buildPageSelector(), Expanded( child: NetworkError( withAppbar: false, - message: error!, + message: _error!, retry: () { setState(() { - error = null; + _error = null; }); }, ), @@ -787,8 +808,8 @@ class _ComicListState extends State { ], ); } - if (data[page] == null) { - loadPage(page); + if (_data[_page] == null) { + _loadPage(_page); return const Center( child: CircularProgressIndicator(), ); @@ -796,9 +817,12 @@ class _ComicListState extends State { return SmoothCustomScrollView( slivers: [ if (widget.leadingSliver != null) widget.leadingSliver!, - buildSliverPageSelector(), - SliverGridComics(comics: data[page] ?? const []), - if (data[page]!.length > 6) buildSliverPageSelector(), + _buildSliverPageSelector(), + SliverGridComics( + comics: _data[_page] ?? const [], + menuBuilder: widget.menuBuilder, + ), + if (_data[_page]!.length > 6) _buildSliverPageSelector(), if (widget.trailingSliver != null) widget.trailingSliver!, ], ); diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index a4d6a44..6d2b23a 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -12,12 +12,16 @@ class Comment { final int? voteStatus; // 1: upvote, -1: downvote, 0: none static String? parseTime(dynamic value) { - if(value == null) return null; - if(value is int) { - if(value < 10000000000) { - return DateTime.fromMillisecondsSinceEpoch(value * 1000).toString().substring(0, 19); + if (value == null) return null; + if (value is int) { + if (value < 10000000000) { + return DateTime.fromMillisecondsSinceEpoch(value * 1000) + .toString() + .substring(0, 19); } else { - return DateTime.fromMillisecondsSinceEpoch(value).toString().substring(0, 19); + return DateTime.fromMillisecondsSinceEpoch(value) + .toString() + .substring(0, 19); } } return value.toString(); @@ -89,6 +93,15 @@ class Comic { description = json["description"] ?? "", maxPage = json["maxPage"], language = json["language"]; + + @override + bool operator ==(Object other) { + if (other is! Comic) return false; + return other.id == id && other.sourceKey == sourceKey; + } + + @override + int get hashCode => id.hashCode ^ sourceKey.hashCode; } class ComicDetails with HistoryMixin { diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index 96c2114..ea61004 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:venera/components/components.dart'; @@ -90,9 +92,8 @@ class _FavoritesPageState extends State { return Align( alignment: Alignment.centerLeft, child: Material( - color: context.colorScheme.surfaceContainerLow, child: SizedBox( - width: 256, + width: min(300, context.width-16), child: _LeftBar( withAppbar: true, favPage: this, diff --git a/lib/pages/favorites/network_favorites_page.dart b/lib/pages/favorites/network_favorites_page.dart index 3ca3975..99f2071 100644 --- a/lib/pages/favorites/network_favorites_page.dart +++ b/lib/pages/favorites/network_favorites_page.dart @@ -1,5 +1,59 @@ part of 'favorites_page.dart'; +// TODO: Add a menu option to delete a comic from favorites + +Future _deleteComic(String cid, String? fid, String sourceKey) async { + var source = ComicSource.find(sourceKey); + if (source == null) { + return false; + } + + var result = false; + + await showDialog( + context: App.rootContext, + builder: (context) { + bool loading = false; + return StatefulBuilder(builder: (context, setState) { + return ContentDialog( + title: "Delete".tl, + content: Text("Are you sure you want to delete this comic?".tl) + .paddingHorizontal(16), + actions: [ + Button.filled( + isLoading: loading, + color: context.colorScheme.error, + onPressed: () async { + setState(() { + loading = true; + }); + var res = await source.favoriteData!.addOrDelFavorite!( + cid, + fid ?? '', + false, + ); + if (res.success) { + context.showMessage(message: "Deleted".tl); + result = true; + context.pop(); + } else { + setState(() { + loading = false; + }); + context.showMessage(message: res.errorMessage!); + } + }, + child: Text("Confirm".tl), + ), + ], + ); + }); + }, + ); + + return result; +} + class NetworkFavoritePage extends StatelessWidget { const NetworkFavoritePage(this.data, {super.key}); @@ -14,13 +68,16 @@ class NetworkFavoritePage extends StatelessWidget { } class _NormalFavoritePage extends StatelessWidget { - const _NormalFavoritePage(this.data); + _NormalFavoritePage(this.data); final FavoriteData data; + final comicListKey = GlobalKey(); + @override Widget build(BuildContext context) { return ComicList( + key: comicListKey, leadingSliver: SliverAppbar( leading: Tooltip( message: "Folders".tl, @@ -52,6 +109,20 @@ class _NormalFavoritePage extends StatelessWidget { title: Text(data.title), ), loadPage: (i) => data.loadComic(i), + menuBuilder: (comic) { + return [ + MenuEntry( + icon: Icons.delete_outline, + text: "Remove".tl, + onClick: () async { + var res = await _deleteComic(comic.id, null, comic.sourceKey); + if (res) { + comicListKey.currentState!.remove(comic); + } + }, + ), + ]; + }, ); } } @@ -413,7 +484,7 @@ class _CreateFolderDialogState extends State<_CreateFolderDialog> { } class _FavoriteFolder extends StatelessWidget { - const _FavoriteFolder(this.data, this.folderID, this.title); + _FavoriteFolder(this.data, this.folderID, this.title); final FavoriteData data; @@ -421,13 +492,30 @@ class _FavoriteFolder extends StatelessWidget { final String title; + final comicListKey = GlobalKey(); + @override Widget build(BuildContext context) { return ComicList( + key: comicListKey, leadingSliver: SliverAppbar( title: Text(title), ), loadPage: (i) => data.loadComic(i, folderID), + menuBuilder: (comic) { + return [ + MenuEntry( + icon: Icons.delete_outline, + text: "Remove".tl, + onClick: () async { + var res = await _deleteComic(comic.id, null, comic.sourceKey); + if (res) { + comicListKey.currentState!.remove(comic); + } + }, + ), + ]; + }, ); } } diff --git a/lib/pages/favorites/side_bar.dart b/lib/pages/favorites/side_bar.dart index 11c45aa..71f09b3 100644 --- a/lib/pages/favorites/side_bar.dart +++ b/lib/pages/favorites/side_bar.dart @@ -61,13 +61,18 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { const SizedBox(width: 8), const CloseButton(), const SizedBox(width: 8), - Text("Folders".tl, style: ts.s18,), + Text( + "Folders".tl, + style: ts.s18, + ), ], ), ).paddingTop(context.padding.top), Expanded( child: ListView.builder( - padding: widget.withAppbar ? EdgeInsets.zero : EdgeInsets.only(top: context.padding.top), + padding: widget.withAppbar + ? EdgeInsets.zero + : EdgeInsets.only(top: context.padding.top), itemCount: folders.length + networkFolders.length + 2, itemBuilder: (context, index) { if (index == 0) { @@ -76,8 +81,11 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { child: Row( children: [ const SizedBox(width: 16), - const Icon(Icons.local_activity), - const SizedBox(width: 8), + Icon( + Icons.local_activity, + color: context.colorScheme.secondary, + ), + const SizedBox(width: 12), Text("Local".tl), const Spacer(), IconButton( @@ -103,12 +111,23 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { index -= folders.length; if (index == 0) { return Container( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: context.colorScheme.outlineVariant, + width: 0.6, + ), + ), + ), child: Row( children: [ const SizedBox(width: 16), - const Icon(Icons.cloud), - const SizedBox(width: 8), + Icon( + Icons.cloud, + color: context.colorScheme.secondary, + ), + const SizedBox(width: 12), Text("Network".tl), ], ), diff --git a/lib/utils/tags_translation.dart b/lib/utils/tags_translation.dart index e7b7cd8..082d6b4 100644 --- a/lib/utils/tags_translation.dart +++ b/lib/utils/tags_translation.dart @@ -56,7 +56,7 @@ extension TagsTranslation on String{ String get translateTagsToCN => _translateTags(this); static String translateTag(String tag) { - if(tag.contains(':')) { + if(tag.contains(':') && tag.indexOf(':') == tag.lastIndexOf(':')) { var [namespace, text] = tag.split(':'); return translationTagWithNamespace(text, namespace); } else {