From 69befb9a843b47b597f9dc12c489cf74c29b503c Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 28 Oct 2024 15:59:17 +0800 Subject: [PATCH] io utils; single favorite folder exporting and importing --- assets/translation.json | 10 ++- lib/components/comic.dart | 13 +++ lib/foundation/favorites.dart | 72 ++++++++++++++- lib/network/images.dart | 2 +- lib/pages/comic_page.dart | 2 +- lib/pages/comic_source_page.dart | 16 ++-- lib/pages/favorites/favorite_actions.dart | 18 +++- lib/pages/favorites/favorites_page.dart | 9 +- lib/pages/favorites/local_favorites_page.dart | 15 +++- lib/pages/favorites/side_bar.dart | 2 + lib/pages/settings/app.dart | 7 +- lib/pages/settings/settings_page.dart | 2 - lib/utils/io.dart | 52 ++++++++--- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 88 ++++++++++++++++--- pubspec.yaml | 3 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 20 files changed, 276 insertions(+), 46 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index dda65ab..a3bae80 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -126,7 +126,10 @@ "Network Favorite Pages": "网络收藏页面", "Block": "屏蔽", "Add new favorite to": "添加新收藏到", - "Move favorite after reading": "阅读后移动收藏" + "Move favorite after reading": "阅读后移动收藏", + "Are you sure you want to delete this folder?" : "确定要删除这个收藏夹吗?", + "Import from file": "从文件导入", + "Failed to import": "导入失败" }, "zh_TW": { "Home": "首頁", @@ -255,6 +258,9 @@ "Network Favorite Pages": "網路收藏頁面", "Block": "屏蔽", "Add new favorite to": "添加新收藏到", - "Move favorite after reading": "閱讀後移動收藏" + "Move favorite after reading": "閱讀後移動收藏", + "Are you sure you want to delete this folder?" : "確定要刪除這個收藏夾嗎?", + "Import from file": "從文件匯入", + "Failed to import": "匯入失敗" } } \ No newline at end of file diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 3bee958..a10d1db 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -567,6 +567,19 @@ class SliverGridComics extends StatefulWidget { class _SliverGridComicsState extends State { List comics = []; + @override + void didUpdateWidget(covariant SliverGridComics oldWidget) { + if (oldWidget.comics != widget.comics) { + comics.clear(); + for (var comic in widget.comics) { + if (isBlocked(comic) == null) { + comics.add(comic); + } + } + } + super.didUpdateWidget(oldWidget); + } + @override void initState() { for (var comic in widget.comics) { diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index 8a3e537..97423b8 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; + import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/appdata.dart'; +import 'package:venera/foundation/log.dart'; import 'dart:io'; import 'app.dart'; @@ -92,7 +95,39 @@ class FavoriteItem implements Comic { @override Map toJson() { - throw UnimplementedError(); + return { + "name": name, + "author": author, + "type": type.value, + "tags": tags, + "id": id, + "coverPath": coverPath, + }; + } + + static FavoriteItem fromJson(Map json) { + var type = json["type"] as int; + if(type == 0 && json['coverPath'].toString().startsWith('http')) { + type = 'picacg'.hashCode; + } else if(type == 1) { + type = 'ehentai'.hashCode; + } else if(type == 2) { + type = 'jm'.hashCode; + } else if(type == 3) { + type = 'hitomi'.hashCode; + } else if(type == 4) { + type = 'wnacg'.hashCode; + } else if(type == 6) { + type = 'nhentai'.hashCode; + } + return FavoriteItem( + id: json["id"] ?? json['target'], + name: json["name"], + author: json["author"], + coverPath: json["coverPath"], + type: ComicType(type), + tags: List.from(json["tags"] ?? []), + ); } } @@ -525,4 +560,39 @@ class LocalFavoritesManager { comic.type.value ]); } + + String folderToJson(String folder) { + var res = _db.select(""" + select * from "$folder"; + """); + return jsonEncode({ + "info": "Generated by Venera", + "name": folder, + "comics": res.map((e) => FavoriteItem.fromRow(e).toJson()).toList(), + }); + } + + void fromJson(String json) { + var data = jsonDecode(json); + var folder = data["name"]; + if(folder == null || folder is! String) { + throw "Invalid data"; + } + if (folderNames.contains(folder)) { + int i = 0; + while (folderNames.contains("$folder($i)")) { + i++; + } + folder = "$folder($i)"; + } + createFolder(folder); + for (var comic in data["comics"]) { + try { + addComic(folder, FavoriteItem.fromJson(comic)); + } + catch(e) { + Log.error("Import Data", e.toString()); + } + } + } } diff --git a/lib/network/images.dart b/lib/network/images.dart index cfe4634..ea887bd 100644 --- a/lib/network/images.dart +++ b/lib/network/images.dart @@ -24,7 +24,7 @@ class ImageDownloader { var configs = {}; if (sourceKey != null) { var comicSource = ComicSource.find(sourceKey); - configs = comicSource!.getThumbnailLoadingConfig?.call(url) ?? {}; + configs = comicSource?.getThumbnailLoadingConfig?.call(url) ?? {}; } configs['headers'] ??= {}; if(configs['headers']['user-agent'] == null diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 85aea33..dd349e4 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -1145,7 +1145,7 @@ class _FavoritePanelState extends State<_FavoritePanel> { title: Text("Favorite".tl), ), body: DefaultTabController( - length: comicSource.favoriteData == null ? 1 : 2, + length: hasNetwork ? 2 : 1, child: Column( children: [ TabBar(tabs: [ diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 598df91..69be594 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -1,7 +1,4 @@ import 'dart:convert'; -import 'dart:io'; - -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; @@ -11,6 +8,7 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/app_dio.dart'; import 'package:venera/utils/ext.dart'; +import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; class ComicSourcePage extends StatefulWidget { @@ -328,7 +326,7 @@ class _BodyState extends State<_Body> { Row( children: [ TextButton( - onPressed: selectFile, child: Text("Select file".tl)) + onPressed: _selectFile, child: Text("Select file".tl)) .paddingLeft(8), const Spacer(), TextButton( @@ -350,16 +348,12 @@ class _BodyState extends State<_Body> { ); } - void selectFile() async { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['js'], - ); - final file = result?.files.first; + void _selectFile() async { + final file = await selectFile(ext: ["js"]); if (file == null) return; try { var fileName = file.name; - var bytes = await File(file.path!).readAsBytes(); + var bytes = await file.readAsBytes(); var content = utf8.decode(bytes); await addSource(content, fileName); } catch (e, s) { diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index 844bb31..a51f091 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -27,10 +27,26 @@ Future newFolder() async { }); } }, - ) + ), ], ).paddingHorizontal(16), actions: [ + TextButton( + child: Text("Import from file".tl), + onPressed: () async { + var file = await selectFile(ext: ['json']); + if(file == null) return; + var data = await file.readAsBytes(); + try { + LocalFavoritesManager().fromJson(utf8.decode(data)); + } + catch(e) { + context.showMessage(message: "Failed to import".tl); + return; + } + context.pop(); + }, + ).paddingRight(4), FilledButton( onPressed: () { var e = validateFolderName(controller.text); diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index ea61004..e3cbc09 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; @@ -9,6 +10,7 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/res.dart'; +import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; part 'favorite_actions.dart'; @@ -134,7 +136,12 @@ class _FavoritesPageState extends State { ) : null, ), - title: Text("Unselected".tl), + title: GestureDetector( + onTap: context.width < _kTwoPanelChangeWidth + ? showFolderSelector + : null, + child: Text("Unselected".tl), + ), ), ], ); diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 24c9f7e..0497042 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -15,8 +15,10 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { late List comics; void updateComics() { + print(comics.length); setState(() { comics = LocalFavoritesManager().getAllComics(widget.folder); + print(comics.length); }); } @@ -64,7 +66,6 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { favPage.setFolder(false, null); LocalFavoritesManager().deleteFolder(widget.folder); favPage.folderList?.updateFolders(); - context.pop(); }, ); }), @@ -110,6 +111,18 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { }, ); }), + MenuEntry( + icon: Icons.upload_file, + text: "Export".tl, + onClick: () { + var json = LocalFavoritesManager().folderToJson( + widget.folder, + ); + saveFile( + data: utf8.encode(json), + filename: "${widget.folder}.json", + ); + }), ], ), ], diff --git a/lib/pages/favorites/side_bar.dart b/lib/pages/favorites/side_bar.dart index 71f09b3..71839f6 100644 --- a/lib/pages/favorites/side_bar.dart +++ b/lib/pages/favorites/side_bar.dart @@ -211,11 +211,13 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { @override void update() { + if(!mounted) return; setState(() {}); } @override void updateFolders() { + if(!mounted) return; setState(() { folders = LocalFavoritesManager().folderNames; networkFolders = ComicSource.all() diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index 30ff8ff..5752687 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -25,8 +25,11 @@ class _AppSettingsState extends State { title: "Set New Storage Path".tl, actionTitle: "Set".tl, callback: () async { - var picker = FilePicker.platform; - var result = await picker.getDirectoryPath(); + if(App.isIOS) { + context.showMessage(message: "Not supported on iOS".tl); + return; + } + var result = await selectDirectory(); if (result == null) return; var loadingDialog = showLoadingDialog( App.rootContext, diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index 64f3c7c..188dc2f 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -1,6 +1,4 @@ import 'dart:convert'; - -import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 24d3084..690712d 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -1,12 +1,13 @@ import 'dart:convert'; import 'dart:io'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/utils/ext.dart'; import 'package:path/path.dart' as p; import 'package:share_plus/share_plus.dart' as s; +import 'package:file_selector/file_selector.dart' as file_selector; export 'dart:io'; export 'dart:typed_data'; @@ -128,7 +129,7 @@ class DirectoryPicker { Future pickDirectory() async { if (App.isWindows || App.isLinux) { - var d = await FilePicker.platform.getDirectoryPath(); + var d = await file_selector.getDirectoryPath(); _directory = d; return d == null ? null : Directory(d); } else if (App.isAndroid) { @@ -156,15 +157,46 @@ class DirectoryPicker { } } -Future saveFile( - {required Uint8List data, required String filename}) async { - var res = await FilePicker.platform.saveFile( - bytes: data, - fileName: filename, - lockParentWindow: true, +Future selectFile({required List ext}) async { + file_selector.XTypeGroup typeGroup = file_selector.XTypeGroup( + label: 'files', + extensions: ext, ); - if (App.isDesktop && res != null) { - await File(res).writeAsBytes(data); + final file_selector.XFile? file = await file_selector.openFile( + acceptedTypeGroups: [typeGroup], + ); + return file; +} + +Future selectDirectory() async { + var path = await file_selector.getDirectoryPath(); + return path; +} + +Future saveFile( + {Uint8List? data, required String filename, File? file}) async { + if(data == null && file == null) { + throw Exception("data and file cannot be null at the same time"); + } + if(data != null) { + var cache = FilePath.join(App.cachePath, filename); + if(File(cache).existsSync()) { + File(cache).deleteSync(); + } + await File(cache).writeAsBytes(data); + file = File(cache); + } + if(App.isMobile) { + final params = SaveFileDialogParams(sourceFilePath: file!.path); + await FlutterFileDialog.saveFile(params: params); + } else { + final result = await file_selector.getSaveLocation( + suggestedName: filename, + ); + if (result != null) { + var xFile = file_selector.XFile(file!.path); + await xFile.saveTo(result.path); + } } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index bee88c4..37c71c0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) flutter_qjs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterQjsPlugin"); flutter_qjs_plugin_register_with_registrar(flutter_qjs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index a37a6c8..7f61925 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_webview_window + file_selector_linux flutter_qjs gtk screen_retriever diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 786de6e..9bf393f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import app_links import desktop_webview_window +import file_selector_macos import flutter_inappwebview_macos import path_provider_foundation import screen_retriever @@ -18,6 +19,7 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) diff --git a/pubspec.lock b/pubspec.lock index e34d6e4..82a634d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -162,14 +162,70 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - file_picker: + file_selector: dependency: "direct main" description: - name: file_picker - sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" + name: file_selector + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "1.0.3" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: ec439df07c4999faad319ce8ad9e971795c2f1d7132ad5a793b9370a863c6128 + url: "https://pub.dev" + source: hosted + version: "0.5.1+10" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420" + url: "https://pub.dev" + source: hosted + version: "0.5.3+1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" + url: "https://pub.dev" + source: hosted + version: "0.9.3" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" fixnum: dependency: transitive description: @@ -183,6 +239,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_file_dialog: + dependency: "direct main" + description: + name: flutter_file_dialog + sha256: "9344b8f07be6a1b6f9854b723fb0cf84a8094ba94761af1d213589d3cb087488" + url: "https://pub.dev" + source: hosted + version: "3.0.2" flutter_inappwebview: dependency: "direct main" description: @@ -260,14 +324,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" - url: "https://pub.dev" - source: hosted - version: "2.0.22" flutter_qjs: dependency: "direct main" description: @@ -328,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.4" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" http_parser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5da5dfa..72ace46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,6 @@ dependencies: dio: any html: any pointycastle: any - file_picker: ^8.1.2 url_launcher: ^6.3.0 path: ^1.9.0 photo_view: @@ -50,6 +49,8 @@ dependencies: flutter_inappwebview: ^6.1.5 app_links: ^6.3.2 sliver_tools: ^0.2.12 + flutter_file_dialog: ^3.0.2 + file_selector: ^1.0.3 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c807ed8..c2fe9ba 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AppLinksPluginCApi")); DesktopWebviewWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); FlutterQjsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e0556be..7ccdf82 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links desktop_webview_window + file_selector_windows flutter_inappwebview_windows flutter_qjs screen_retriever