diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 87abbb8..1a48e2e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -170,6 +170,9 @@ jobs: sudo apt-get update -y sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1 dart pub global activate flutter_to_debian + - name: "Patch font" + run: | + dart run patch/font.dart - run: python3 debian/build.py arm64 - uses: actions/upload-artifact@v4 with: diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 75f982a..9678791 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -13,7 +13,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.5.1"; + final version = "1.5.2"; bool get isAndroid => Platform.isAndroid; diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index d198ec3..8081f94 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:convert'; import 'dart:ffi'; import 'dart:isolate'; @@ -213,12 +214,10 @@ class LocalFavoritesManager with ChangeNotifier { late Map counts; + var _hashedIds = {}; + int get totalComics { - int total = 0; - for (var t in counts.values) { - total += t; - } - return total; + return _hashedIds.length; } int folderComics(String folder) { @@ -280,6 +279,48 @@ class LocalFavoritesManager with ChangeNotifier { for (var folder in folderNames) { counts[folder] = count(folder); } + _initHashedIds(folderNames, _db.handle).then((value) { + _hashedIds = value; + notifyListeners(); + }); + } + + void refreshHashedIds() { + _initHashedIds(folderNames, _db.handle).then((value) { + _hashedIds = value; + notifyListeners(); + }); + } + + void reduceHashedId(String id, int type) { + var hash = id.hashCode ^ type; + if (_hashedIds.containsKey(hash)) { + if (_hashedIds[hash]! > 1) { + _hashedIds[hash] = _hashedIds[hash]! - 1; + } else { + _hashedIds.remove(hash); + } + } + } + + static Future> _initHashedIds( + List folders, Pointer p) { + return Isolate.run(() { + var db = sqlite3.fromPointer(p); + var hashedIds = {}; + for (var folder in folders) { + var rows = db.select(""" + select id, type from "$folder"; + """); + for (var row in rows) { + var id = row["id"] as String; + var type = row["type"] as int; + var hash = id.hashCode ^ type; + hashedIds[hash] = (hashedIds[hash] ?? 0) + 1; + } + } + return hashedIds; + }); } List find(String id, ComicType type) { @@ -559,7 +600,6 @@ class LocalFavoritesManager with ChangeNotifier { /// return true if success, false if already exists bool addComic(String folder, FavoriteItem comic, [int? order, String? updateTime]) { - _modifiedAfterLastCache = true; if (!existsFolder(folder)) { throw Exception("Folder does not exists"); } @@ -614,14 +654,14 @@ class LocalFavoritesManager with ChangeNotifier { } else { counts[folder] = counts[folder]! + 1; } + var hash = comic.id.hashCode ^ comic.type.value; + _hashedIds[hash] = (_hashedIds[hash] ?? 0) + 1; notifyListeners(); return true; } void moveFavorite( String sourceFolder, String targetFolder, String id, ComicType type) { - _modifiedAfterLastCache = true; - if (!existsFolder(sourceFolder)) { throw Exception("Source folder does not exist"); } @@ -655,8 +695,6 @@ class LocalFavoritesManager with ChangeNotifier { void batchMoveFavorites( String sourceFolder, String targetFolder, List items) { - _modifiedAfterLastCache = true; - if (!existsFolder(sourceFolder)) { throw Exception("Source folder does not exist"); } @@ -691,25 +729,15 @@ class LocalFavoritesManager with ChangeNotifier { _db.execute("COMMIT"); // Update counts - if (counts[targetFolder] == null) { - counts[targetFolder] = count(targetFolder); - } else { - counts[targetFolder] = counts[targetFolder]! + items.length; - } - - if (counts[sourceFolder] != null) { - counts[sourceFolder] = counts[sourceFolder]! - items.length; - } else { - counts[sourceFolder] = count(sourceFolder); - } + counts[targetFolder] = count(targetFolder); + counts[sourceFolder] = count(sourceFolder); + refreshHashedIds(); notifyListeners(); } void batchCopyFavorites( String sourceFolder, String targetFolder, List items) { - _modifiedAfterLastCache = true; - if (!existsFolder(sourceFolder)) { throw Exception("Source folder does not exist"); } @@ -740,18 +768,14 @@ class LocalFavoritesManager with ChangeNotifier { _db.execute("COMMIT"); // Update counts - if (counts[targetFolder] == null) { - counts[targetFolder] = count(targetFolder); - } else { - counts[targetFolder] = counts[targetFolder]! + items.length; - } + counts[targetFolder] = count(targetFolder); + refreshHashedIds(); notifyListeners(); } /// delete a folder void deleteFolder(String name) { - _modifiedAfterLastCache = true; _db.execute(""" drop table "$name"; """); @@ -760,11 +784,11 @@ class LocalFavoritesManager with ChangeNotifier { where folder_name == ?; """, [name]); counts.remove(name); + refreshHashedIds(); notifyListeners(); } void deleteComicWithId(String folder, String id, ComicType type) { - _modifiedAfterLastCache = true; LocalFavoriteImageProvider.delete(id, type.value); _db.execute(""" delete from "$folder" @@ -775,11 +799,11 @@ class LocalFavoritesManager with ChangeNotifier { } else { counts[folder] = count(folder); } + reduceHashedId(id, type.value); notifyListeners(); } void batchDeleteComics(String folder, List comics) { - _modifiedAfterLastCache = true; _db.execute("BEGIN TRANSACTION"); try { for (var comic in comics) { @@ -800,11 +824,13 @@ class LocalFavoritesManager with ChangeNotifier { return; } _db.execute("COMMIT"); + for (var comic in comics) { + reduceHashedId(comic.id, comic.type.value); + } notifyListeners(); } void batchDeleteComicsInAllFolders(List comics) { - _modifiedAfterLastCache = true; _db.execute("BEGIN TRANSACTION"); var folderNames = _getFolderNamesWithDB(); try { @@ -824,6 +850,10 @@ class LocalFavoritesManager with ChangeNotifier { } initCounts(); _db.execute("COMMIT"); + for (var comic in comics) { + var hash = comic.id.hashCode ^ comic.type.value; + _hashedIds.remove(hash); + } notifyListeners(); } @@ -908,7 +938,6 @@ class LocalFavoritesManager with ChangeNotifier { markAsRead(id, type); return; } - _modifiedAfterLastCache = true; var followUpdatesFolder = appdata.settings['followUpdatesFolder']; for (final folder in folderNames) { var rows = _db.select(""" @@ -1029,28 +1058,9 @@ class LocalFavoritesManager with ChangeNotifier { notifyListeners(); } - final _cachedFavoritedIds = {}; - bool isExist(String id, ComicType type) { - if (_modifiedAfterLastCache) { - _cacheFavoritedIds(); - } - return _cachedFavoritedIds.containsKey("$id@${type.value}"); - } - - bool _modifiedAfterLastCache = true; - - void _cacheFavoritedIds() { - _modifiedAfterLastCache = false; - _cachedFavoritedIds.clear(); - for (var folder in folderNames) { - var rows = _db.select(""" - select id, type from "$folder"; - """); - for (var row in rows) { - _cachedFavoritedIds["${row["id"]}@${row["type"]}"] = true; - } - } + var hash = id.hashCode ^ type.value; + return _hashedIds.containsKey(hash); } void updateInfo(String folder, FavoriteItem comic, [bool notify = true]) { diff --git a/lib/foundation/follow_updates.dart b/lib/foundation/follow_updates.dart index 98144c3..f50ee3a 100644 --- a/lib/foundation/follow_updates.dart +++ b/lib/foundation/follow_updates.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/log.dart'; +import 'package:venera/utils/channel.dart'; class ComicUpdateResult { final bool updated; @@ -62,6 +63,7 @@ Future updateComic( return ComicUpdateResult(updated, null); } catch (e, s) { Log.error("Check Updates", e, s); + await Future.delayed(const Duration(seconds: 2)); retries--; if (retries == 0) { return ComicUpdateResult(false, e.toString()); @@ -114,23 +116,50 @@ void updateFolderBase( current = 0; stream.add(UpdateProgress(total, current, errors, updated)); - var futures = []; - for (var comic in comicsToUpdate) { - var future = updateComic(comic, folder).then((result) { - current++; - if (result.updated) { - updated++; + var channel = Channel(10); + + // Producer + () async { + var c = 0; + for (var comic in comicsToUpdate) { + await channel.push(comic); + c++; + // Throttle + if (c % 5 == 0) { + var delay = c % 100 + 1; + if (delay > 10) { + delay = 10; + } + await Future.delayed(Duration(seconds: delay)); } - if (result.errorMessage != null) { - errors++; + } + channel.close(); + }(); + + // Consumers + var updateFutures = []; + for (var i = 0; i < 5; i++) { + var f = () async { + while (true) { + var comic = await channel.pop(); + if (comic == null) { + break; + } + var result = await updateComic(comic, folder); + current++; + if (result.updated) { + updated++; + } + if (result.errorMessage != null) { + errors++; + } + stream.add(UpdateProgress(total, current, errors, updated, comic, result.errorMessage)); } - stream.add( - UpdateProgress(total, current, errors, updated, comic, result.errorMessage)); - }); - futures.add(future); + }(); + updateFutures.add(f); } - await Future.wait(futures); + await Future.wait(updateFutures); if (updated > 0) { LocalFavoritesManager().notifyChanges(); diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 36cbe47..1d21c61 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -423,6 +423,7 @@ class LocalManager with ChangeNotifier { if (comic.hasChapters) { var cid = ep is int ? comic.chapters!.ids.elementAt(ep - 1) : (ep as String); + cid = getChapterDirectoryName(cid); directory = Directory(FilePath.join(directory.path, cid)); } var files = []; @@ -600,7 +601,10 @@ class LocalManager with ChangeNotifier { } var shouldRemovedDirs = []; for (var chapter in chapters) { - var dir = Directory(FilePath.join(c.baseDir, chapter)); + var dir = Directory(FilePath.join( + c.baseDir, + getChapterDirectoryName(chapter), + )); if (dir.existsSync()) { shouldRemovedDirs.add(dir); } @@ -668,6 +672,21 @@ class LocalManager with ChangeNotifier { } }); } + + static String getChapterDirectoryName(String name) { + var builder = StringBuffer(); + for (var i = 0; i < name.length; i++) { + var char = name[i]; + if (char == '/' || char == '\\' || char == ':' || char == '*' || + char == '?' + || char == '"' || char == '<' || char == '>' || char == '|') { + builder.write('_'); + } else { + builder.write(char); + } + } + return builder.toString(); + } } enum LocalSortType { diff --git a/lib/main.dart b/lib/main.dart index 99b2701..93d131d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -199,6 +199,7 @@ class _MyAppState extends State with WidgetsBindingObserver { tertiary = light.tertiary; } return MaterialApp( + title: "venera", home: home, debugShowCheckedModeBanner: false, theme: getTheme(primary, secondary, tertiary, Brightness.light), @@ -246,7 +247,7 @@ class _MyAppState extends State with WidgetsBindingObserver { /// https://github.com/flutter/flutter/issues/161086 var isPaddingCheckError = MediaQuery.of(context).viewPadding.top <= 0 || - MediaQuery.of(context).viewPadding.top > 50; + MediaQuery.of(context).viewPadding.top > 200; if (isPaddingCheckError && Platform.isAndroid) { widget = MediaQuery( diff --git a/lib/network/download.dart b/lib/network/download.dart index 7ab6777..269ec83 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -107,7 +107,21 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { var local = LocalManager().find(id, comicType); if (path != null) { if (local == null) { - Directory(path!).deleteIgnoreError(recursive: true); + Future.sync(() async { + var tasks = this.tasks.values.toList(); + for (var i = 0; i < tasks.length; i++) { + if (!tasks[i].isComplete) { + tasks[i].cancel(); + await tasks[i].wait(); + } + } + try { + await Directory(path!).delete(recursive: true); + } + catch(e) { + Log.error("Download", "Failed to delete directory: $e"); + } + }); } else if (chapters != null) { for (var c in chapters!) { var dir = Directory(FilePath.join(path!, c)); @@ -197,7 +211,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { if (comic!.chapters != null) { saveTo = Directory(FilePath.join( path!, - _images!.keys.elementAt(_chapter), + LocalManager.getChapterDirectoryName( + _images!.keys.elementAt(_chapter), + ), )); if (!saveTo.existsSync()) { saveTo.createSync(recursive: true); diff --git a/lib/utils/channel.dart b/lib/utils/channel.dart new file mode 100644 index 0000000..2731b39 --- /dev/null +++ b/lib/utils/channel.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'dart:collection'; + +class Channel { + final Queue _queue; + + final int size; + + Channel(this.size) : _queue = Queue(); + + Completer? _releaseCompleter; + + Completer? _pushCompleter; + + var currentSize = 0; + + var isClosed = false; + + Future push(T item) async { + if (currentSize >= size) { + _releaseCompleter ??= Completer(); + return _releaseCompleter!.future.then((_) { + if (isClosed) { + return; + } + _queue.addLast(item); + currentSize++; + }); + } + _queue.addLast(item); + currentSize++; + _pushCompleter?.complete(); + _pushCompleter = null; + } + + Future pop() async { + while (_queue.isEmpty) { + if (isClosed) { + return null; + } + _pushCompleter ??= Completer(); + await _pushCompleter!.future; + } + var item = _queue.removeFirst(); + currentSize--; + if (_releaseCompleter != null && currentSize < size) { + _releaseCompleter!.complete(); + _releaseCompleter = null; + } + return item; + } + + void close() { + isClosed = true; + _pushCompleter?.complete(); + _releaseCompleter?.complete(); + } +} \ No newline at end of file diff --git a/patch/font.dart b/patch/font.dart new file mode 100644 index 0000000..c6218a6 --- /dev/null +++ b/patch/font.dart @@ -0,0 +1,28 @@ +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:dio/dio.dart'; + +void main() async { + const harmonySansLink = "https://developer.huawei.com/images/download/general/HarmonyOS-Sans.zip"; + + var dio = Dio(); + await dio.download(harmonySansLink, "HarmonyOS-Sans.zip"); + await extractFileToDisk("HarmonyOS-Sans.zip", "./assets/"); + File("HarmonyOS-Sans.zip").deleteSync(); + + var pubspec = await File("pubspec.yaml").readAsString(); + pubspec = pubspec.replaceFirst("# fonts:", +""" fonts: + - family: HarmonyOS Sans + fonts: + - asset: assets/HarmonyOS Sans/HarmonyOS_Sans_SC/HarmonyOS_Sans_SC_Regular.ttf +"""); + await File("pubspec.yaml").writeAsString(pubspec); + + var mainDart = await File("lib/main.dart").readAsString(); + mainDart = mainDart.replaceFirst("Noto Sans CJK", "HarmonyOS Sans"); + await File("lib/main.dart").writeAsString(mainDart); + + print("Successfully patched font."); +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 1d94e5e..5c713bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + archive: + dependency: "direct dev" + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: transitive description: @@ -770,6 +778,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" rhttp: dependency: "direct main" description: @@ -1116,4 +1132,4 @@ packages: version: "0.0.12" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.35.2" + flutter: ">=3.35.5" diff --git a/pubspec.yaml b/pubspec.yaml index 3da3c9c..a7cccbf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,11 +2,11 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.5.1+151 +version: 1.5.2+152 environment: sdk: '>=3.8.0 <4.0.0' - flutter: 3.35.3 + flutter: 3.35.5 dependencies: flutter: @@ -94,6 +94,7 @@ dev_dependencies: flutter_lints: ^5.0.0 flutter_to_arch: ^1.0.1 flutter_to_debian: ^2.0.2 + archive: any flutter: uses-material-design: true @@ -104,6 +105,7 @@ flutter: - assets/tags.json - assets/tags_tw.json - assets/opencc.txt +# fonts: flutter_to_arch: name: Venera diff --git a/test/channel_test.dart b/test/channel_test.dart new file mode 100644 index 0000000..b906738 --- /dev/null +++ b/test/channel_test.dart @@ -0,0 +1,115 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:venera/utils/channel.dart'; + +void main() { + test("1-1-1", () async { + var channel = Channel(1); + await channel.push(1); + var item = await channel.pop(); + expect(item, 1); + }); + + test("1-3-1", () async { + var channel = Channel(1); + + // producer + () async { + await channel.push(1); + }(); + () async { + await channel.push(2); + }(); + () async { + await channel.push(3); + }(); + + // consumer + var results = []; + for (var i = 0; i < 3; i++) { + var item = await channel.pop(); + if (item != null) { + results.add(item); + } + } + expect(results.length, 3); + }); + + test("2-3-1", () async { + var channel = Channel(2); + + // producer + () async { + await channel.push(1); + }(); + () async { + await channel.push(2); + }(); + () async { + await channel.push(3); + }(); + + // consumer + var results = []; + for (var i = 0; i < 3; i++) { + var item = await channel.pop(); + if (item != null) { + results.add(item); + } + } + expect(results.length, 3); + }); + + test("1-1-3", () async { + var channel = Channel(1); + + // producer + () async { + print("push 1"); + await channel.push(1); + print("push 2"); + await channel.push(2); + print("push 3"); + await channel.push(3); + print("push done"); + channel.close(); + }(); + + // consumer + var consumers = []; + var results = []; + for (var i = 0; i < 3; i++) { + consumers.add(() async { + while (true) { + var item = await channel.pop(); + if (item == null) { + break; + } + print("pop $item"); + results.add(item); + } + }()); + } + + await Future.wait(consumers); + expect(results.length, 3); + }); + + test("close", () async { + var channel = Channel(2); + + // producer + () async { + await channel.push(1); + await channel.push(2); + await channel.push(3); + channel.close(); + }(); + + // consumer + await channel.pop(); + await channel.pop(); + await channel.pop(); + var item4 = await channel.pop(); + expect(item4, null); + }); +} \ No newline at end of file