improve favorites page

This commit is contained in:
nyne
2024-10-18 13:07:57 +08:00
parent 30b3256eec
commit 700630e317
6 changed files with 221 additions and 76 deletions

View File

@@ -21,7 +21,7 @@ class ComicTile extends StatelessWidget {
final VoidCallback? onTap; final VoidCallback? onTap;
void _onTap() { void _onTap() {
if(onTap != null) { if (onTap != null) {
onTap!(); onTap!();
return; return;
} }
@@ -192,6 +192,9 @@ class ComicTile extends StatelessWidget {
badge: badge, badge: badge,
tags: comic.tags, tags: comic.tags,
maxLines: 2, maxLines: 2,
enableTranslate: ComicSource.find(comic.sourceKey)
?.enableTagsTranslate ??
false,
), ),
), ),
], ],
@@ -274,13 +277,15 @@ class ComicTile extends StatelessWidget {
} }
class _ComicDescription extends StatelessWidget { class _ComicDescription extends StatelessWidget {
const _ComicDescription( const _ComicDescription({
{required this.title, required this.title,
required this.subtitle, required this.subtitle,
required this.description, required this.description,
this.badge, required this.enableTranslate,
this.maxLines = 2, this.badge,
this.tags}); this.maxLines = 2,
this.tags,
});
final String title; final String title;
final String subtitle; final String subtitle;
@@ -288,13 +293,15 @@ class _ComicDescription extends StatelessWidget {
final String? badge; final String? badge;
final List<String>? tags; final List<String>? tags;
final int maxLines; final int maxLines;
final bool enableTranslate;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (tags != null) { if (tags != null) {
tags!.removeWhere((element) => element.removeAllBlank == ""); tags!.removeWhere((element) => element.removeAllBlank == "");
} }
var enableTranslate = App.locale.languageCode == 'zh'; var enableTranslate =
App.locale.languageCode == 'zh' && this.enableTranslate;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@@ -334,10 +341,10 @@ class _ComicDescription extends StatelessWidget {
color: s == "Unavailable" color: s == "Unavailable"
? Theme.of(context).colorScheme.errorContainer ? Theme.of(context).colorScheme.errorContainer
: Theme.of(context) : Theme.of(context)
.colorScheme .colorScheme
.secondaryContainer, .secondaryContainer,
borderRadius: borderRadius:
const BorderRadius.all(Radius.circular(8)), const BorderRadius.all(Radius.circular(8)),
), ),
child: Text( child: Text(
enableTranslate ? TagsTranslation.translateTag(s) : s, enableTranslate ? TagsTranslation.translateTag(s) : s,
@@ -583,6 +590,7 @@ class ComicList extends StatefulWidget {
this.leadingSliver, this.leadingSliver,
this.trailingSliver, this.trailingSliver,
this.errorLeading, this.errorLeading,
this.menuBuilder,
}); });
final Future<Res<List<Comic>>> Function(int page)? loadPage; final Future<Res<List<Comic>>> Function(int page)? loadPage;
@@ -595,32 +603,45 @@ class ComicList extends StatefulWidget {
final Widget? errorLeading; final Widget? errorLeading;
final List<MenuEntry> Function(Comic)? menuBuilder;
@override @override
State<ComicList> createState() => _ComicListState(); State<ComicList> createState() => ComicListState();
} }
class _ComicListState extends State<ComicList> { class ComicListState extends State<ComicList> {
int? maxPage; int? _maxPage;
Map<int, List<Comic>> data = {}; final Map<int, List<Comic>> _data = {};
int page = 1; int _page = 1;
String? error; String? _error;
Map<int, bool> loading = {}; final Map<int, bool> _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( return Row(
children: [ children: [
FilledButton( FilledButton(
onPressed: page > 1 onPressed: _page > 1
? () { ? () {
setState(() { setState(() {
error = null; _error = null;
page--; _page--;
}); });
} }
: null, : null,
@@ -661,10 +682,10 @@ class _ComicListState extends State<ComicList> {
context.showMessage(message: "Invalid page".tl); context.showMessage(message: "Invalid page".tl);
} else { } else {
if (page > 0 && if (page > 0 &&
(maxPage == null || page <= maxPage!)) { (_maxPage == null || page <= _maxPage!)) {
setState(() { setState(() {
error = null; _error = null;
this.page = page; this._page = page;
}); });
} else { } else {
context.showMessage( context.showMessage(
@@ -682,18 +703,18 @@ class _ComicListState extends State<ComicList> {
child: Padding( child: Padding(
padding: padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 6), const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Text("Page $page / ${maxPage ?? '?'}"), child: Text("Page $_page / ${_maxPage ?? '?'}"),
), ),
), ),
), ),
), ),
), ),
FilledButton( FilledButton(
onPressed: page < (maxPage ?? (page + 1)) onPressed: _page < (_maxPage ?? (_page + 1))
? () { ? () {
setState(() { setState(() {
error = null; _error = null;
page++; _page++;
}); });
} }
: null, : null,
@@ -703,63 +724,63 @@ class _ComicListState extends State<ComicList> {
).paddingVertical(8).paddingHorizontal(16); ).paddingVertical(8).paddingHorizontal(16);
} }
Widget buildSliverPageSelector() { Widget _buildSliverPageSelector() {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: buildPageSelector(), child: _buildPageSelector(),
); );
} }
Future<void> loadPage(int page) async { Future<void> _loadPage(int page) async {
if (loading[page] == true) { if (_loading[page] == true) {
return; return;
} }
loading[page] = true; _loading[page] = true;
try { try {
if (widget.loadPage != null) { if (widget.loadPage != null) {
var res = await widget.loadPage!(page); var res = await widget.loadPage!(page);
if (res.success) { if (res.success) {
if (res.data.isEmpty) { if (res.data.isEmpty) {
data[page] = const []; _data[page] = const [];
setState(() { setState(() {
maxPage = page; _maxPage = page;
}); });
} else { } else {
setState(() { setState(() {
data[page] = res.data; _data[page] = res.data;
if (res.subData != null && res.subData is int) { if (res.subData != null && res.subData is int) {
maxPage = res.subData; _maxPage = res.subData;
} }
}); });
} }
} else { } else {
setState(() { setState(() {
error = res.errorMessage ?? "Unknown error".tl; _error = res.errorMessage ?? "Unknown error".tl;
}); });
} }
} else { } else {
try { try {
while (data[page] == null) { while (_data[page] == null) {
await fetchNext(); await _fetchNext();
} }
setState(() {}); setState(() {});
} catch (e) { } catch (e) {
setState(() { setState(() {
error = e.toString(); _error = e.toString();
}); });
} }
} }
} finally { } finally {
loading[page] = false; _loading[page] = false;
} }
} }
Future<void> fetchNext() async { Future<void> _fetchNext() async {
var res = await widget.loadNext!(nextUrl); var res = await widget.loadNext!(_nextUrl);
data[data.length + 1] = res.data; _data[_data.length + 1] = res.data;
if (res.subData['next'] == null) { if (res.subData['next'] == null) {
maxPage = data.length; _maxPage = _data.length;
} else { } else {
nextUrl = res.subData['next']; _nextUrl = res.subData['next'];
} }
} }
@@ -768,18 +789,18 @@ class _ComicListState extends State<ComicList> {
if (widget.loadPage == null && widget.loadNext == null) { if (widget.loadPage == null && widget.loadNext == null) {
throw Exception("loadPage and loadNext can't be null at the same time"); throw Exception("loadPage and loadNext can't be null at the same time");
} }
if (error != null) { if (_error != null) {
return Column( return Column(
children: [ children: [
if (widget.errorLeading != null) widget.errorLeading!, if (widget.errorLeading != null) widget.errorLeading!,
buildPageSelector(), _buildPageSelector(),
Expanded( Expanded(
child: NetworkError( child: NetworkError(
withAppbar: false, withAppbar: false,
message: error!, message: _error!,
retry: () { retry: () {
setState(() { setState(() {
error = null; _error = null;
}); });
}, },
), ),
@@ -787,8 +808,8 @@ class _ComicListState extends State<ComicList> {
], ],
); );
} }
if (data[page] == null) { if (_data[_page] == null) {
loadPage(page); _loadPage(_page);
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
@@ -796,9 +817,12 @@ class _ComicListState extends State<ComicList> {
return SmoothCustomScrollView( return SmoothCustomScrollView(
slivers: [ slivers: [
if (widget.leadingSliver != null) widget.leadingSliver!, if (widget.leadingSliver != null) widget.leadingSliver!,
buildSliverPageSelector(), _buildSliverPageSelector(),
SliverGridComics(comics: data[page] ?? const []), SliverGridComics(
if (data[page]!.length > 6) buildSliverPageSelector(), comics: _data[_page] ?? const [],
menuBuilder: widget.menuBuilder,
),
if (_data[_page]!.length > 6) _buildSliverPageSelector(),
if (widget.trailingSliver != null) widget.trailingSliver!, if (widget.trailingSliver != null) widget.trailingSliver!,
], ],
); );

View File

@@ -12,12 +12,16 @@ class Comment {
final int? voteStatus; // 1: upvote, -1: downvote, 0: none final int? voteStatus; // 1: upvote, -1: downvote, 0: none
static String? parseTime(dynamic value) { static String? parseTime(dynamic value) {
if(value == null) return null; if (value == null) return null;
if(value is int) { if (value is int) {
if(value < 10000000000) { if (value < 10000000000) {
return DateTime.fromMillisecondsSinceEpoch(value * 1000).toString().substring(0, 19); return DateTime.fromMillisecondsSinceEpoch(value * 1000)
.toString()
.substring(0, 19);
} else { } else {
return DateTime.fromMillisecondsSinceEpoch(value).toString().substring(0, 19); return DateTime.fromMillisecondsSinceEpoch(value)
.toString()
.substring(0, 19);
} }
} }
return value.toString(); return value.toString();
@@ -89,6 +93,15 @@ class Comic {
description = json["description"] ?? "", description = json["description"] ?? "",
maxPage = json["maxPage"], maxPage = json["maxPage"],
language = json["language"]; 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 { class ComicDetails with HistoryMixin {

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
import 'package:venera/components/components.dart'; import 'package:venera/components/components.dart';
@@ -90,9 +92,8 @@ class _FavoritesPageState extends State<FavoritesPage> {
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Material( child: Material(
color: context.colorScheme.surfaceContainerLow,
child: SizedBox( child: SizedBox(
width: 256, width: min(300, context.width-16),
child: _LeftBar( child: _LeftBar(
withAppbar: true, withAppbar: true,
favPage: this, favPage: this,

View File

@@ -1,5 +1,59 @@
part of 'favorites_page.dart'; part of 'favorites_page.dart';
// TODO: Add a menu option to delete a comic from favorites
Future<bool> _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 { class NetworkFavoritePage extends StatelessWidget {
const NetworkFavoritePage(this.data, {super.key}); const NetworkFavoritePage(this.data, {super.key});
@@ -14,13 +68,16 @@ class NetworkFavoritePage extends StatelessWidget {
} }
class _NormalFavoritePage extends StatelessWidget { class _NormalFavoritePage extends StatelessWidget {
const _NormalFavoritePage(this.data); _NormalFavoritePage(this.data);
final FavoriteData data; final FavoriteData data;
final comicListKey = GlobalKey<ComicListState>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ComicList( return ComicList(
key: comicListKey,
leadingSliver: SliverAppbar( leadingSliver: SliverAppbar(
leading: Tooltip( leading: Tooltip(
message: "Folders".tl, message: "Folders".tl,
@@ -52,6 +109,20 @@ class _NormalFavoritePage extends StatelessWidget {
title: Text(data.title), title: Text(data.title),
), ),
loadPage: (i) => data.loadComic(i), 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 { class _FavoriteFolder extends StatelessWidget {
const _FavoriteFolder(this.data, this.folderID, this.title); _FavoriteFolder(this.data, this.folderID, this.title);
final FavoriteData data; final FavoriteData data;
@@ -421,13 +492,30 @@ class _FavoriteFolder extends StatelessWidget {
final String title; final String title;
final comicListKey = GlobalKey<ComicListState>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ComicList( return ComicList(
key: comicListKey,
leadingSliver: SliverAppbar( leadingSliver: SliverAppbar(
title: Text(title), title: Text(title),
), ),
loadPage: (i) => data.loadComic(i, folderID), 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);
}
},
),
];
},
); );
} }
} }

View File

@@ -61,13 +61,18 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
const SizedBox(width: 8), const SizedBox(width: 8),
const CloseButton(), const CloseButton(),
const SizedBox(width: 8), const SizedBox(width: 8),
Text("Folders".tl, style: ts.s18,), Text(
"Folders".tl,
style: ts.s18,
),
], ],
), ),
).paddingTop(context.padding.top), ).paddingTop(context.padding.top),
Expanded( Expanded(
child: ListView.builder( 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, itemCount: folders.length + networkFolders.length + 2,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
@@ -76,8 +81,11 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 16), const SizedBox(width: 16),
const Icon(Icons.local_activity), Icon(
const SizedBox(width: 8), Icons.local_activity,
color: context.colorScheme.secondary,
),
const SizedBox(width: 12),
Text("Local".tl), Text("Local".tl),
const Spacer(), const Spacer(),
IconButton( IconButton(
@@ -103,12 +111,23 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
index -= folders.length; index -= folders.length;
if (index == 0) { if (index == 0) {
return Container( 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( child: Row(
children: [ children: [
const SizedBox(width: 16), const SizedBox(width: 16),
const Icon(Icons.cloud), Icon(
const SizedBox(width: 8), Icons.cloud,
color: context.colorScheme.secondary,
),
const SizedBox(width: 12),
Text("Network".tl), Text("Network".tl),
], ],
), ),

View File

@@ -56,7 +56,7 @@ extension TagsTranslation on String{
String get translateTagsToCN => _translateTags(this); String get translateTagsToCN => _translateTags(this);
static String translateTag(String tag) { static String translateTag(String tag) {
if(tag.contains(':')) { if(tag.contains(':') && tag.indexOf(':') == tag.lastIndexOf(':')) {
var [namespace, text] = tag.split(':'); var [namespace, text] = tag.split(':');
return translationTagWithNamespace(text, namespace); return translationTagWithNamespace(text, namespace);
} else { } else {