diff --git a/debian/gui/venera.desktop b/debian/gui/venera.desktop index 39d1650..e11ee22 100644 --- a/debian/gui/venera.desktop +++ b/debian/gui/venera.desktop @@ -1,5 +1,4 @@ [Desktop Entry] -Version={{Version}} Name=Venera GenericName=Venera Comment=venera diff --git a/lib/components/comic.dart b/lib/components/comic.dart index b131e4d..344cab7 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -342,21 +342,39 @@ class ComicTile extends StatelessWidget { } List _splitText(String text) { - // split text by space, comma. text in brackets will be kept together. + // split text by comma, brackets var words = []; var buffer = StringBuffer(); var inBracket = false; + String? prevBracket; for (var i = 0; i < text.length; i++) { var c = text[i]; if (c == '[' || c == '(') { - inBracket = true; - } else if (c == ']' || c == ')') { - inBracket = false; - } else if (c == ' ' || c == ',') { if (inBracket) { buffer.write(c); } else { - words.add(buffer.toString()); + if (buffer.isNotEmpty) { + words.add(buffer.toString().trim()); + buffer.clear(); + } + inBracket = true; + prevBracket = c; + } + } else if (c == ']' || c == ')') { + if (prevBracket == '[' && c == ']' || prevBracket == '(' && c == ')') { + if (buffer.isNotEmpty) { + words.add(buffer.toString().trim()); + buffer.clear(); + } + inBracket = false; + } else { + buffer.write(c); + } + } else if (c == ',') { + if (inBracket) { + buffer.write(c); + } else { + words.add(buffer.toString().trim()); buffer.clear(); } } else { @@ -364,8 +382,10 @@ class ComicTile extends StatelessWidget { } } if (buffer.isNotEmpty) { - words.add(buffer.toString()); + words.add(buffer.toString().trim()); } + words.removeWhere((element) => element == ""); + words = words.toSet().toList(); return words; } @@ -383,26 +403,33 @@ class ComicTile extends StatelessWidget { return StatefulBuilder(builder: (context, setState) { return ContentDialog( title: 'Block'.tl, - content: Wrap( - runSpacing: 8, - spacing: 8, - children: [ - for (var word in all) - OptionChip( - text: word, - isSelected: words.contains(word), - onTap: () { - setState(() { - if (!words.contains(word)) { - words.add(word); - } else { - words.remove(word); - } - }); - }, - ), - ], - ).paddingHorizontal(16), + content: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: math.min(400, context.height - 136), + ), + child: SingleChildScrollView( + child: Wrap( + runSpacing: 8, + spacing: 8, + children: [ + for (var word in all) + OptionChip( + text: word, + isSelected: words.contains(word), + onTap: () { + setState(() { + if (!words.contains(word)) { + words.add(word); + } else { + words.remove(word); + } + }); + }, + ), + ], + ), + ).paddingHorizontal(16), + ), actions: [ Button.filled( onPressed: () { @@ -833,6 +860,7 @@ class ComicList extends StatefulWidget { this.menuBuilder, this.controller, this.refreshHandlerCallback, + this.enablePageStorage = false, }); final Future>> Function(int page)? loadPage; @@ -851,6 +879,8 @@ class ComicList extends StatefulWidget { final void Function(VoidCallback c)? refreshHandlerCallback; + final bool enablePageStorage; + @override State createState() => ComicListState(); } @@ -868,6 +898,8 @@ class ComicListState extends State { String? _nextUrl; + late bool enablePageStorage = widget.enablePageStorage; + Map get state => { 'maxPage': _maxPage, 'data': _data, @@ -878,7 +910,7 @@ class ComicListState extends State { }; void restoreState(Map? state) { - if (state == null) { + if (state == null || !enablePageStorage) { return; } _maxPage = state['maxPage']; @@ -892,7 +924,9 @@ class ComicListState extends State { } void storeState() { - PageStorage.of(context).writeState(context, state); + if(enablePageStorage) { + PageStorage.of(context).writeState(context, state); + } } void refresh() { @@ -1122,7 +1156,7 @@ class ComicListState extends State { ); } return SmoothCustomScrollView( - key: const PageStorageKey('scroll'), + key: enablePageStorage ? PageStorageKey('scroll$_page') : null, controller: widget.controller, slivers: [ if (widget.leadingSliver != null) widget.leadingSliver!, diff --git a/lib/components/message.dart b/lib/components/message.dart index 3618e40..e5038de 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -290,6 +290,7 @@ class ContentDialog extends StatelessWidget { : const EdgeInsets.symmetric(horizontal: 16), elevation: 2, shadowColor: context.colorScheme.shadow, + backgroundColor: context.colorScheme.surface, child: AnimatedSize( duration: const Duration(milliseconds: 200), alignment: Alignment.topCenter, diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 523ff9b..acd8cdf 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -10,7 +10,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.1.2"; + final version = "1.1.3"; bool get isAndroid => Platform.isAndroid; diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 646eecf..1867264 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -201,8 +201,6 @@ class HistoryManager with ChangeNotifier { Map? _cachedHistory; - static const _kMaxHistoryLength = 200; - Future init() async { _db = sqlite3.open("${App.dataPath}/history.db"); @@ -228,12 +226,6 @@ class HistoryManager with ChangeNotifier { /// /// This function would be called when user start reading. Future addHistory(History newItem) async { - while(count() >= _kMaxHistoryLength) { - _db.execute(""" - delete from history - where time == (select min(time) from history); - """); - } _db.execute(""" insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index b1d0601..5d00c64 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -76,15 +76,16 @@ class LocalComic with HistoryMixin implements Comic { cover, )); - String get baseDir => (directory.contains('/') || directory.contains('\\')) ? directory : FilePath.join(LocalManager().path, directory); + String get baseDir => (directory.contains('/') || directory.contains('\\')) + ? directory + : FilePath.join(LocalManager().path, directory); @override String get description => ""; @override - String get sourceKey => comicType == ComicType.local - ? "local" - : comicType.sourceKey; + String get sourceKey => + comicType == ComicType.local ? "local" : comicType.sourceKey; @override Map toJson() { @@ -112,11 +113,12 @@ class LocalComic with HistoryMixin implements Comic { chapters: chapters, initialChapter: history?.ep, initialPage: history?.page, - history: history ?? History.fromModel( - model: this, - ep: 0, - page: 0, - ), + history: history ?? + History.fromModel( + model: this, + ep: 0, + page: 0, + ), ), ); } @@ -153,6 +155,15 @@ class LocalManager with ChangeNotifier { Directory get directory => Directory(path); + void _checkNoMedia() { + if (App.isAndroid) { + var file = File(FilePath.join(path, '.nomedia')); + if (!file.existsSync()) { + file.createSync(); + } + } + } + // return error message if failed Future setNewPath(String newPath) async { var newDir = Directory(newPath); @@ -167,13 +178,15 @@ class LocalManager with ChangeNotifier { directory, newDir, ); - await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath); + await File(FilePath.join(App.dataPath, 'local_path')) + .writeAsString(newPath); } catch (e, s) { Log.error("IO", e, s); return e.toString(); } await directory.deleteContents(recursive: true); path = newPath; + _checkNoMedia(); return null; } @@ -187,7 +200,8 @@ class LocalManager with ChangeNotifier { } } else if (App.isIOS) { var oldPath = FilePath.join(App.dataPath, 'local'); - if (Directory(oldPath).existsSync() && Directory(oldPath).listSync().isNotEmpty) { + if (Directory(oldPath).existsSync() && + Directory(oldPath).listSync().isNotEmpty) { return oldPath; } else { var directory = await getApplicationDocumentsDirectory(); @@ -198,6 +212,18 @@ class LocalManager with ChangeNotifier { } } + Future _checkPathValidation() async { + var testFile = File(FilePath.join(path, 'venera_test')); + try { + testFile.createSync(); + testFile.deleteSync(); + } catch (e) { + Log.error("IO", + "Failed to create test file in local path: $e\nUsing default path instead."); + path = await findDefaultPath(); + } + } + Future init() async { _db = sqlite3.open( '${App.dataPath}/local.db', @@ -229,10 +255,11 @@ class LocalManager with ChangeNotifier { if (!directory.existsSync()) { await directory.create(); } - } - catch(e, s) { + } catch (e, s) { Log.error("IO", "Failed to create local folder: $e", s); } + _checkPathValidation(); + _checkNoMedia(); restoreDownloadingTasks(); } @@ -242,7 +269,8 @@ class LocalManager with ChangeNotifier { SELECT id FROM comics WHERE comic_type = ? ORDER BY CAST(id AS INTEGER) DESC LIMIT 1; - ''', [type.value], + ''', + [type.value], ); if (res.isEmpty) { return '1'; @@ -352,15 +380,14 @@ class LocalManager with ChangeNotifier { } Future> getImages(String id, ComicType type, Object ep) async { - if(ep is! String && ep is! int) { + if (ep is! String && ep is! int) { throw "Invalid ep"; } var comic = find(id, type) ?? (throw "Comic Not Found"); var directory = Directory(comic.baseDir); if (comic.chapters != null) { - var cid = ep is int - ? comic.chapters!.keys.elementAt(ep - 1) - : (ep as String); + var cid = + ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String); directory = Directory(FilePath.join(directory.path, cid)); } var files = []; @@ -372,7 +399,7 @@ class LocalManager with ChangeNotifier { continue; } //Hidden file in some file system - if(entity.name.startsWith('.')) { + if (entity.name.startsWith('.')) { continue; } files.add(entity); @@ -394,7 +421,7 @@ class LocalManager with ChangeNotifier { if (comic == null) return false; if (comic.chapters == null || ep == null) return true; return comic.downloadedChapters - .contains(comic.chapters!.keys.elementAt(ep-1)); + .contains(comic.chapters!.keys.elementAt(ep - 1)); } List downloadingTasks = []; @@ -451,12 +478,17 @@ class LocalManager with ChangeNotifier { void restoreDownloadingTasks() { var file = File(FilePath.join(App.dataPath, 'downloading_tasks.json')); if (file.existsSync()) { - var tasks = jsonDecode(file.readAsStringSync()); - for (var e in tasks) { - var task = DownloadTask.fromJson(e); - if (task != null) { - downloadingTasks.add(task); + try { + var tasks = jsonDecode(file.readAsStringSync()); + for (var e in tasks) { + var task = DownloadTask.fromJson(e); + if (task != null) { + downloadingTasks.add(task); + } } + } catch (e) { + file.delete(); + Log.error("LocalManager", "Failed to restore downloading tasks: $e"); } } } @@ -469,13 +501,13 @@ class LocalManager with ChangeNotifier { } void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) { - if(removeFileOnDisk) { + if (removeFileOnDisk) { var dir = Directory(FilePath.join(path, c.directory)); dir.deleteIgnoreError(recursive: true); } // Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted. - if(c.comicType == ComicType.local) { - if(HistoryManager().findSync(c.id, c.comicType) != null) { + if (c.comicType == ComicType.local) { + if (HistoryManager().findSync(c.id, c.comicType) != null) { HistoryManager().remove(c.id, c.comicType); } var folders = LocalFavoritesManager().find(c.id, c.comicType); @@ -505,4 +537,4 @@ enum LocalSortType { } return name; } -} \ No newline at end of file +} diff --git a/lib/init.dart b/lib/init.dart index 5fa28b6..ec4ea7d 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -6,23 +6,37 @@ import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/js_engine.dart'; import 'package:venera/foundation/local.dart'; +import 'package:venera/foundation/log.dart'; import 'package:venera/network/cookie_jar.dart'; import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; import 'foundation/appdata.dart'; +extension FutureInit on Future { + /// Prevent unhandled exception + /// + /// A unhandled exception occurred in init() will cause the app to crash. + Future wait() async { + try { + await this; + } catch (e, s) { + Log.error("init", "$e\n$s"); + } + } +} + Future init() async { - await SAFTaskWorker().init(); - await AppTranslation.init(); - await appdata.init(); - await App.init(); - await HistoryManager().init(); - await TagsTranslation.readData(); - await LocalFavoritesManager().init(); + await SAFTaskWorker().init().wait(); + await AppTranslation.init().wait(); + await appdata.init().wait(); + await App.init().wait(); + await HistoryManager().init().wait(); + await TagsTranslation.readData().wait(); + await LocalFavoritesManager().init().wait(); SingleInstanceCookieJar("${App.dataPath}/cookie.db"); - await JsEngine().init(); - await ComicSource.init(); - await LocalManager().init(); + await JsEngine().init().wait(); + await ComicSource.init().wait(); + await LocalManager().init().wait(); CacheManager().setLimitSize(appdata.settings['cacheSize']); } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index dd6fa77..5f0208a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -156,30 +156,40 @@ class _MyAppState extends State with WidgetsBindingObserver { home = const MainPage(); } return DynamicColorBuilder(builder: (light, dark) { - if (appdata.settings['color'] != 'system' || light == null || dark == null) { + if (appdata.settings['color'] != 'system' || + light == null || + dark == null) { var color = translateColorSetting(); light = ColorScheme.fromSeed( seedColor: color, + surface: Colors.white, ); dark = ColorScheme.fromSeed( seedColor: color, brightness: Brightness.dark, + surface: Colors.black, + ); + } else { + light = ColorScheme.fromSeed( + seedColor: light.primary, + surface: Colors.white, + ); + dark = ColorScheme.fromSeed( + seedColor: dark.primary, + brightness: Brightness.dark, + surface: Colors.black, ); } return MaterialApp( home: home, debugShowCheckedModeBanner: false, theme: ThemeData( - colorScheme: light.copyWith( - surface: Colors.white, - ), + colorScheme: light, fontFamily: App.isWindows ? "Microsoft YaHei" : null, ), navigatorKey: App.rootNavigatorKey, darkTheme: ThemeData( - colorScheme: dark.copyWith( - surface: Colors.black, - ), + colorScheme: dark, fontFamily: App.isWindows ? "Microsoft YaHei" : null, ), themeMode: switch (appdata.settings['theme_mode']) { @@ -211,8 +221,8 @@ class _MyAppState extends State with WidgetsBindingObserver { ], builder: (context, widget) { ErrorWidget.builder = (details) { - Log.error( - "Unhandled Exception", "${details.exception}\n${details.stack}"); + Log.error("Unhandled Exception", + "${details.exception}\n${details.stack}"); return Material( child: Center( child: Text(details.exception.toString()), diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index efd1b6c..28c3bcc 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -163,6 +163,9 @@ class _BodyState extends State<_Body> { break; } } + } else { + current = item.value['options'] + .firstWhere((e) => e['value'] == current)['text'] ?? current; } yield ListTile( title: Text((item.value['title'] as String).ts(source.key)), diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index dcb6b53..d43e57b 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -295,6 +295,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> ); } else if (data.loadPage != null || data.loadNext != null) { return ComicList( + enablePageStorage: true, loadPage: data.loadPage, loadNext: data.loadNext, key: const PageStorageKey("comic_list"), diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index 6c3a822..b9512d7 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -146,6 +146,18 @@ Future> updateComicsInfo(String folder) async { var newInfo = (await comicSource.loadComicInfo!(c.id)).data; + var newTags = []; + 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) { + newTags.add("$namespace:$tag"); + } + } + comics[index] = FavoriteItem( id: c.id, name: newInfo.title, @@ -154,7 +166,7 @@ Future> updateComicsInfo(String folder) async { newInfo.tags['author']?.firstOrNull ?? c.author, type: c.type, - tags: c.tags, + tags: newTags, ); LocalFavoritesManager().updateInfo(folder, comics[index]); diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 256d763..2a8f344 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -102,10 +102,13 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { } } + var scrollController = ScrollController(); + @override Widget build(BuildContext context) { - var body = Scaffold( - body: SmoothCustomScrollView(slivers: [ + Widget body = SmoothCustomScrollView( + controller: scrollController, + slivers: [ if (!searchMode && !multiSelectMode) SliverAppbar( style: context.width < changePoint @@ -387,6 +390,19 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { ); }, ), + if (appdata.settings["onClickFavorite"] == "viewDetail") + MenuEntry( + icon: Icons.menu_book_outlined, + text: "Read".tl, + onClick: () { + App.mainNavigatorKey?.currentContext?.to( + () => ReaderWithLoading( + id: c.id, + sourceKey: c.sourceKey, + ), + ); + }, + ), ]; }, onTap: (c) { @@ -447,7 +463,17 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { }); }, ), - ]), + ], + ); + body = Scrollbar( + controller: scrollController, + thickness: App.isDesktop ? 8 : 12, + radius: const Radius.circular(8), + interactive: true, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: body, + ), ); return PopScope( canPop: !multiSelectMode && !searchMode, diff --git a/lib/pages/favorites/network_favorites_page.dart b/lib/pages/favorites/network_favorites_page.dart index c382b09..5cd0f53 100644 --- a/lib/pages/favorites/network_favorites_page.dart +++ b/lib/pages/favorites/network_favorites_page.dart @@ -166,6 +166,7 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> { ), ]; }, + enablePageStorage: true, ); } } @@ -548,6 +549,7 @@ class _FavoriteFolder extends StatelessWidget { Widget build(BuildContext context) { return ComicList( key: comicListKey, + enablePageStorage: true, leadingSliver: SliverAppbar( title: Text(title), actions: [ diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 05c6dd8..d82f09b 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -4,6 +4,7 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; +import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/downloading_page.dart'; import 'package:venera/pages/favorites/favorites_page.dart'; import 'package:venera/utils/cbz.dart'; @@ -140,6 +141,19 @@ class _LocalComicsPageState extends State { addFavorite(selectedComics.keys.toList()); }, ), + if (selectedComics.length == 1) + MenuEntry( + icon: Icons.chrome_reader_mode_outlined, + text: "View Detail".tl, + onClick: () { + context.to(() => ComicPage( + id: selectedComics.keys.first.id, + sourceKey: selectedComics.keys.first.sourceKey, + )); + }, + ), + if (selectedComics.length == 1) + ...exportActions(selectedComics.keys.first), ]); } @@ -182,59 +196,63 @@ class _LocalComicsPageState extends State { buildMultiSelectMenu(), ]; + List normalActions = [ + Tooltip( + message: "Search".tl, + child: IconButton( + icon: const Icon(Icons.search), + onPressed: () { + setState(() { + searchMode = true; + }); + }, + ), + ), + Tooltip( + message: "Sort".tl, + child: IconButton( + icon: const Icon(Icons.sort), + onPressed: sort, + ), + ), + Tooltip( + message: "Downloading".tl, + child: IconButton( + icon: const Icon(Icons.download), + onPressed: () { + showPopUpWidget(context, const DownloadingPage()); + }, + ), + ), + ]; + var body = Scaffold( body: SmoothCustomScrollView( slivers: [ - if (!searchMode && !multiSelectMode) - SliverAppbar( - title: Text("Local".tl), - actions: [ - Tooltip( - message: "Search".tl, - child: IconButton( - icon: const Icon(Icons.search), - onPressed: () { - setState(() { - searchMode = true; - }); - }, - ), - ), - Tooltip( - message: "Sort".tl, - child: IconButton( - icon: const Icon(Icons.sort), - onPressed: sort, - ), - ), - Tooltip( - message: "Downloading".tl, - child: IconButton( - icon: const Icon(Icons.download), - onPressed: () { - showPopUpWidget(context, const DownloadingPage()); - }, - ), - ), - ], - ) - else if (multiSelectMode) + if (!searchMode) SliverAppbar( leading: Tooltip( - message: "Cancel".tl, + message: multiSelectMode ? "Cancel".tl : "Back".tl, child: IconButton( - icon: const Icon(Icons.close), onPressed: () { - setState(() { - multiSelectMode = false; - selectedComics.clear(); - }); + if (multiSelectMode) { + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); + } else { + context.pop(); + } }, + icon: multiSelectMode + ? const Icon(Icons.close) + : const Icon(Icons.arrow_back), ), ), - title: Text( - "Selected @c comics".tlParams({"c": selectedComics.length})), - actions: selectActions, + title: multiSelectMode + ? Text(selectedComics.length.toString()) + : Text("Local".tl), + actions: multiSelectMode ? selectActions : normalActions, ) else if (searchMode) SliverAppbar( @@ -273,14 +291,14 @@ class _LocalComicsPageState extends State { }); }, onTap: (c) { - if(multiSelectMode) { + if (multiSelectMode) { setState(() { if (selectedComics.containsKey(c as LocalComic)) { selectedComics.remove(c); } else { selectedComics[c] = true; } - if(selectedComics.isEmpty) { + if (selectedComics.isEmpty) { multiSelectMode = false; } }); @@ -291,88 +309,20 @@ class _LocalComicsPageState extends State { menuBuilder: (c) { return [ MenuEntry( - icon: Icons.delete, - text: "Delete".tl, - onClick: () { - deleteComics([c as LocalComic]).then((value) { - if (value && multiSelectMode) { - setState(() { - multiSelectMode = false; - selectedComics.clear(); - }); - } - }); - }), - MenuEntry( - icon: Icons.outbox_outlined, - text: "Export as cbz".tl, - onClick: () async { - var controller = showLoadingDialog( - context, - allowCancel: false, - ); - try { - var file = await CBZ.export(c as LocalComic); - await saveFile(filename: file.name, file: file); - await file.delete(); - } catch (e) { - context.showMessage(message: e.toString()); + icon: Icons.delete, + text: "Delete".tl, + onClick: () { + deleteComics([c as LocalComic]).then((value) { + if (value && multiSelectMode) { + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); } - controller.close(); - }), - MenuEntry( - icon: Icons.picture_as_pdf_outlined, - text: "Export as pdf".tl, - onClick: () async { - var cache = FilePath.join(App.cachePath, 'temp.pdf'); - var controller = showLoadingDialog( - context, - allowCancel: false, - ); - try { - await createPdfFromComicIsolate( - comic: c as LocalComic, - savePath: cache, - ); - await saveFile( - file: File(cache), - filename: "${c.title}.pdf", - ); - } catch (e, s) { - Log.error("PDF Export", e, s); - context.showMessage(message: e.toString()); - } finally { - controller.close(); - File(cache).deleteIgnoreError(); - } - }, - ), - MenuEntry( - icon: Icons.import_contacts_outlined, - text: "Export as epub".tl, - onClick: () async { - var controller = showLoadingDialog( - context, - allowCancel: false, - ); - File? file; - try { - file = await createEpubWithLocalComic( - c as LocalComic, - ); - await saveFile( - file: file, - filename: "${c.title}.epub", - ); - } catch (e, s) { - Log.error("EPUB Export", e, s); - context.showMessage(message: e.toString()); - } finally { - controller.close(); - file?.deleteIgnoreError(); - } - }, - ) + }); + }, + ), + ...exportActions(c as LocalComic), ]; }, ), @@ -439,4 +389,79 @@ class _LocalComicsPageState extends State { ); return isDeleted; } + + List exportActions(LocalComic c) { + return [ + MenuEntry( + icon: Icons.outbox_outlined, + text: "Export as cbz".tl, + onClick: () async { + var controller = showLoadingDialog( + context, + allowCancel: false, + ); + try { + var file = await CBZ.export(c); + await saveFile(filename: file.name, file: file); + await file.delete(); + } catch (e) { + context.showMessage(message: e.toString()); + } + controller.close(); + }), + MenuEntry( + icon: Icons.picture_as_pdf_outlined, + text: "Export as pdf".tl, + onClick: () async { + var cache = FilePath.join(App.cachePath, 'temp.pdf'); + var controller = showLoadingDialog( + context, + allowCancel: false, + ); + try { + await createPdfFromComicIsolate( + comic: c, + savePath: cache, + ); + await saveFile( + file: File(cache), + filename: "${c.title}.pdf", + ); + } catch (e, s) { + Log.error("PDF Export", e, s); + context.showMessage(message: e.toString()); + } finally { + controller.close(); + File(cache).deleteIgnoreError(); + } + }, + ), + MenuEntry( + icon: Icons.import_contacts_outlined, + text: "Export as epub".tl, + onClick: () async { + var controller = showLoadingDialog( + context, + allowCancel: false, + ); + File? file; + try { + file = await createEpubWithLocalComic( + c, + ); + await saveFile( + file: file, + filename: "${c.title}.epub", + ); + } catch (e, s) { + Log.error("EPUB Export", e, s); + context.showMessage(message: e.toString()); + } finally { + controller.close(); + file?.deleteIgnoreError(); + } + }, + ) + ]; + } } diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 2801e55..fdd4e7b 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -62,9 +62,7 @@ class _MainPageState extends State { } final _pages = [ - const HomePage( - key: PageStorageKey('home'), - ), + const HomePage(), const FavoritesPage( key: PageStorageKey('favorites'), ), diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 38489a5..2d74801 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -356,6 +356,7 @@ class _ContinuousModeState extends State<_ContinuousMode> var isCTRLPressed = false; static var _isMouseScrolling = false; var fingers = 0; + bool disableScroll = false; @override void initState() { @@ -426,7 +427,7 @@ class _ContinuousModeState extends State<_ContinuousMode> ? Axis.vertical : Axis.horizontal, reverse: reader.mode == ReaderMode.continuousRightToLeft, - physics: isCTRLPressed || _isMouseScrolling + physics: isCTRLPressed || _isMouseScrolling || disableScroll ? const NeverScrollableScrollPhysics() : const ClampingScrollPhysics(), itemBuilder: (context, index) { @@ -460,6 +461,11 @@ class _ContinuousModeState extends State<_ContinuousMode> widget = Listener( onPointerDown: (event) { fingers++; + if(fingers > 1 && !disableScroll) { + setState(() { + disableScroll = true; + }); + } futurePosition = null; if (_isMouseScrolling) { setState(() { @@ -469,6 +475,11 @@ class _ContinuousModeState extends State<_ContinuousMode> }, onPointerUp: (event) { fingers--; + if(fingers <= 1 && disableScroll) { + setState(() { + disableScroll = false; + }); + } }, onPointerPanZoomUpdate: (event) { if (event.scale == 1.0) { diff --git a/lib/pages/reader/loading.dart b/lib/pages/reader/loading.dart index 682f457..c0c7e27 100644 --- a/lib/pages/reader/loading.dart +++ b/lib/pages/reader/loading.dart @@ -25,6 +25,8 @@ class _ReaderWithLoadingState name: data.name, chapters: data.chapters, history: data.history, + initialChapter: data.history.ep, + initialPage: data.history.page, ); } diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index ec04d06..d3221cc 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -212,7 +212,11 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { if(history != null) { history!.page = page; history!.ep = chapter; + if (maxPage > 1) { + history!.maxPage = maxPage; + } history!.readEpisode.add(chapter); + history!.time = DateTime.now(); HistoryManager().addHistory(history!); } } diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 5fb17bd..33cf7ef 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -456,58 +456,63 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { var imagesOnScreen = continuesState.itemPositionsListener.itemPositions.value; var images = imagesOnScreen - .map((e) => context.reader.images![e.index - 1]) + .map((e) => context.reader.images!.elementAtOrNull(e.index - 1)) + .whereType() .toList(); String? selected; - await showPopUpWidget( - context, - PopUpWidgetScaffold( - title: "Select an image on screen".tl, - body: GridView.builder( - itemCount: images.length, - itemBuilder: (context, index) { - ImageProvider image; - var imageKey = images[index]; - if (imageKey.startsWith('file://')) { - image = FileImage(File(imageKey.replaceFirst("file://", ''))); - } else { - image = ReaderImageProvider( - imageKey, - reader.type.comicSource!.key, - reader.cid, - reader.eid, - ); - } - return InkWell( - borderRadius: const BorderRadius.all(Radius.circular(16)), - onTap: () { - selected = images[index]; - App.rootContext.pop(); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(16)), - border: Border.all( - color: Theme.of(context).colorScheme.outline, + if (images.length > 1) { + await showPopUpWidget( + context, + PopUpWidgetScaffold( + title: "Select an image on screen".tl, + body: GridView.builder( + itemCount: images.length, + itemBuilder: (context, index) { + ImageProvider image; + var imageKey = images[index]; + if (imageKey.startsWith('file://')) { + image = FileImage(File(imageKey.replaceFirst("file://", ''))); + } else { + image = ReaderImageProvider( + imageKey, + reader.type.comicSource!.key, + reader.cid, + reader.eid, + ); + } + return InkWell( + borderRadius: const BorderRadius.all(Radius.circular(16)), + onTap: () { + selected = images[index]; + App.rootContext.pop(); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), ), - ), - width: double.infinity, - height: double.infinity, - child: Image( width: double.infinity, height: double.infinity, - image: image, + child: Image( + width: double.infinity, + height: double.infinity, + image: image, + ), ), - ), - ).padding(const EdgeInsets.all(8)); - }, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - childAspectRatio: 0.7, + ).padding(const EdgeInsets.all(8)); + }, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + childAspectRatio: 0.7, + ), ), ), - ), - ); + ); + } else { + selected = images.first; + } if (selected == null) { return null; } else { diff --git a/lib/utils/import_comic.dart b/lib/utils/import_comic.dart index 05ac47a..6904fcc 100644 --- a/lib/utils/import_comic.dart +++ b/lib/utils/import_comic.dart @@ -39,7 +39,7 @@ class ImportComic { Future multipleCbz() async { var picker = DirectoryPicker(); - var dir = await picker.pickDirectory(); + var dir = await picker.pickDirectory(directAccess: true); if (dir != null) { var files = (await dir.list().toList()).whereType().toList(); files.removeWhere((e) => e.extension != 'cbz' && e.extension != 'zip'); diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 307e6a5..79020d4 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -197,7 +197,7 @@ class DirectoryPicker { static const _methodChannel = MethodChannel("venera/method_channel"); - Future pickDirectory() async { + Future pickDirectory({bool directAccess = false}) async { IO._isSelectingFiles = true; try { String? directory; @@ -205,6 +205,16 @@ class DirectoryPicker { directory = await file_selector.getDirectoryPath(); } else if (App.isAndroid) { directory = (await AndroidDirectory.pickDirectory())?.path; + if (directory != null && directAccess) { + // Native library does not have access to the directory. Copy it to cache. + var cache = FilePath.join(App.cachePath, "selected_directory"); + if (Directory(cache).existsSync()) { + Directory(cache).deleteSync(recursive: true); + } + Directory(cache).createSync(); + await copyDirectoryIsolate(Directory(directory), Directory(cache)); + directory = cache; + } } else { // ios, macos directory = diff --git a/pubspec.lock b/pubspec.lock index 9907c10..0fe101a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: "direct main" description: name: app_links - sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99 + sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" app_links_linux: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: battery_plus - sha256: "220c8f1961efb01d6870493b5ac5a80afaeaffc8757f7a11ed3025a8570d29e7" + sha256: a0409fe7d21905987eb1348ad57c634f913166f14f0c8936b73d3f5940fac551 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.2.1" battery_plus_platform_interface: dependency: transitive description: @@ -425,10 +425,10 @@ packages: dependency: transitive description: name: flutter_rust_bridge - sha256: fb9d3c9395eae3c71d4fe3ec343b9f30636c9988150c8bb33b60047549b34e3d + sha256: "35c257fc7f98e34c1314d6c145e5ed54e7c94e8a9f469947e31c9298177d546f" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" flutter_saf: dependency: "direct main" description: @@ -447,10 +447,10 @@ packages: dependency: "direct dev" description: name: flutter_to_arch - sha256: "656cffc182b05af38aa96a1115931620b8865c4b0cfe00813b26fcff0875f2ab" + sha256: b68b2757a89a517ae2141cbc672acdd1f69721dd686cacad03876b6f436ff040 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" flutter_to_debian: dependency: "direct dev" description: @@ -798,10 +798,10 @@ packages: dependency: "direct main" description: name: rhttp - sha256: "581d57b5b6056d31489af94db8653a1c11d7b59050cbbc41ece4279e50414de5" + sha256: "8212cbc816cc3e761eecb8d4dbbaa1eca95de715428320a198a4e7a89acdcd2e" url: "https://pub.dev" source: hosted - version: "0.9.6" + version: "0.9.8" screen_retriever: dependency: transitive description: @@ -855,18 +855,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8" + sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" url: "https://pub.dev" source: hosted - version: "10.1.2" + version: "10.1.3" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48 + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" shimmer: dependency: "direct main" description: @@ -916,10 +916,10 @@ packages: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "636b0fe8a2de894e5455572f6cbbc458f4ffecfe9f860b79439e27041ea4f0b9" + sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785" url: "https://pub.dev" source: hosted - version: "0.5.27" + version: "0.5.28" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 01eb54e..32e6075 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.1.2+112 +version: 1.1.3+113 environment: sdk: '>=3.6.0 <4.0.0' @@ -17,7 +17,7 @@ dependencies: intl: ^0.19.0 window_manager: ^0.4.3 sqlite3: ^2.4.7 - sqlite3_flutter_libs: any + sqlite3_flutter_libs: ^0.5.28 flutter_qjs: git: url: https://github.com/wgh136/flutter_qjs @@ -33,7 +33,7 @@ dependencies: url: https://github.com/wgh136/photo_view ref: 94724a0b mime: ^2.0.0 - share_plus: ^10.0.2 + share_plus: ^10.1.3 scrollable_positioned_list: git: url: https://github.com/venera-app/flutter.widgets @@ -47,7 +47,7 @@ dependencies: url: https://github.com/wgh136/flutter_desktop_webview path: packages/desktop_webview_window flutter_inappwebview: ^6.1.5 - app_links: ^6.3.2 + app_links: ^6.3.3 sliver_tools: ^0.2.12 flutter_file_dialog: ^3.0.2 file_selector: ^1.0.3 @@ -56,12 +56,12 @@ dependencies: git: url: https://github.com/venera-app/lodepng_flutter ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00 - rhttp: 0.9.6 + rhttp: 0.9.8 webdav_client: git: url: https://github.com/wgh136/webdav_client ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1 - battery_plus: ^6.2.0 + battery_plus: ^6.2.1 local_auth: ^2.3.0 flutter_saf: git: @@ -76,7 +76,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 - flutter_to_arch: ^1.0.0 + flutter_to_arch: ^1.0.1 flutter_to_debian: flutter: