From 30a1c806cdeb7def095a4bdaf418c96425b45002 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 16 Nov 2024 16:51:56 +0800 Subject: [PATCH] Convert network folder to local --- assets/translation.json | 10 +- lib/components/flyout.dart | 4 + lib/foundation/favorites.dart | 54 +++++- lib/pages/favorites/favorite_actions.dart | 175 ++++++++++++++++++ lib/pages/favorites/favorites_page.dart | 1 + lib/pages/favorites/local_favorites_page.dart | 51 +++++ .../favorites/network_favorites_page.dart | 22 +++ 7 files changed, 308 insertions(+), 9 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 0fd80a4..7612940 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -212,7 +212,10 @@ "Select an image on screen": "选择屏幕上的图片", "Added @count comics to download queue.": "已添加 @count 本漫画到下载队列", "Ignore Certificate Errors": "忽略证书错误", - "Authorization Required": "需要身份验证" + "Authorization Required": "需要身份验证", + "Sync": "同步", + "The folder is Linked to @source": "文件夹已关联到 @source", + "Source Folder": "源收藏夹" }, "zh_TW": { "Home": "首頁", @@ -427,6 +430,9 @@ "Select an image on screen": "選擇屏幕上的圖片", "Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列", "Ignore Certificate Errors": "忽略證書錯誤", - "Authorization Required": "需要身份驗證" + "Authorization Required": "需要身份驗證", + "Sync": "同步", + "The folder is Linked to @source": "文件夾已關聯到 @source", + "Source Folder": "源收藏夾" } } \ No newline at end of file diff --git a/lib/components/flyout.dart b/lib/components/flyout.dart index 8bd131e..d183dc4 100644 --- a/lib/components/flyout.dart +++ b/lib/components/flyout.dart @@ -51,6 +51,10 @@ class Flyout extends StatefulWidget { @override State createState() => FlyoutState(); + + static FlyoutState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } } class FlyoutState extends State { diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index eef3fa3..d6f245c 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -346,6 +346,32 @@ class LocalFavoritesManager with ChangeNotifier { return name; } + void linkFolderToNetwork(String folder, String source, String networkFolder) { + _db.execute(""" + insert or replace into folder_sync (folder_name, source_key, source_folder) + values (?, ?, ?); + """, [folder, source, networkFolder]); + } + + bool isLinkedToNetworkFolder(String folder, String source, String networkFolder) { + var res = _db.select(""" + select * from folder_sync + where folder_name == ? and source_key == ? and source_folder == ?; + """, [folder, source, networkFolder]); + return res.isNotEmpty; + } + + (String?, String?) findLinked(String folder) { + var res = _db.select(""" + select * from folder_sync + where folder_name == ?; + """, [folder]); + if (res.isEmpty) { + return (null, null); + } + return (res.first["source_key"], res.first["source_folder"]); + } + bool comicExists(String folder, String id, ComicType type) { var res = _db.select(""" select * from "$folder" @@ -365,20 +391,19 @@ class LocalFavoritesManager with ChangeNotifier { return FavoriteItem.fromRow(res.first); } - /// add comic to a folder - /// - /// This method will download cover to local, to avoid problems like changing url - void addComic(String folder, FavoriteItem comic, [int? order]) async { + /// add comic to a folder. + /// return true if success, false if already exists + bool addComic(String folder, FavoriteItem comic, [int? order]) { _modifiedAfterLastCache = true; if (!existsFolder(folder)) { throw Exception("Folder does not exists"); } var res = _db.select(""" select * from "$folder" - where id == '${comic.id}'; - """); + where id == ? and type == ?; + """, [comic.id, comic.type.value]); if (res.isNotEmpty) { - return; + return false; } final params = [ comic.id, @@ -406,6 +431,7 @@ class LocalFavoritesManager with ChangeNotifier { """, [...params, minValue(folder) - 1]); } notifyListeners(); + return true; } /// delete a folder @@ -414,6 +440,10 @@ class LocalFavoritesManager with ChangeNotifier { _db.execute(""" drop table "$name"; """); + _db.execute(""" + delete from folder_order + where folder_name == ?; + """, [name]); notifyListeners(); } @@ -461,6 +491,16 @@ class LocalFavoritesManager with ChangeNotifier { ALTER TABLE "$before" RENAME TO "$after"; """); + _db.execute(""" + update folder_order + set folder_name = ? + where folder_name == ?; + """, [after, before]); + _db.execute(""" + update folder_sync + set folder_name = ? + where folder_name == ?; + """, [after, before]); notifyListeners(); } diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index 1cb8847..e19686f 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -288,3 +288,178 @@ Future sortFolders() async { LocalFavoritesManager().updateOrder(folders); } + +Future importNetworkFolder( + String source, + String? folder, + String? folderID, +) async { + var comicSource = ComicSource.find(source); + if (comicSource == null) { + return; + } + if(folder != null && folder.isEmpty) { + folder = null; + } + var resultName = folder ?? comicSource.name; + var exists = LocalFavoritesManager().existsFolder(resultName); + if (exists) { + if (!LocalFavoritesManager() + .isLinkedToNetworkFolder(resultName, source, folderID ?? "")) { + App.rootContext.showMessage(message: "Folder already exists".tl); + return; + } + } + if(!exists) { + LocalFavoritesManager().createFolder(resultName); + LocalFavoritesManager().linkFolderToNetwork( + resultName, + source, + folderID ?? "", + ); + } + + var current = 0; + var isFinished = false; + String? next; + + Future fetchNext() async { + var retry = 3; + + while (true) { + try { + if (comicSource.favoriteData?.loadComic != null) { + next ??= '1'; + var page = int.parse(next!); + var res = await comicSource.favoriteData!.loadComic!(page, folderID); + var count = 0; + for (var c in res.data) { + var result = LocalFavoritesManager().addComic( + resultName, + FavoriteItem( + id: c.id, + name: c.title, + coverPath: c.cover, + type: ComicType(source.hashCode), + author: c.subtitle ?? '', + tags: c.tags ?? [], + ), + ); + if (result) { + count++; + } + } + current += count; + if (res.data.isEmpty || res.subData == page) { + isFinished = true; + next = null; + } else { + next = (page + 1).toString(); + } + } else if (comicSource.favoriteData?.loadNext != null) { + var res = await comicSource.favoriteData!.loadNext!(next, folderID); + var count = 0; + for (var c in res.data) { + var result = LocalFavoritesManager().addComic( + resultName, + FavoriteItem( + id: c.id, + name: c.title, + coverPath: c.cover, + type: ComicType(source.hashCode), + author: c.subtitle ?? '', + tags: c.tags ?? [], + ), + ); + if (result) { + count++; + } + } + current += count; + if (res.data.isEmpty || res.subData == null) { + isFinished = true; + next = null; + } else { + next = res.subData; + } + } else { + throw "Unsupported source"; + } + return; + } catch (e) { + retry--; + if (retry == 0) { + rethrow; + } + continue; + } + } + } + + bool isCanceled = false; + String? errorMsg; + bool isErrored() => errorMsg != null; + + void Function()? updateDialog; + + showDialog( + context: App.rootContext, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + updateDialog = () => setState(() {}); + return ContentDialog( + title: isFinished + ? "Finished".tl + : isErrored() + ? "Error".tl + : "Importing".tl, + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + LinearProgressIndicator( + value: isFinished ? 1 : null, + ), + const SizedBox(height: 4), + Text("Imported @c comics".tlParams({ + "c": current, + })), + const SizedBox(height: 4), + if (isErrored()) Text("Error: $errorMsg"), + ], + ).paddingHorizontal(16), + actions: [ + Button.filled( + color: (isFinished || isErrored()) + ? null + : context.colorScheme.error, + onPressed: () { + isCanceled = true; + context.pop(); + }, + child: (isFinished || isErrored()) + ? Text("OK".tl) + : Text("Cancel".tl), + ), + ], + ); + }, + ); + }, + ).then((_) { + isCanceled = true; + }); + + while (!isFinished && !isCanceled) { + try { + await fetchNext(); + updateDialog?.call(); + } catch (e) { + errorMsg = e.toString(); + updateDialog?.call(); + break; + } + } +} diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index c380a9a..f815506 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:venera/components/components.dart'; diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 4bbef38..ec9e070 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -14,6 +14,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { late List comics; + String? networkSource; + String? networkFolder; + void updateComics() { setState(() { comics = LocalFavoritesManager().getAllComics(widget.folder); @@ -24,6 +27,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { void initState() { favPage = context.findAncestorStateOfType<_FavoritesPageState>()!; comics = LocalFavoritesManager().getAllComics(widget.folder); + var (a, b) = LocalFavoritesManager().findLinked(widget.folder); + networkSource = a; + networkFolder = b; super.initState(); } @@ -49,6 +55,51 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { child: Text(favPage.folder ?? "Unselected".tl), ), actions: [ + if (networkSource != null) + Tooltip( + 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"; + } + return FlyoutContent( + title: "Sync".tl, + content: Text(text), + actions: [ + Button.filled( + child: Text("Update".tl), + onPressed: () { + context.pop(); + importNetworkFolder( + networkSource!, + widget.folder, + networkFolder!, + ).then( + (value) { + updateComics(); + }, + ); + }, + ), + ], + ); + }, + child: Builder(builder: (context) { + return IconButton( + icon: const Icon(Icons.sync), + onPressed: () { + Flyout.of(context).show(); + }, + ); + }), + ), + ), MenuButton( entries: [ MenuEntry( diff --git a/lib/pages/favorites/network_favorites_page.dart b/lib/pages/favorites/network_favorites_page.dart index d08d1fc..b842d55 100644 --- a/lib/pages/favorites/network_favorites_page.dart +++ b/lib/pages/favorites/network_favorites_page.dart @@ -108,6 +108,17 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> { onTap: context.width < _kTwoPanelChangeWidth ? showFolders : null, child: Text(widget.data.title), ), + actions: [ + MenuButton(entries: [ + MenuEntry( + icon: Icons.sync, + text: "Convert to local".tl, + onClick: () { + importNetworkFolder(widget.data.key, null, null); + }, + ) + ]), + ], ), errorLeading: Appbar( leading: Tooltip( @@ -533,6 +544,17 @@ class _FavoriteFolder extends StatelessWidget { key: comicListKey, leadingSliver: SliverAppbar( title: Text(title), + actions: [ + MenuButton(entries: [ + MenuEntry( + icon: Icons.sync, + text: "Convert to local".tl, + onClick: () { + importNetworkFolder(data.key, title, folderID); + }, + ) + ]), + ], ), errorLeading: Appbar( title: Text(title),