diff --git a/README.md b/README.md index 160b3d0..5b70569 100644 --- a/README.md +++ b/README.md @@ -34,4 +34,7 @@ See [Comic Source](doc/comic_source.md) ### Tags Translation [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=EhTagTranslation&repo=Database)](https://github.com/EhTagTranslation/Database) +## Headless Mode +See [Headless Doc](doc/headless_doc.md) + The Chinese translation of the manga tags is from this project. diff --git a/doc/headless_doc.md b/doc/headless_doc.md new file mode 100644 index 0000000..441543f --- /dev/null +++ b/doc/headless_doc.md @@ -0,0 +1,180 @@ +# Venera Headless Mode + +Venera's headless mode allows you to run key features from the command line, making it easy to automate tasks and integrate with other tools. This document outlines the available commands and their usage. + +## How to Use + +To activate headless mode, use the `--headless` flag when running the Venera executable, followed by the desired command. + +```bash +venera --headless [subcommand] [options] +``` + +## Global Options + +- **`--ignore-disheadless-log`**: Suppresses log output, providing a cleaner output for scripting. + +## Commands + +### `webdav` + +Manage WebDAV data synchronization. + +- **`webdav up`**: Uploads your local configuration to the WebDAV server. +- **`webdav down`**: Downloads and applies the remote configuration from the WebDAV server. + +**Example:** + +```bash +venera --headless webdav up +``` + +### `updatescript` + +Update comic source scripts. + +- **`updatescript all`**: Checks for and applies all available updates for your comic source scripts. + +**Example:** + +```bash +venera --headless updatescript all +``` + +**Output Format:** + +The `updatescript` command provides detailed progress and a final summary. + +**Progress Logs:** + +- **`Progress`**: Indicates a successful update for a single script. +- **`ProgressError`**: Indicates a failure during a script update. + +**Example `Progress` Log:** + +```json +{ + "status": "running", + "message": "Progress", + "data": { + "current": 1, + "total": 5, + "source": { + "key": "source-key", + "name": "Source Name", + "version": "1.0.0", + "url": "https://example.com/source.js" + } + } +} +``` + +**Final Summary:** + +A summary is provided at the end, detailing the total number of scripts, how many were updated, and how many failed. + +```json +{ + "status": "success", + "message": "All scripts updated.", + "data": { + "total": 5, + "updated": 4, + "errors": 1 + } +} +``` + +### `updatesubscribe` + +Update your subscribed comics and retrieve a list of updated comics. + +- **`updatesubscribe`**: Checks all subscribed comics for updates. +- **`updatesubscribe --update-comic-by-id-type `**: Updates a single comic specified by its `id` and `type`. + +**Example:** + +```bash +# Update all subscriptions +venera --headless updatesubscribe + +# Update a single comic +venera --headless updatesubscribe --update-comic-by-id-type "comic-id" "source-key" +``` + +## Output Format + +All headless commands output JSON objects prefixed with `[CLI PRINT]`. This structured format allows for easy parsing in automated scripts. The JSON object always contains a `status` and a `message`. For commands that return data, a `data` field will also be present. + +### `updatesubscribe` Output + +The `updatesubscribe` command provides detailed progress and final results in JSON format. + +**Progress Logs:** + +During an update, you will receive `Progress` or `ProgressError` messages. + +- **`Progress`**: Indicates a successful step in the update process. +- **`ProgressError`**: Indicates an error occurred while updating a specific comic. + +**Example `Progress` Log:** + +```json +{ + "status": "running", + "message": "Progress", + "data": { + "current": 1, + "total": 10, + "comic": { + "id": "some-comic-id", + "name": "Some Comic Name", + "coverUrl": "https://example.com/cover.jpg", + "author": "Author Name", + "type": "source-key", + "updateTime": "2023-10-27T12:00:00Z", + "tags": ["tag1", "tag2"] + } + } +} +``` + +**Example `ProgressError` Log:** + +```json +{ + "status": "running", + "message": "ProgressError", + "data": { + "current": 2, + "total": 10, + "comic": { + "id": "another-comic-id", + "name": "Another Comic Name", + ... + }, + "error": "Error message here" + } +} +``` + +**Final Output:** + +Once the update process is complete, a final JSON object is returned with a list of all comics that have been updated. + +```json +{ + "status": "success", + "message": "Updated comics list.", + "data": [ + { + "id": "some-comic-id", + "name": "Some Comic Name", + "coverUrl": "https://example.com/cover.jpg", + "author": "Author Name", + "type": "source-key", + "updateTime": "2023-10-27T12:00:00Z", + "tags": ["tag1", "tag2"] + } + ] +} diff --git a/lib/foundation/log.dart b/lib/foundation/log.dart index fba4caf..67a8b12 100644 --- a/lib/foundation/log.dart +++ b/lib/foundation/log.dart @@ -28,6 +28,8 @@ class Log { static bool ignoreLimitation = false; + static bool isMuted = false; + static void printWarning(String text) { debugPrint('\x1B[33m$text\x1B[0m'); } @@ -39,6 +41,7 @@ class Log { static IOSink? _file; static void addLog(LogLevel level, String title, String content) { + if (isMuted) return; if (_file == null) { Directory dir; if (App.isAndroid) { diff --git a/lib/headless.dart b/lib/headless.dart new file mode 100644 index 0000000..1811334 --- /dev/null +++ b/lib/headless.dart @@ -0,0 +1,244 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/widgets.dart'; +import 'package:venera/utils/data_sync.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; +import 'package:venera/foundation/log.dart'; +import 'package:venera/pages/comic_source_page.dart'; +import 'package:venera/init.dart'; +import 'package:venera/logic/follow_updates.dart'; +import 'package:venera/foundation/appdata.dart'; +import 'package:venera/foundation/favorites.dart'; + +void cliPrint(Map data) { + print('[CLI PRINT] ${jsonEncode(data)}'); +} + +Future runHeadlessMode(List args) async { + WidgetsFlutterBinding.ensureInitialized(); + if (args.contains('--ignore-disheadless-log')) { + Log.isMuted = true; + } + if(Platform.isLinux || Platform.isMacOS){ + Directory.current = Platform.environment['HOME']!; + } + // The first arg is '--headless', so we look at the next ones. + var commandIndex = args.indexOf('--headless') + 1; + if (commandIndex >= args.length) { + cliPrint({'status': 'error', 'message': 'No command provided for headless mode.'}); + exit(1); + } + + // Need to initialize the app for some features to work + await init(); + + var command = args[commandIndex]; + var subCommand = (commandIndex + 1 < args.length) ? args[commandIndex + 1] : null; + + switch (command) { + case 'webdav': + if (subCommand == 'up') { + cliPrint({'status': 'running', 'message': 'Uploading WebDAV data...'}); + await DataSync().uploadData(); + cliPrint({'status': 'success', 'message': 'Upload complete.'}); + } else if (subCommand == 'down') { + cliPrint({'status': 'running', 'message': 'Downloading WebDAV data...'}); + await DataSync().downloadData(); + cliPrint({'status': 'success', 'message': 'Download complete.'}); + } else { + cliPrint({'status': 'error', 'message': 'Invalid webdav command. Use "up" or "down".'}); + exit(1); + } + break; + case 'updatescript': + if (subCommand == 'all') { + cliPrint({'status': 'running', 'message': 'Checking for comic source script updates...'}); + await ComicSourcePage.checkComicSourceUpdate(); + var updates = ComicSourceManager().availableUpdates; + if (updates.isEmpty) { + cliPrint({'status': 'success', 'message': 'No updates found.'}); + } else { + var total = updates.length; + var current = 0; + var errors = 0; + var updated = 0; + cliPrint({ + 'status': 'running', + 'message': 'Updating all comic source scripts...', + 'data': { + 'total': total, + 'current': 0, + 'updated': 0, + 'errors': 0, + } + }); + for (var key in updates.keys) { + var source = ComicSource.find(key); + if (source != null) { + current++; + var data = { + 'current': current, + 'total': total, + 'source': { + 'key': source.key, + 'name': source.name, + 'version': source.version, + 'url': source.url, + } + }; + try { + await ComicSourcePage.update(source, false); + updated++; + cliPrint({ + 'status': 'running', + 'message': 'Progress', + 'data': data, + }); + } catch (e) { + errors++; + cliPrint({ + 'status': 'running', + 'message': 'ProgressError', + 'data': { + ...data, + 'error': e.toString(), + }, + }); + } + } + } + cliPrint({ + 'status': 'success', + 'message': 'All scripts updated.', + 'data': { + 'total': total, + 'updated': updated, + 'errors': errors, + } + }); + } + } else { + cliPrint({'status': 'error', 'message': 'Invalid updatescript command. Use "all".'}); + exit(1); + } + break; + case 'updatesubscribe': + cliPrint({'status': 'running', 'message': 'Updating subscribed comics...'}); + var folder = appdata.settings["followUpdatesFolder"]; + if (folder == null) { + cliPrint({'status': 'error', 'message': 'Follow updates folder is not configured.'}); + exit(1); + } + + var updateIndex = args.indexOf('--update-comic-by-id-type'); + if (updateIndex != -1) { + var id = args[updateIndex + 1]; + var type = args[updateIndex + 2]; + var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder); + var comic = comics.firstWhere((c) => c.id == id && c.type.sourceKey == type); + + var result = await updateComic(comic, folder); + + Map data = { + 'current': 1, + 'total': 1, + 'comic': { + 'id': comic.id, + 'name': comic.name, + 'coverUrl': comic.coverPath, + 'author': comic.author, + 'type': comic.type.sourceKey, + 'updateTime': comic.updateTime, + 'tags': comic.tags, + } + }; + + var message = 'Progress'; + if (result.errorMessage != null) { + message = 'ProgressError'; + data['error'] = result.errorMessage; + } + + cliPrint({ + 'status': 'running', + 'message': message, + 'data': data, + }); + + cliPrint({ + 'status': 'running', + 'message': 'Update check complete.', + 'data': { + 'total': 1, + 'updated': result.updated ? 1 : 0, + 'errors': result.errorMessage != null ? 1 : 0, + } + }); + + await Future.delayed(const Duration(milliseconds: 500)); + var json = await getUpdatedComicsAsJson(folder); + cliPrint({ + 'status': result.errorMessage != null ? 'error' : 'success', + 'message': 'Updated comics list.', + 'data': jsonDecode(json), + }); + } else { + int total = 0; + int updated = 0; + int errors = 0; + await for (var progress in updateFolder(folder, true)) { + total = progress.total; + updated = progress.updated; + errors = progress.errors; + Map data = { + 'current': progress.current, + 'total': progress.total, + }; + if (progress.comic != null) { + data['comic'] = { + 'id': progress.comic!.id, + 'name': progress.comic!.name, + 'coverUrl': progress.comic!.coverPath, + 'author': progress.comic!.author, + 'type': progress.comic!.type.sourceKey, + 'updateTime': progress.comic!.updateTime, + 'tags': progress.comic!.tags, + }; + } + var message = 'Progress'; + if (progress.errorMessage != null) { + message = 'ProgressError'; + data['error'] = progress.errorMessage; + } + cliPrint({ + 'status': 'running', + 'message': message, + 'data': data, + }); + } + cliPrint({ + 'status': 'running', + 'message': 'Update check complete.', + 'data': { + 'total': total, + 'updated': updated, + 'errors': errors, + } + }); + await Future.delayed(const Duration(milliseconds: 500)); + var json = await getUpdatedComicsAsJson(folder); + cliPrint({ + 'status': errors > 0 ? 'error' : 'success', + 'message': 'Updated comics list.', + 'data': jsonDecode(json), + }); + } + break; + default: + cliPrint({'status': 'error', 'message': 'Unknown command: $command'}); + exit(1); + } + + // Exit after command execution + exit(0); +} diff --git a/lib/init.dart b/lib/init.dart index 3e21d24..e9008c1 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -37,17 +37,21 @@ extension _FutureInit on Future { Future init() async { await App.init().wait(); await SingleInstanceCookieJar.createInstance(); - var futures = [ - Rhttp.init(), - App.initComponents(), - SAFTaskWorker().init().wait(), - AppTranslation.init().wait(), - TagsTranslation.readData().wait(), - JsEngine().init().wait(), - ComicSourceManager().init().wait(), - OpenCC.init(), - ]; - await Future.wait(futures); + try { + var futures = [ + Rhttp.init(), + App.initComponents(), + SAFTaskWorker().init().wait(), + AppTranslation.init().wait(), + TagsTranslation.readData().wait(), + JsEngine().init().wait(), + ComicSourceManager().init().wait(), + OpenCC.init(), + ]; + await Future.wait(futures); + } catch (e, s) { + Log.error("init", "$e\n$s"); + } CacheManager().setLimitSize(appdata.settings['cacheSize']); _checkOldConfigs(); if (App.isAndroid) { diff --git a/lib/logic/follow_updates.dart b/lib/logic/follow_updates.dart new file mode 100644 index 0000000..98144c3 --- /dev/null +++ b/lib/logic/follow_updates.dart @@ -0,0 +1,162 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:venera/foundation/favorites.dart'; +import 'package:venera/foundation/log.dart'; + +class ComicUpdateResult { + final bool updated; + final String? errorMessage; + + ComicUpdateResult(this.updated, this.errorMessage); +} + +Future updateComic( + FavoriteItemWithUpdateInfo c, String folder) async { + int retries = 3; + while (true) { + try { + var comicSource = c.type.comicSource; + if (comicSource == null) { + return ComicUpdateResult(false, "Comic source not found"); + } + 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"); + } + } + + var item = FavoriteItem( + id: c.id, + name: newInfo.title, + coverPath: newInfo.cover, + author: newInfo.subTitle ?? + newInfo.tags['author']?.firstOrNull ?? + c.author, + type: c.type, + tags: newTags, + ); + + LocalFavoritesManager().updateInfo(folder, item, false); + + var updated = false; + var updateTime = newInfo.findUpdateTime(); + if (updateTime != null && updateTime != c.updateTime) { + LocalFavoritesManager().updateUpdateTime( + folder, + c.id, + c.type, + updateTime, + ); + updated = true; + } else { + LocalFavoritesManager().updateCheckTime(folder, c.id, c.type); + } + return ComicUpdateResult(updated, null); + } catch (e, s) { + Log.error("Check Updates", e, s); + retries--; + if (retries == 0) { + return ComicUpdateResult(false, e.toString()); + } + } + } +} + +class UpdateProgress { + final int total; + final int current; + final int errors; + final int updated; + final FavoriteItemWithUpdateInfo? comic; + final String? errorMessage; + + UpdateProgress(this.total, this.current, this.errors, this.updated, + [this.comic, this.errorMessage]); +} + +void updateFolderBase( + String folder, + StreamController stream, + bool ignoreCheckTime, +) async { + var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder); + int total = comics.length; + int current = 0; + int errors = 0; + int updated = 0; + + stream.add(UpdateProgress(total, current, errors, updated)); + + var comicsToUpdate = []; + + for (var comic in comics) { + if (!ignoreCheckTime) { + var lastCheckTime = comic.lastCheckTime; + if (lastCheckTime != null && + DateTime.now().difference(lastCheckTime).inDays < 1) { + current++; + stream.add(UpdateProgress(total, current, errors, updated)); + continue; + } + } + comicsToUpdate.add(comic); + } + + total = comicsToUpdate.length; + 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++; + } + if (result.errorMessage != null) { + errors++; + } + stream.add( + UpdateProgress(total, current, errors, updated, comic, result.errorMessage)); + }); + futures.add(future); + } + + await Future.wait(futures); + + if (updated > 0) { + LocalFavoritesManager().notifyChanges(); + } + + stream.close(); +} + + +Stream updateFolder(String folder, bool ignoreCheckTime) { + var stream = StreamController(); + updateFolderBase(folder, stream, ignoreCheckTime); + return stream.stream; +} + +Future getUpdatedComicsAsJson(String folder) async { + var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder); + var updatedComics = comics.where((c) => c.hasNewUpdate).toList(); + var jsonList = updatedComics.map((c) => { + 'id': c.id, + 'name': c.name, + 'coverUrl': c.coverPath, + 'author': c.author, + 'type': c.type.sourceKey, + 'updateTime': c.updateTime, + 'tags': c.tags, + }).toList(); + return jsonEncode(jsonList); +} diff --git a/lib/main.dart b/lib/main.dart index c4b932d..6c8d899 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,9 +14,14 @@ import 'components/components.dart'; import 'components/window_frame.dart'; import 'foundation/app.dart'; import 'foundation/appdata.dart'; +import 'headless.dart'; import 'init.dart'; void main(List args) { + if (args.contains('--headless')) { + runHeadlessMode(args); + return; + } if (runWebViewTitleBarWidget(args)) return; overrideIO(() { runZonedGuarded(() async { diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 42ebe9c..9235710 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -18,6 +18,54 @@ import 'package:venera/utils/translations.dart'; class ComicSourcePage extends StatelessWidget { const ComicSourcePage({super.key}); + static Future update( + ComicSource source, [ + bool showLoading = true, + ]) async { + if (!source.url.isURL) { + if (showLoading) { + App.rootContext.showMessage(message: "Invalid url config"); + return; + } else { + throw Exception("Invalid url config"); + } + } + ComicSourceManager().remove(source.key); + bool cancel = false; + LoadingDialogController? controller; + if (showLoading) { + controller = showLoadingDialog( + App.rootContext, + onCancel: () => cancel = true, + barrierDismissible: false, + ); + } + try { + var res = await AppDio().get( + source.url, + options: Options(responseType: ResponseType.plain), + ); + if (cancel) return; + controller?.close(); + await ComicSourceParser().parse(res.data!, source.filePath); + await io.File(source.filePath).writeAsString(res.data!); + if (ComicSourceManager().availableUpdates.containsKey(source.key)) { + ComicSourceManager().availableUpdates.remove(source.key); + } + } catch (e) { + if (cancel) return; + if (showLoading) { + App.rootContext.showMessage(message: e.toString()); + } else { + rethrow; + } + } + await ComicSourceManager().reload(); + if (showLoading) { + App.forceRebuild(); + } + } + static Future checkComicSourceUpdate() async { if (ComicSource.all().isEmpty) { return 0; @@ -152,42 +200,11 @@ class _BodyState extends State<_Body> { ); } - static Future update( + void update( ComicSource source, [ bool showLoading = true, - ]) async { - if (!source.url.isURL) { - App.rootContext.showMessage(message: "Invalid url config"); - return; - } - ComicSourceManager().remove(source.key); - bool cancel = false; - LoadingDialogController? controller; - if (showLoading) { - controller = showLoadingDialog( - App.rootContext, - onCancel: () => cancel = true, - barrierDismissible: false, - ); - } - try { - var res = await AppDio().get( - source.url, - options: Options(responseType: ResponseType.plain), - ); - if (cancel) return; - controller?.close(); - await ComicSourceParser().parse(res.data!, source.filePath); - await File(source.filePath).writeAsString(res.data!); - if (ComicSourceManager().availableUpdates.containsKey(source.key)) { - ComicSourceManager().availableUpdates.remove(source.key); - } - } catch (e) { - if (cancel) return; - App.rootContext.showMessage(message: e.toString()); - } - await ComicSourceManager().reload(); - App.forceRebuild(); + ]) { + ComicSourcePage.update(source, showLoading); } Widget buildCard(BuildContext context) { @@ -679,7 +696,7 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> { var shouldUpdate = ComicSourceManager().availableUpdates.keys.toList(); for (var key in shouldUpdate) { var source = ComicSource.find(key)!; - await _BodyState.update(source, false); + await ComicSourcePage.update(source, false); current++; loadingController.setProgress(current / total); } diff --git a/lib/pages/follow_updates_page.dart b/lib/pages/follow_updates_page.dart index 0633f49..3d92fb7 100644 --- a/lib/pages/follow_updates_page.dart +++ b/lib/pages/follow_updates_page.dart @@ -5,10 +5,10 @@ import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/favorites.dart'; -import 'package:venera/foundation/log.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/translations.dart'; import '../foundation/global_state.dart'; +import 'package:venera/logic/follow_updates.dart'; class FollowUpdatesWidget extends StatefulWidget { const FollowUpdatesWidget({super.key}); @@ -460,7 +460,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { message: "Updating comics...".tl, ); - await for (var progress in _updateFolder(folder, true)) { + await for (var progress in updateFolder(folder, true)) { if (isCanceled) { return; } @@ -497,7 +497,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { int updated = 0; - await for (var progress in _updateFolder(folder!, true)) { + await for (var progress in updateFolder(folder!, true)) { if (isCanceled) { return; } @@ -532,128 +532,6 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { Object? get key => 'FollowUpdatesPage'; } -class _UpdateProgress { - final int total; - final int current; - final int errors; - final int updated; - - _UpdateProgress(this.total, this.current, this.errors, this.updated); -} - -void _updateFolderBase( - String folder, - StreamController<_UpdateProgress> stream, - bool ignoreCheckTime, -) async { - var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder); - int current = 0; - int errors = 0; - int updated = 0; - var futures = []; - const maxConcurrent = 5; - - for (int i = 0; i < comics.length; i++) { - if (stream.isClosed) { - return; - } - if (!ignoreCheckTime) { - var lastCheckTime = comics[i].lastCheckTime; - if (lastCheckTime != null && - DateTime.now().difference(lastCheckTime).inDays < 1) { - current++; - stream.add(_UpdateProgress(comics.length, current, errors, updated)); - continue; - } - } - - if (futures.length >= maxConcurrent) { - await Future.any(futures); - } - - var future = () async { - int retries = 3; - while (true) { - try { - var c = comics[i]; - var comicSource = c.type.comicSource; - if (comicSource == null) return; - 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"); - } - } - - var item = FavoriteItem( - id: c.id, - name: newInfo.title, - coverPath: newInfo.cover, - author: newInfo.subTitle ?? - newInfo.tags['author']?.firstOrNull ?? - c.author, - type: c.type, - tags: newTags, - ); - - LocalFavoritesManager().updateInfo(folder, item, false); - - var updateTime = newInfo.findUpdateTime(); - if (updateTime != null && updateTime != c.updateTime) { - LocalFavoritesManager().updateUpdateTime( - folder, - c.id, - c.type, - updateTime, - ); - } else { - LocalFavoritesManager().updateCheckTime(folder, c.id, c.type); - } - updated++; - return; - } catch (e, s) { - Log.error("Check Updates", e, s); - retries--; - if (retries == 0) { - errors++; - return; - } - } finally { - current++; - stream.add(_UpdateProgress(comics.length, current, errors, updated)); - } - } - }(); - - future.then((_) { - futures.remove(future); - }); - - futures.add(future); - } - - await Future.wait(futures); - - if (updated > 0) { - LocalFavoritesManager().notifyChanges(); - } - - stream.close(); -} - -Stream<_UpdateProgress> _updateFolder(String folder, bool ignoreCheckTime) { - var stream = StreamController<_UpdateProgress>(); - _updateFolderBase(folder, stream, ignoreCheckTime); - return stream.stream; -} - /// Background service for checking updates abstract class FollowUpdatesService { static bool _isChecking = false; @@ -683,7 +561,7 @@ abstract class FollowUpdatesService { int updated = 0; try { - await for (var progress in _updateFolder(folder, false)) { + await for (var progress in updateFolder(folder, false)) { if (isCanceled) { return; }