Feat: Image favorites (#126)

* feat: 增加图片收藏

* feat: 主体图片收藏页面实现

* feat: 点击打开大图浏览

* feat: 数据结构变更

* feat: 基本完成

* feat: 翻译与bug修复

* feat: 实机测试和问题修复

* feat: jm导入, pica历史记录nhentai有问题, 一键反转

* fix: 大小写不一致, 一个htManga, 一个htmanga

* feat: 拉取收藏优化

* feat: 改成以ep为准

* feat: 兜底一些可能报错场景

* chore: 没有用到

* feat: 尽量保证和网络收藏顺序一致

* feat: 支持显示热点tag

* feat: 支持双击收藏, 不过此时禁止放大图片

* fix: 自动塞封面逻辑完善, 切换快速收藏图片立刻生效

* Refactor

* fix updateValue

* feat: 双击功能提示

* fix: 被确定取消收藏的才删除

* Refactor ImageFavoritesPage

* translate author

* feat: 功能提示改到dialog中

* fix text editing

* fix text editing

* feat: 功能提示放到邮件或长按菜单中

* fix: 修复tag过滤不生效问题

* Improve image loading

* The default value of quickCollectImage should be false.

* Refactor DragListener

* Refactor ImageFavoriteItem & ImageFavoritePhotoView

* Refactor

* Fix `ImageFavoriteManager.has`

* Fix UI

* Improve UI

---------

Co-authored-by: nyne <me@nyne.dev>
This commit is contained in:
luckyray
2025-01-15 16:07:08 +08:00
committed by GitHub
parent 213c225e1e
commit d874920c88
42 changed files with 3054 additions and 226 deletions

View File

@@ -147,13 +147,13 @@ Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
var newTags = <String>[];
for(var entry in newInfo.tags.entries) {
for (var entry in newInfo.tags.entries) {
const shouldIgnore = ['author', 'artist', 'time'];
var namespace = entry.key;
if (shouldIgnore.contains(namespace.toLowerCase())) {
continue;
}
for(var tag in entry.value) {
for (var tag in entry.value) {
newTags.add("$namespace:$tag");
}
}
@@ -305,6 +305,7 @@ Future<void> sortFolders() async {
Future<void> importNetworkFolder(
String source,
int updatePageNum,
String? folder,
String? folderID,
) async {
@@ -312,7 +313,7 @@ Future<void> importNetworkFolder(
if (comicSource == null) {
return;
}
if(folder != null && folder.isEmpty) {
if (folder != null && folder.isEmpty) {
folder = null;
}
var resultName = folder ?? comicSource.name;
@@ -324,7 +325,7 @@ Future<void> importNetworkFolder(
return;
}
}
if(!exists) {
if (!exists) {
LocalFavoritesManager().createFolder(resultName);
LocalFavoritesManager().linkFolderToNetwork(
resultName,
@@ -332,37 +333,46 @@ Future<void> importNetworkFolder(
folderID ?? "",
);
}
bool isOldToNewSort = comicSource.favoriteData?.isOldToNewSort ?? false;
var current = 0;
int receivedComics = 0;
int requestCount = 0;
var isFinished = false;
int maxPage = 1;
List<FavoriteItem> comics = [];
String? next;
// 如果是从旧到新, 先取一下maxPage
if (isOldToNewSort) {
var res = await comicSource.favoriteData?.loadComic!(1, folderID);
maxPage = res?.subData ?? 1;
}
Future<void> fetchNext() async {
var retry = 3;
while (true) {
while (updatePageNum > requestCount && !isFinished) {
try {
if (comicSource.favoriteData?.loadComic != null) {
next ??= '1';
// 从旧到新的情况下, 假设有10页, 更新3页, 则从第8页开始, 8, 9, 10 三页
next ??=
isOldToNewSort ? (maxPage - updatePageNum + 1).toString() : '1';
var page = int.parse(next!);
var res = await comicSource.favoriteData!.loadComic!(page, folderID);
var count = 0;
receivedComics += res.data.length;
for (var c in res.data) {
var result = LocalFavoritesManager().addComic(
resultName,
FavoriteItem(
if (!LocalFavoritesManager()
.comicExists(resultName, c.id, ComicType(source.hashCode))) {
count++;
comics.add(FavoriteItem(
id: c.id,
name: c.title,
coverPath: c.cover,
type: ComicType(source.hashCode),
author: c.subtitle ?? '',
tags: c.tags ?? [],
),
);
if (result) {
count++;
));
}
}
requestCount++;
current += count;
if (res.data.isEmpty || res.subData == page) {
isFinished = true;
@@ -373,22 +383,22 @@ Future<void> importNetworkFolder(
} else if (comicSource.favoriteData?.loadNext != null) {
var res = await comicSource.favoriteData!.loadNext!(next, folderID);
var count = 0;
receivedComics += res.data.length;
for (var c in res.data) {
var result = LocalFavoritesManager().addComic(
resultName,
FavoriteItem(
if (!LocalFavoritesManager()
.comicExists(resultName, c.id, ComicType(source.hashCode))) {
count++;
comics.add(FavoriteItem(
id: c.id,
name: c.title,
coverPath: c.cover,
type: ComicType(source.hashCode),
author: c.subtitle ?? '',
tags: c.tags ?? [],
),
);
if (result) {
count++;
));
}
}
requestCount++;
current += count;
if (res.data.isEmpty || res.subData == null) {
isFinished = true;
@@ -408,6 +418,8 @@ Future<void> importNetworkFolder(
continue;
}
}
// 跳出循环, 表示已经完成, 强制为 true, 避免死循环
isFinished = true;
}
bool isCanceled = false;
@@ -415,6 +427,7 @@ Future<void> importNetworkFolder(
bool isErrored() => errorMsg != null;
void Function()? updateDialog;
void Function()? closeDialog;
showDialog(
context: App.rootContext,
@@ -422,6 +435,7 @@ Future<void> importNetworkFolder(
return StatefulBuilder(
builder: (context, setState) {
updateDialog = () => setState(() {});
closeDialog = () => Navigator.pop(context);
return ContentDialog(
title: isFinished
? "Finished".tl
@@ -437,8 +451,11 @@ Future<void> importNetworkFolder(
value: isFinished ? 1 : null,
),
const SizedBox(height: 4),
Text("Imported @c comics".tlParams({
"c": current,
Text("Imported @a comics, loaded @b pages, received @c comics"
.tlParams({
"a": current,
"b": requestCount,
"c": receivedComics,
})),
const SizedBox(height: 4),
if (isErrored()) Text("Error: $errorMsg"),
@@ -476,4 +493,18 @@ Future<void> importNetworkFolder(
break;
}
}
try {
if (appdata.settings['newFavoriteAddTo'] == "start" && !isOldToNewSort) {
// 如果是插到最前, 并且是从新到旧, 反转一下
comics = comics.reversed.toList();
}
for (var c in comics) {
LocalFavoritesManager().addComic(resultName, c);
}
// 延迟一点, 让用户看清楚到底新增了多少
await Future.delayed(const Duration(milliseconds: 500));
closeDialog?.call();
} catch (e, stackTrace) {
Log.error("Unhandled Exception", e.toString(), stackTrace);
}
}

View File

@@ -11,6 +11,7 @@ import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/network/download.dart';
import 'package:venera/pages/comic_page.dart';
@@ -35,7 +36,7 @@ class FavoritesPage extends StatefulWidget {
State<FavoritesPage> createState() => _FavoritesPageState();
}
class _FavoritesPageState extends State<FavoritesPage> {
class _FavoritesPageState extends State<FavoritesPage> {
String? folder;
bool isNetwork = false;
@@ -58,7 +59,7 @@ class _FavoritesPageState extends State<FavoritesPage> {
@override
void initState() {
var data = appdata.implicitData['favoriteFolder'];
if(data != null){
if (data != null) {
folder = data['name'];
isNetwork = data['isNetwork'] ?? false;
}
@@ -101,7 +102,7 @@ class _FavoritesPageState extends State<FavoritesPage> {
alignment: Alignment.centerLeft,
child: Material(
child: SizedBox(
width: min(300, context.width-16),
width: min(300, context.width - 16),
child: _LeftBar(
withAppbar: true,
favPage: this,
@@ -153,14 +154,16 @@ class _FavoritesPageState extends State<FavoritesPage> {
);
}
if (!isNetwork) {
return _LocalFavoritesPage(folder: folder!, key: PageStorageKey("local_$folder"));
return _LocalFavoritesPage(
folder: folder!, key: PageStorageKey("local_$folder"));
} else {
var favoriteData = getFavoriteDataOrNull(folder!);
if (favoriteData == null) {
folder = null;
return buildBody();
} else {
return NetworkFavoritePage(favoriteData, key: PageStorageKey("network_$folder"));
return NetworkFavoritePage(favoriteData,
key: PageStorageKey("network_$folder"));
}
}
}

View File

@@ -136,17 +136,17 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
message: "Sync".tl,
child: Flyout(
flyoutBuilder: (context) {
var sourceName = ComicSource.find(networkSource!)?.name ??
networkSource!;
var text = "The folder is Linked to @source".tlParams({
"source": sourceName,
});
if (networkFolder != null && networkFolder!.isNotEmpty) {
text += "\n${"Source Folder".tl}: $networkFolder";
}
final GlobalKey<_SelectUpdatePageNumState>
selectUpdatePageNumKey =
GlobalKey<_SelectUpdatePageNumState>();
var updatePageWidget = _SelectUpdatePageNum(
networkSource: networkSource!,
networkFolder: networkFolder,
key: selectUpdatePageNumKey,
);
return FlyoutContent(
title: "Sync".tl,
content: Text(text),
content: updatePageWidget,
actions: [
Button.filled(
child: Text("Update".tl),
@@ -154,6 +154,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
context.pop();
importNetworkFolder(
networkSource!,
selectUpdatePageNumKey
.currentState!.updatePageNum,
widget.folder,
networkFolder!,
).then(
@@ -741,6 +743,17 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
);
},
),
IconButton(
icon: const Icon(Icons.swap_vert),
onPressed: () {
setState(() {
comics = comics.reversed.toList();
changed = true;
showToast(
message: "Reversed successfully".tl, context: context);
});
},
),
],
),
body: ReorderableBuilder<FavoriteItem>(
@@ -776,3 +789,76 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
);
}
}
class _SelectUpdatePageNum extends StatefulWidget {
const _SelectUpdatePageNum({
required this.networkSource,
this.networkFolder,
super.key,
});
final String? networkFolder;
final String networkSource;
@override
State<_SelectUpdatePageNum> createState() => _SelectUpdatePageNumState();
}
class _SelectUpdatePageNumState extends State<_SelectUpdatePageNum> {
int updatePageNum = 9999999;
String get _allPageText => 'All'.tl;
List<String> get pageNumList =>
['1', '2', '3', '5', '10', '20', '50', '100', '200', _allPageText];
@override
void initState() {
updatePageNum =
appdata.implicitData["local_favorites_update_page_num"] ?? 9999999;
super.initState();
}
@override
Widget build(BuildContext context) {
var source = ComicSource.find(widget.networkSource);
var sourceName = source?.name ?? widget.networkSource;
var text = "The folder is Linked to @source".tlParams({
"source": sourceName,
});
if (widget.networkFolder != null && widget.networkFolder!.isNotEmpty) {
text += "\n${"Source Folder".tl}: ${widget.networkFolder}";
}
return Column(
children: [
Row(
children: [Text(text)],
),
Row(
children: [
Text("Update the page number by the latest collection".tl),
Spacer(),
Select(
current: updatePageNum.toString() == '9999999'
? _allPageText
: updatePageNum.toString(),
values: pageNumList,
minWidth: 48,
onTap: (index) {
setState(() {
updatePageNum = int.parse(pageNumList[index] == _allPageText
? '9999999'
: pageNumList[index]);
appdata.implicitData["local_favorites_update_page_num"] =
updatePageNum;
appdata.writeImplicitData();
});
},
)
],
),
],
);
}
}

View File

@@ -20,8 +20,7 @@ Future<bool> _deleteComic(
return StatefulBuilder(builder: (context, setState) {
return ContentDialog(
title: "Remove".tl,
content: Text("Remove comic from favorite?".tl)
.paddingHorizontal(16),
content: Text("Remove comic from favorite?".tl).paddingHorizontal(16),
actions: [
Button.filled(
isLoading: loading,
@@ -94,9 +93,8 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
return ComicList(
key: comicListKey,
leadingSliver: SliverAppbar(
style: context.width < changePoint
? AppbarStyle.shadow
: AppbarStyle.blur,
style:
context.width < changePoint ? AppbarStyle.shadow : AppbarStyle.blur,
leading: Tooltip(
message: "Folders".tl,
child: context.width <= _kTwoPanelChangeWidth
@@ -117,7 +115,7 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
icon: Icons.sync,
text: "Convert to local".tl,
onClick: () {
importNetworkFolder(widget.data.key, null, null);
importNetworkFolder(widget.data.key, 9999999, null, null);
},
)
]),
@@ -215,9 +213,8 @@ class _MultiFolderFavoritesPageState extends State<_MultiFolderFavoritesPage> {
@override
Widget build(BuildContext context) {
var sliverAppBar = SliverAppbar(
style: context.width < changePoint
? AppbarStyle.shadow
: AppbarStyle.blur,
style:
context.width < changePoint ? AppbarStyle.shadow : AppbarStyle.blur,
leading: Tooltip(
message: "Folders".tl,
child: context.width <= _kTwoPanelChangeWidth
@@ -431,8 +428,7 @@ class _FolderTile extends StatelessWidget {
return StatefulBuilder(builder: (context, setState) {
return ContentDialog(
title: "Delete".tl,
content: Text("Delete folder?".tl)
.paddingHorizontal(16),
content: Text("Delete folder?".tl).paddingHorizontal(16),
actions: [
Button.filled(
isLoading: loading,
@@ -558,7 +554,7 @@ class _FavoriteFolder extends StatelessWidget {
icon: Icons.sync,
text: "Convert to local".tl,
onClick: () {
importNetworkFolder(data.key, title, folderID);
importNetworkFolder(data.key, 9999999, title, folderID);
},
)
]),