diff --git a/lib/eval/javascript/service.dart b/lib/eval/javascript/service.dart index 633a47e2..3072d363 100644 --- a/lib/eval/javascript/service.dart +++ b/lib/eval/javascript/service.dart @@ -33,11 +33,12 @@ class JsExtensionService implements ExtensionService { JsUtils(runtime).init(); JsVideosExtractors(runtime).init(); JsPreferences(runtime, source).init(); + final sourceJson = jsonEncode(source.toMSource().toJson()); runtime.evaluate(''' class MProvider { get source() { - return JSON.parse('${jsonEncode(source.toMSource().toJson())}'); + return $sourceJson; } get supportsLatest() { throw new Error("supportsLatest not implemented"); @@ -96,7 +97,7 @@ var extention = new DefaultExtension(); @override Map getHeaders() { return _extensionCall( - 'getHeaders(`${source.baseUrl ?? ''}`)', + 'getHeaders(${jsonEncode(source.baseUrl ?? '')})', {}, ).toMapStringString!; } @@ -127,14 +128,16 @@ var extention = new DefaultExtension(); Future search(String query, int page, List filters) async { return MPages.fromJson( await _extensionCallAsync( - 'search("$query",$page,${jsonEncode(filterValuesListToJson(filters))})', + 'search(${jsonEncode(query)},$page,${jsonEncode(filterValuesListToJson(filters))})', ), ); } @override Future getDetail(String url) async { - return MManga.fromJson(await _extensionCallAsync('getDetail(`$url`)')); + return MManga.fromJson( + await _extensionCallAsync('getDetail(${jsonEncode(url)})'), + ); } @override @@ -144,7 +147,9 @@ var extention = new DefaultExtension(); hashCode: (p) => p.url.hashCode, ); - for (final e in await _extensionCallAsync('getPageList(`$url`)')) { + for (final e in await _extensionCallAsync( + 'getPageList(${jsonEncode(url)})', + )) { if (e != null) { final page = e is String ? PageUrl(e.trim()) @@ -164,7 +169,7 @@ var extention = new DefaultExtension(); ); for (final element in await _extensionCallAsync( - 'getVideoList(`$url`)', + 'getVideoList(${jsonEncode(url)})', )) { if (element['url'] != null && element['originalUrl'] != null) { videos.add(Video.fromJson(element)); @@ -178,7 +183,7 @@ var extention = new DefaultExtension(); _init(); final res = (await runtime.handlePromise( await runtime.evaluateAsync( - 'jsonStringify(() => extention.getHtmlContent(`$name`, `$url`))', + 'jsonStringify(() => extention.getHtmlContent(${jsonEncode(name)}, ${jsonEncode(url)}))', ), )).stringResult; return res; @@ -189,7 +194,7 @@ var extention = new DefaultExtension(); _init(); final res = (await runtime.handlePromise( await runtime.evaluateAsync( - 'jsonStringify(() => extention.cleanHtmlContent(`$html`))', + 'jsonStringify(() => extention.cleanHtmlContent(${jsonEncode(html)}))', ), )).stringResult; return res; diff --git a/lib/eval/lnreader/service.dart b/lib/eval/lnreader/service.dart index 88664cc0..28ef9e80 100644 --- a/lib/eval/lnreader/service.dart +++ b/lib/eval/lnreader/service.dart @@ -154,7 +154,10 @@ const extension = exports.default; @override Future search(String query, int page, List filters) async { final items = - ((await _extensionCallAsync('searchNovels("$query",$page)', []))) + ((await _extensionCallAsync( + 'searchNovels(${jsonEncode(query)},$page)', + [], + ))) .map((e) => NovelItem.fromJson(e)) .map( (e) => MManga( @@ -171,10 +174,13 @@ const extension = exports.default; @override Future getDetail(String url) async { final item = SourceNovel.fromJson( - await _extensionCallAsync('parseNovel(`$url`)', {}), + await _extensionCallAsync('parseNovel(${jsonEncode(url)})', {}), ); final chapters = SourcePage.fromJson( - await _extensionCallAsync('parsePage(`${item.path}`, `1`)', {}), + await _extensionCallAsync( + 'parsePage(${jsonEncode(item.path)}, ${jsonEncode('1')})', + {}, + ), ); final chaps = ((chapters.chapters.isNotEmpty ? chapters.chapters : item.chapters) @@ -225,7 +231,7 @@ const extension = exports.default; _init(); final res = (await runtime.handlePromise( await runtime.evaluateAsync( - 'jsonStringify(() => extension.parseChapter(`$url`))', + 'jsonStringify(() => extension.parseChapter(${jsonEncode(url)}))', ), )).stringResult; return res; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 306768eb..3df361a3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -71,6 +71,188 @@ "latest": "Latest", "extensions": "Extensions", "migrate": "Migrate", + "mass_migration_title": "Mass migration", + "mass_migration_preview_items": "Preview items", + "mass_migration_destination_source": "Destination source", + "mass_migration_no_library_items": "No library items are available for mass migration.", + "mass_migration_no_destination_sources": "No installed destination sources are available.", + "mass_migration_installed": "Installed", + "mass_migration_items_ready_for_review": "{count, plural, =1{1 item ready for review} other{{count} items ready for review}}", + "@mass_migration_items_ready_for_review": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_item_count": "{count, plural, =1{1 item} other{{count} items}}", + "@mass_migration_item_count": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_select_destination_source": "Select destination source", + "mass_migration_finding_matches": "Finding matches in {source} • {language}", + "@mass_migration_finding_matches": { + "placeholders": { + "source": {}, + "language": {} + } + }, + "mass_migration_processing_item": "Processing item {current} of {total}", + "@mass_migration_processing_item": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "mass_migration_waiting_next_item": "Waiting 2 seconds before the next item...", + "mass_migration_waiting_next_migration": "Waiting 2 seconds before the next migration...", + "mass_migration_matched_so_far": "Matched so far: {count}", + "@mass_migration_matched_so_far": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_no_match_count": "No match: {count}", + "@mass_migration_no_match_count": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_review_matches": "Review matches for {source}", + "@mass_migration_review_matches": { + "placeholders": { + "source": {} + } + }, + "mass_migration_found_matches": "Found matches: {count}", + "@mass_migration_found_matches": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_no_matches": "No matches: {count}", + "@mass_migration_no_matches": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_selected_to_migrate": "Selected to migrate: {count}", + "@mass_migration_selected_to_migrate": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_finish_review": "Finish review", + "mass_migration_migrate_selected": "Migrate selected items ({count})", + "@mass_migration_migrate_selected": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_migrating_selected": "Migrating selected items to {source}", + "@mass_migration_migrating_selected": { + "placeholders": { + "source": {} + } + }, + "mass_migration_no_items_selected": "No items selected for migration.", + "mass_migration_migrating_item": "Migrating item {current} of {total}", + "@mass_migration_migrating_item": { + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "mass_migration_complete": "Mass migration complete", + "mass_migration_complete_success_message": "All selected items were processed successfully.", + "mass_migration_complete_partial_message": "Migration finished with a few items that still need manual attention.", + "mass_migration_route_summary": "{source} → {destination}", + "@mass_migration_route_summary": { + "placeholders": { + "source": {}, + "destination": {} + } + }, + "mass_migration_processed": "Processed", + "mass_migration_matched": "Matched", + "mass_migration_migrated": "Migrated", + "mass_migration_skipped": "Skipped", + "mass_migration_failed": "Failed", + "mass_migration_failed_items": "Failed Items", + "mass_migration_exit": "Exit Mass Migration", + "mass_migration_no_destination_match": "No destination match found", + "mass_migration_query": "Query: {query}", + "@mass_migration_query": { + "placeholders": { + "query": {} + } + }, + "mass_migration_skip": "Skip", + "mass_migration_loading": "Loading...", + "mass_migration_choose_another_result": "Choose another result", + "mass_migration_source_chapters": "Source chapters", + "mass_migration_destination_chapters": "Destination chapters", + "mass_migration_chapter_count": "{count, plural, =1{1 chapter} other{{count} chapters}}", + "@mass_migration_chapter_count": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_source_chapter_count": "{count, plural, =1{1 source chapter} other{{count} source chapters}}", + "@mass_migration_source_chapter_count": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_destination_chapter_count": "{count, plural, =1{1 destination chapter} other{{count} destination chapters}}", + "@mass_migration_destination_chapter_count": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_no_chapters_found": "No chapters found.", + "mass_migration_and_more_chapters": "And {count} more...", + "@mass_migration_and_more_chapters": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mass_migration_unknown_title": "Unknown title", + "mass_migration_unknown_match": "Unknown match", + "mass_migration_unknown_source": "Unknown source", + "mass_migration_unknown_chapter": "Unknown chapter", "migrate_confirm": "Migrate to another source", "clean_database": "Clean database", "cleaned_database": "Database cleaned! {x} entries removed", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 13977f77..cafe66bd 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -503,6 +503,312 @@ abstract class AppLocalizations { /// **'Migrate'** String get migrate; + /// No description provided for @mass_migration_title. + /// + /// In en, this message translates to: + /// **'Mass migration'** + String get mass_migration_title; + + /// No description provided for @mass_migration_preview_items. + /// + /// In en, this message translates to: + /// **'Preview items'** + String get mass_migration_preview_items; + + /// No description provided for @mass_migration_destination_source. + /// + /// In en, this message translates to: + /// **'Destination source'** + String get mass_migration_destination_source; + + /// No description provided for @mass_migration_no_library_items. + /// + /// In en, this message translates to: + /// **'No library items are available for mass migration.'** + String get mass_migration_no_library_items; + + /// No description provided for @mass_migration_no_destination_sources. + /// + /// In en, this message translates to: + /// **'No installed destination sources are available.'** + String get mass_migration_no_destination_sources; + + /// No description provided for @mass_migration_installed. + /// + /// In en, this message translates to: + /// **'Installed'** + String get mass_migration_installed; + + /// No description provided for @mass_migration_items_ready_for_review. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 item ready for review} other{{count} items ready for review}}'** + String mass_migration_items_ready_for_review(int count); + + /// No description provided for @mass_migration_item_count. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 item} other{{count} items}}'** + String mass_migration_item_count(int count); + + /// No description provided for @mass_migration_select_destination_source. + /// + /// In en, this message translates to: + /// **'Select destination source'** + String get mass_migration_select_destination_source; + + /// No description provided for @mass_migration_finding_matches. + /// + /// In en, this message translates to: + /// **'Finding matches in {source} • {language}'** + String mass_migration_finding_matches(Object source, Object language); + + /// No description provided for @mass_migration_processing_item. + /// + /// In en, this message translates to: + /// **'Processing item {current} of {total}'** + String mass_migration_processing_item(int current, int total); + + /// No description provided for @mass_migration_waiting_next_item. + /// + /// In en, this message translates to: + /// **'Waiting 2 seconds before the next item...'** + String get mass_migration_waiting_next_item; + + /// No description provided for @mass_migration_waiting_next_migration. + /// + /// In en, this message translates to: + /// **'Waiting 2 seconds before the next migration...'** + String get mass_migration_waiting_next_migration; + + /// No description provided for @mass_migration_matched_so_far. + /// + /// In en, this message translates to: + /// **'Matched so far: {count}'** + String mass_migration_matched_so_far(int count); + + /// No description provided for @mass_migration_no_match_count. + /// + /// In en, this message translates to: + /// **'No match: {count}'** + String mass_migration_no_match_count(int count); + + /// No description provided for @mass_migration_review_matches. + /// + /// In en, this message translates to: + /// **'Review matches for {source}'** + String mass_migration_review_matches(Object source); + + /// No description provided for @mass_migration_found_matches. + /// + /// In en, this message translates to: + /// **'Found matches: {count}'** + String mass_migration_found_matches(int count); + + /// No description provided for @mass_migration_no_matches. + /// + /// In en, this message translates to: + /// **'No matches: {count}'** + String mass_migration_no_matches(int count); + + /// No description provided for @mass_migration_selected_to_migrate. + /// + /// In en, this message translates to: + /// **'Selected to migrate: {count}'** + String mass_migration_selected_to_migrate(int count); + + /// No description provided for @mass_migration_finish_review. + /// + /// In en, this message translates to: + /// **'Finish review'** + String get mass_migration_finish_review; + + /// No description provided for @mass_migration_migrate_selected. + /// + /// In en, this message translates to: + /// **'Migrate selected items ({count})'** + String mass_migration_migrate_selected(int count); + + /// No description provided for @mass_migration_migrating_selected. + /// + /// In en, this message translates to: + /// **'Migrating selected items to {source}'** + String mass_migration_migrating_selected(Object source); + + /// No description provided for @mass_migration_no_items_selected. + /// + /// In en, this message translates to: + /// **'No items selected for migration.'** + String get mass_migration_no_items_selected; + + /// No description provided for @mass_migration_migrating_item. + /// + /// In en, this message translates to: + /// **'Migrating item {current} of {total}'** + String mass_migration_migrating_item(int current, int total); + + /// No description provided for @mass_migration_complete. + /// + /// In en, this message translates to: + /// **'Mass migration complete'** + String get mass_migration_complete; + + /// No description provided for @mass_migration_complete_success_message. + /// + /// In en, this message translates to: + /// **'All selected items were processed successfully.'** + String get mass_migration_complete_success_message; + + /// No description provided for @mass_migration_complete_partial_message. + /// + /// In en, this message translates to: + /// **'Migration finished with a few items that still need manual attention.'** + String get mass_migration_complete_partial_message; + + /// No description provided for @mass_migration_route_summary. + /// + /// In en, this message translates to: + /// **'{source} → {destination}'** + String mass_migration_route_summary(Object source, Object destination); + + /// No description provided for @mass_migration_processed. + /// + /// In en, this message translates to: + /// **'Processed'** + String get mass_migration_processed; + + /// No description provided for @mass_migration_matched. + /// + /// In en, this message translates to: + /// **'Matched'** + String get mass_migration_matched; + + /// No description provided for @mass_migration_migrated. + /// + /// In en, this message translates to: + /// **'Migrated'** + String get mass_migration_migrated; + + /// No description provided for @mass_migration_skipped. + /// + /// In en, this message translates to: + /// **'Skipped'** + String get mass_migration_skipped; + + /// No description provided for @mass_migration_failed. + /// + /// In en, this message translates to: + /// **'Failed'** + String get mass_migration_failed; + + /// No description provided for @mass_migration_failed_items. + /// + /// In en, this message translates to: + /// **'Failed Items'** + String get mass_migration_failed_items; + + /// No description provided for @mass_migration_exit. + /// + /// In en, this message translates to: + /// **'Exit Mass Migration'** + String get mass_migration_exit; + + /// No description provided for @mass_migration_no_destination_match. + /// + /// In en, this message translates to: + /// **'No destination match found'** + String get mass_migration_no_destination_match; + + /// No description provided for @mass_migration_query. + /// + /// In en, this message translates to: + /// **'Query: {query}'** + String mass_migration_query(Object query); + + /// No description provided for @mass_migration_skip. + /// + /// In en, this message translates to: + /// **'Skip'** + String get mass_migration_skip; + + /// No description provided for @mass_migration_loading. + /// + /// In en, this message translates to: + /// **'Loading...'** + String get mass_migration_loading; + + /// No description provided for @mass_migration_choose_another_result. + /// + /// In en, this message translates to: + /// **'Choose another result'** + String get mass_migration_choose_another_result; + + /// No description provided for @mass_migration_source_chapters. + /// + /// In en, this message translates to: + /// **'Source chapters'** + String get mass_migration_source_chapters; + + /// No description provided for @mass_migration_destination_chapters. + /// + /// In en, this message translates to: + /// **'Destination chapters'** + String get mass_migration_destination_chapters; + + /// No description provided for @mass_migration_chapter_count. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 chapter} other{{count} chapters}}'** + String mass_migration_chapter_count(int count); + + /// No description provided for @mass_migration_source_chapter_count. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 source chapter} other{{count} source chapters}}'** + String mass_migration_source_chapter_count(int count); + + /// No description provided for @mass_migration_destination_chapter_count. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 destination chapter} other{{count} destination chapters}}'** + String mass_migration_destination_chapter_count(int count); + + /// No description provided for @mass_migration_no_chapters_found. + /// + /// In en, this message translates to: + /// **'No chapters found.'** + String get mass_migration_no_chapters_found; + + /// No description provided for @mass_migration_and_more_chapters. + /// + /// In en, this message translates to: + /// **'And {count} more...'** + String mass_migration_and_more_chapters(int count); + + /// No description provided for @mass_migration_unknown_title. + /// + /// In en, this message translates to: + /// **'Unknown title'** + String get mass_migration_unknown_title; + + /// No description provided for @mass_migration_unknown_match. + /// + /// In en, this message translates to: + /// **'Unknown match'** + String get mass_migration_unknown_match; + + /// No description provided for @mass_migration_unknown_source. + /// + /// In en, this message translates to: + /// **'Unknown source'** + String get mass_migration_unknown_source; + + /// No description provided for @mass_migration_unknown_chapter. + /// + /// In en, this message translates to: + /// **'Unknown chapter'** + String get mass_migration_unknown_chapter; + /// No description provided for @migrate_confirm. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index 9dba8c3c..036199fb 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -210,6 +210,236 @@ class AppLocalizationsAr extends AppLocalizations { @override String get migrate => 'ترحيل'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'الانتقال إلى مصدر آخر'; diff --git a/lib/l10n/generated/app_localizations_as.dart b/lib/l10n/generated/app_localizations_as.dart index f69e180b..8ccc3c84 100644 --- a/lib/l10n/generated/app_localizations_as.dart +++ b/lib/l10n/generated/app_localizations_as.dart @@ -212,6 +212,236 @@ class AppLocalizationsAs extends AppLocalizations { @override String get migrate => 'স্থানান্তৰ'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'অন্য উৎসলৈ স্থানান্তৰ কৰক'; diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index dce513b0..346fa5b9 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -212,6 +212,236 @@ class AppLocalizationsDe extends AppLocalizations { @override String get migrate => 'Migrieren'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'Zu einer anderen Erweiterung migrieren'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 26b8ab29..e662f643 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -211,6 +211,236 @@ class AppLocalizationsEn extends AppLocalizations { @override String get migrate => 'Migrate'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'Migrate to another source'; diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index 20409bb6..47da1a42 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -214,6 +214,236 @@ class AppLocalizationsEs extends AppLocalizations { @override String get migrate => 'Migrar'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'Migrar a otra fuente'; diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 69a0c48b..c4a43402 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -214,6 +214,236 @@ class AppLocalizationsFr extends AppLocalizations { @override String get migrate => 'Migrer'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'Migrer vers une autre source'; diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index db747df3..a2eb9bf4 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -212,6 +212,236 @@ class AppLocalizationsHi extends AppLocalizations { @override String get migrate => 'स्थानांतरण'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'दूसरे स्रोत में माइग्रेट करें'; diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index 9694274f..36190690 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -214,6 +214,236 @@ class AppLocalizationsId extends AppLocalizations { @override String get migrate => 'Migrasi'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'Migrasi ke sumber lain'; diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 73237cf2..358bc803 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -214,6 +214,236 @@ class AppLocalizationsIt extends AppLocalizations { @override String get migrate => 'Migra'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'Migrare a un\'altra fonte'; diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index b0bb3658..d9ab8673 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -207,6 +207,236 @@ class AppLocalizationsJa extends AppLocalizations { @override String get migrate => '移行'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => '別のソースに移行'; diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index b92b0a23..d78e13f3 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -214,6 +214,236 @@ class AppLocalizationsPt extends AppLocalizations { @override String get migrate => 'Migrar'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'Migrar para outra fonte'; diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index e67ae1a3..49bdf5ab 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -213,6 +213,236 @@ class AppLocalizationsRu extends AppLocalizations { @override String get migrate => 'Перенести'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'Перенести на другой источник'; diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index 469715e7..ae6843a6 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -211,6 +211,236 @@ class AppLocalizationsTh extends AppLocalizations { @override String get migrate => 'ผนวก'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'ย้ายไปยังแหล่งอื่น'; diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index f8f76742..cebb7d05 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -211,6 +211,236 @@ class AppLocalizationsTr extends AppLocalizations { @override String get migrate => 'Taşı'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => 'Başka bir kaynağa geç'; diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 6a3c5b3e..150129ea 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -207,6 +207,236 @@ class AppLocalizationsZh extends AppLocalizations { @override String get migrate => '迁移'; + @override + String get mass_migration_title => 'Mass migration'; + + @override + String get mass_migration_preview_items => 'Preview items'; + + @override + String get mass_migration_destination_source => 'Destination source'; + + @override + String get mass_migration_no_library_items => + 'No library items are available for mass migration.'; + + @override + String get mass_migration_no_destination_sources => + 'No installed destination sources are available.'; + + @override + String get mass_migration_installed => 'Installed'; + + @override + String mass_migration_items_ready_for_review(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items ready for review', + one: '1 item ready for review', + ); + return '$_temp0'; + } + + @override + String mass_migration_item_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get mass_migration_select_destination_source => + 'Select destination source'; + + @override + String mass_migration_finding_matches(Object source, Object language) { + return 'Finding matches in $source • $language'; + } + + @override + String mass_migration_processing_item(int current, int total) { + return 'Processing item $current of $total'; + } + + @override + String get mass_migration_waiting_next_item => + 'Waiting 2 seconds before the next item...'; + + @override + String get mass_migration_waiting_next_migration => + 'Waiting 2 seconds before the next migration...'; + + @override + String mass_migration_matched_so_far(int count) { + return 'Matched so far: $count'; + } + + @override + String mass_migration_no_match_count(int count) { + return 'No match: $count'; + } + + @override + String mass_migration_review_matches(Object source) { + return 'Review matches for $source'; + } + + @override + String mass_migration_found_matches(int count) { + return 'Found matches: $count'; + } + + @override + String mass_migration_no_matches(int count) { + return 'No matches: $count'; + } + + @override + String mass_migration_selected_to_migrate(int count) { + return 'Selected to migrate: $count'; + } + + @override + String get mass_migration_finish_review => 'Finish review'; + + @override + String mass_migration_migrate_selected(int count) { + return 'Migrate selected items ($count)'; + } + + @override + String mass_migration_migrating_selected(Object source) { + return 'Migrating selected items to $source'; + } + + @override + String get mass_migration_no_items_selected => + 'No items selected for migration.'; + + @override + String mass_migration_migrating_item(int current, int total) { + return 'Migrating item $current of $total'; + } + + @override + String get mass_migration_complete => 'Mass migration complete'; + + @override + String get mass_migration_complete_success_message => + 'All selected items were processed successfully.'; + + @override + String get mass_migration_complete_partial_message => + 'Migration finished with a few items that still need manual attention.'; + + @override + String mass_migration_route_summary(Object source, Object destination) { + return '$source → $destination'; + } + + @override + String get mass_migration_processed => 'Processed'; + + @override + String get mass_migration_matched => 'Matched'; + + @override + String get mass_migration_migrated => 'Migrated'; + + @override + String get mass_migration_skipped => 'Skipped'; + + @override + String get mass_migration_failed => 'Failed'; + + @override + String get mass_migration_failed_items => 'Failed Items'; + + @override + String get mass_migration_exit => 'Exit Mass Migration'; + + @override + String get mass_migration_no_destination_match => + 'No destination match found'; + + @override + String mass_migration_query(Object query) { + return 'Query: $query'; + } + + @override + String get mass_migration_skip => 'Skip'; + + @override + String get mass_migration_loading => 'Loading...'; + + @override + String get mass_migration_choose_another_result => 'Choose another result'; + + @override + String get mass_migration_source_chapters => 'Source chapters'; + + @override + String get mass_migration_destination_chapters => 'Destination chapters'; + + @override + String mass_migration_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapters', + one: '1 chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_source_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count source chapters', + one: '1 source chapter', + ); + return '$_temp0'; + } + + @override + String mass_migration_destination_chapter_count(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count destination chapters', + one: '1 destination chapter', + ); + return '$_temp0'; + } + + @override + String get mass_migration_no_chapters_found => 'No chapters found.'; + + @override + String mass_migration_and_more_chapters(int count) { + return 'And $count more...'; + } + + @override + String get mass_migration_unknown_title => 'Unknown title'; + + @override + String get mass_migration_unknown_match => 'Unknown match'; + + @override + String get mass_migration_unknown_source => 'Unknown source'; + + @override + String get mass_migration_unknown_chapter => 'Unknown chapter'; + @override String get migrate_confirm => '迁移到另一个来源'; diff --git a/lib/modules/manga/detail/manga_detail_view.dart b/lib/modules/manga/detail/manga_detail_view.dart index 1cf97e5a..9776e391 100644 --- a/lib/modules/manga/detail/manga_detail_view.dart +++ b/lib/modules/manga/detail/manga_detail_view.dart @@ -631,6 +631,10 @@ class _MangaDetailViewState extends ConsumerState value: 3, child: Text(l10n.migrate), ), + PopupMenuItem( + value: 6, + child: const Text('Mass migration'), + ), if (!isLocalArchive) PopupMenuItem( value: 4, @@ -742,6 +746,12 @@ class _MangaDetailViewState extends ConsumerState botToast("Failed to export metadata: $e"); } break; + case 6: + context.push( + "/massMigration", + extra: widget.manga, + ); + break; } }, ), diff --git a/lib/modules/manga/detail/widgets/migrate_screen.dart b/lib/modules/manga/detail/widgets/migrate_screen.dart index 82daa147..947e7813 100644 --- a/lib/modules/manga/detail/widgets/migrate_screen.dart +++ b/lib/modules/manga/detail/widgets/migrate_screen.dart @@ -7,19 +7,13 @@ import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/eval/model/m_pages.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/category.dart'; -import 'package:mangayomi/models/changed.dart'; -import 'package:mangayomi/models/chapter.dart'; -import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/track_search.dart'; -import 'package:mangayomi/models/update.dart'; import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart'; -import 'package:mangayomi/modules/manga/detail/providers/isar_providers.dart'; +import 'package:mangayomi/modules/mass_migration/services/mass_migration_service.dart'; import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart'; -import 'package:mangayomi/modules/manga/detail/providers/update_manga_detail_providers.dart'; import 'package:mangayomi/modules/manga/detail/widgets/chapter_filter_list_tile_widget.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; -import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/services/get_detail.dart'; @@ -629,103 +623,13 @@ class _MigrationMangaGlobalImageCardState } Future _migrateManga(MManga preview) async { - String? historyChapter; - String? historyDate; - List chaptersProgress = []; - isar.writeTxnSync(() { - final histories = isar.historys - .filter() - .mangaIdEqualTo(widget.oldManga.id) - .sortByDate() - .findAllSync(); - historyChapter = _extractChapterNumber( - histories.lastOrNull?.chapter.value?.name ?? "", - ); - historyDate = histories.lastOrNull?.date; - for (var history in histories) { - isar.historys.deleteSync(history.id!); - ref - .read(synchingProvider(syncId: 1).notifier) - .addChangedPart(ActionType.removeHistory, history.id, "{}", false); - } - for (var chapter in widget.oldManga.chapters) { - chaptersProgress.add(chapter); - isar.updates - .filter() - .mangaIdEqualTo(chapter.mangaId) - .chapterNameEqualTo(chapter.name) - .deleteAllSync(); - isar.chapters.deleteSync(chapter.id!); - ref - .read(synchingProvider(syncId: 1).notifier) - .addChangedPart(ActionType.removeChapter, chapter.id, "{}", false); - } - widget.oldManga.name = widget.manga.name; - widget.oldManga.link = widget.manga.link; - widget.oldManga.imageUrl = widget.manga.imageUrl; - widget.oldManga.lang = widget.source.lang; - widget.oldManga.source = widget.source.name; - widget.oldManga.sourceId = widget.source.id; - widget.oldManga.artist = preview.artist; - widget.oldManga.author = preview.author; - widget.oldManga.status = preview.status ?? widget.oldManga.status; - widget.oldManga.description = preview.description; - widget.oldManga.genre = preview.genre; - widget.oldManga.updatedAt = DateTime.now().millisecondsSinceEpoch; - isar.mangas.putSync(widget.oldManga); - }); - await ref.read( - updateMangaDetailProvider( - mangaId: widget.oldManga.id, - isInit: false, - ).future, + await migrateLibraryItem( + ref: ref, + oldManga: widget.oldManga, + selectedManga: widget.manga, + preview: preview, + destinationSource: widget.source, ); - isar.writeTxnSync(() { - for (var oldChapter in chaptersProgress) { - final chapter = isar.chapters - .filter() - .mangaIdEqualTo(widget.oldManga.id) - .nameContains( - _extractChapterNumber(oldChapter.name ?? "") ?? ".....", - caseSensitive: false, - ) - .findFirstSync(); - if (chapter != null) { - chapter.isBookmarked = oldChapter.isBookmarked; - chapter.lastPageRead = oldChapter.lastPageRead; - chapter.isRead = oldChapter.isRead; - isar.chapters.putSync(chapter); - } - } - final chapter = isar.chapters - .filter() - .mangaIdEqualTo(widget.oldManga.id) - .nameContains(historyChapter ?? ".....", caseSensitive: false) - .findFirstSync(); - if (chapter != null) { - isar.historys.putSync( - History( - mangaId: widget.oldManga.id, - date: - historyDate ?? DateTime.now().millisecondsSinceEpoch.toString(), - itemType: widget.oldManga.itemType, - chapterId: chapter.id, - )..chapter.value = chapter, - ); - } - }); - ref.invalidate(getMangaDetailStreamProvider(mangaId: widget.oldManga.id!)); - } - - String? _extractChapterNumber(String chapterName) { - return RegExp( - r'\s*(\d+\.\d+)\s*', - multiLine: true, - ).firstMatch(chapterName)?.group(0) ?? - RegExp( - r'\s*(\d+)\s*', - multiLine: true, - ).firstMatch(chapterName)?.group(0); } } diff --git a/lib/modules/mass_migration/mass_migration_destination_screen.dart b/lib/modules/mass_migration/mass_migration_destination_screen.dart new file mode 100644 index 00000000..3330693f --- /dev/null +++ b/lib/modules/mass_migration/mass_migration_destination_screen.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:mangayomi/models/source.dart'; +import 'package:mangayomi/modules/mass_migration/mass_migration_runner_screen.dart'; +import 'package:mangayomi/modules/mass_migration/models/mass_migration_models.dart'; +import 'package:mangayomi/modules/mass_migration/widgets/mass_migration_widgets.dart'; +import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/router/router.dart'; +import 'package:mangayomi/utils/language.dart'; + +class MassMigrationDestinationScreen extends StatelessWidget { + const MassMigrationDestinationScreen({required this.sourceGroup, super.key}); + + final MassMigrationSourceGroup sourceGroup; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final sources = buildMassMigrationDestinationSources( + sourceGroup: sourceGroup, + ); + + return Scaffold( + appBar: AppBar(title: Text(l10n.mass_migration_destination_source)), + body: sources.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text(l10n.mass_migration_no_destination_sources), + ), + ) + : ListView.separated( + itemCount: sources.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final source = sources[index]; + return _DestinationSourceTile( + source: source, + onTap: () { + Navigator.push( + context, + createRoute( + page: MassMigrationRunnerScreen( + sourceGroup: sourceGroup, + destinationSource: source, + ), + ), + ); + }, + ); + }, + ), + ); + } +} + +class _DestinationSourceTile extends StatelessWidget { + const _DestinationSourceTile({required this.source, required this.onTap}); + + final Source source; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return ListTile( + leading: MassMigrationSourceIcon(source: source), + title: Text(source.name ?? l10n.mass_migration_unknown_source), + subtitle: Text( + [ + if ((source.lang ?? '').isNotEmpty) + completeLanguageName(source.lang!), + l10n.mass_migration_installed, + ].join(' • '), + ), + trailing: const Icon(Icons.chevron_right), + onTap: onTap, + ); + } +} diff --git a/lib/modules/mass_migration/mass_migration_preview_screen.dart b/lib/modules/mass_migration/mass_migration_preview_screen.dart new file mode 100644 index 00000000..2d8d7741 --- /dev/null +++ b/lib/modules/mass_migration/mass_migration_preview_screen.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:mangayomi/modules/mass_migration/mass_migration_destination_screen.dart'; +import 'package:mangayomi/modules/mass_migration/models/mass_migration_models.dart'; +import 'package:mangayomi/modules/mass_migration/widgets/mass_migration_widgets.dart'; +import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/router/router.dart'; +import 'package:mangayomi/utils/language.dart'; + +class MassMigrationPreviewScreen extends StatelessWidget { + const MassMigrationPreviewScreen({required this.sourceGroup, super.key}); + + final MassMigrationSourceGroup sourceGroup; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Scaffold( + appBar: AppBar(title: Text(l10n.mass_migration_preview_items)), + body: Column( + children: [ + ListTile( + leading: MassMigrationSourceIcon(source: sourceGroup.source), + title: Text(sourceGroup.sourceName), + subtitle: Text( + [ + if ((sourceGroup.lang ?? '').isNotEmpty) + completeLanguageName(sourceGroup.lang!), + l10n.mass_migration_items_ready_for_review(sourceGroup.count), + ].join(' • '), + ), + ), + const Divider(height: 1), + Expanded( + child: ListView.separated( + itemCount: sourceGroup.items.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final manga = sourceGroup.items[index]; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${index + 1}'), + const SizedBox(width: 12), + MassMigrationCover( + libraryItem: manga, + source: sourceGroup.source, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + manga.name ?? + l10n.mass_migration_unknown_title, + style: Theme.of( + context, + ).textTheme.titleMedium, + ), + const SizedBox(height: 6), + Text( + [ + if ((manga.author ?? '') + .trim() + .isNotEmpty) + manga.author!, + if ((manga.artist ?? '') + .trim() + .isNotEmpty) + manga.artist!, + l10n.mass_migration_chapter_count( + manga.chapters.length, + ), + ].join(' • '), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 8), + MassMigrationChapterSection( + title: l10n.mass_migration_source_chapters, + chapters: manga.chapters + .map( + (chapter) => + chapter.name ?? + l10n.mass_migration_unknown_chapter, + ) + .toList(), + ), + ], + ), + ), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () { + Navigator.push( + context, + createRoute( + page: MassMigrationDestinationScreen( + sourceGroup: sourceGroup, + ), + ), + ); + }, + child: Text(l10n.mass_migration_select_destination_source), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/mass_migration/mass_migration_runner_screen.dart b/lib/modules/mass_migration/mass_migration_runner_screen.dart new file mode 100644 index 00000000..66a24aa1 --- /dev/null +++ b/lib/modules/mass_migration/mass_migration_runner_screen.dart @@ -0,0 +1,783 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mangayomi/eval/model/m_manga.dart'; +import 'package:mangayomi/models/source.dart'; +import 'package:mangayomi/modules/mass_migration/models/mass_migration_models.dart'; +import 'package:mangayomi/modules/mass_migration/services/mass_migration_service.dart'; +import 'package:mangayomi/modules/mass_migration/widgets/mass_migration_widgets.dart'; +import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/services/get_detail.dart'; +import 'package:mangayomi/utils/language.dart'; + +enum _MassMigrationPhase { matching, review, applying, summary } + +class MassMigrationRunnerScreen extends ConsumerStatefulWidget { + const MassMigrationRunnerScreen({ + required this.sourceGroup, + required this.destinationSource, + super.key, + }); + + final MassMigrationSourceGroup sourceGroup; + final Source destinationSource; + + @override + ConsumerState createState() => + _MassMigrationRunnerScreenState(); +} + +class _MassMigrationRunnerScreenState + extends ConsumerState { + _MassMigrationPhase _phase = _MassMigrationPhase.matching; + int _currentIndex = 0; + int _migratedCount = 0; + int _skippedCount = 0; + final List _failedItems = []; + final Set _loadingCandidateIndexes = {}; + final List _resolvedItems = []; + bool _isWaitingForNextItem = false; + + @override + void initState() { + super.initState(); + unawaited(_processAllItems()); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Scaffold( + appBar: AppBar(title: Text(l10n.mass_migration_title)), + body: switch (_phase) { + _MassMigrationPhase.matching => _buildMatchingPhase(context), + _MassMigrationPhase.review => _buildReviewPhase(context), + _MassMigrationPhase.applying => _buildApplyingPhase(context), + _MassMigrationPhase.summary => _buildSummaryPhase(context), + }, + ); + } + + Widget _buildMatchingPhase(BuildContext context) { + final l10n = context.l10n; + final total = widget.sourceGroup.items.length; + final currentItem = widget.sourceGroup.items[_currentIndex]; + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + MassMigrationSourceIcon(source: widget.destinationSource), + const SizedBox(width: 12), + Expanded( + child: Text( + l10n.mass_migration_finding_matches( + widget.destinationSource.name ?? '', + completeLanguageName(widget.destinationSource.lang ?? ''), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + LinearProgressIndicator(value: (_currentIndex + 1) / total), + const SizedBox(height: 8), + Text(l10n.mass_migration_processing_item(_currentIndex + 1, total)), + const SizedBox(height: 16), + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MassMigrationCover( + libraryItem: currentItem, + source: widget.sourceGroup.source, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentItem.name ?? l10n.mass_migration_unknown_title, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 6), + Text( + [ + widget.sourceGroup.sourceName, + l10n.mass_migration_chapter_count( + currentItem.chapters.length, + ), + ].join(' • '), + ), + ], + ), + ), + ], + ), + ), + ), + if (_isWaitingForNextItem) ...[ + const SizedBox(height: 12), + Text(l10n.mass_migration_waiting_next_item), + ], + if (_resolvedItems.isNotEmpty) ...[ + const SizedBox(height: 16), + Text( + l10n.mass_migration_matched_so_far( + _resolvedItems.where((item) => item.hasMatch).length, + ), + ), + Text( + l10n.mass_migration_no_match_count( + _resolvedItems.where((item) => !item.hasMatch).length, + ), + ), + ], + ], + ), + ); + } + + Widget _buildReviewPhase(BuildContext context) { + final l10n = context.l10n; + final matchedCount = _resolvedItems.where((item) => item.hasMatch).length; + final selectedCount = _resolvedItems + .where((item) => item.shouldMigrate) + .length; + final noMatchCount = _resolvedItems.where((item) => !item.hasMatch).length; + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + MassMigrationSourceIcon(source: widget.destinationSource), + const SizedBox(width: 12), + Expanded( + child: Text( + l10n.mass_migration_review_matches( + widget.destinationSource.name ?? '', + ), + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ], + ), + const SizedBox(height: 12), + Text(l10n.mass_migration_found_matches(matchedCount)), + Text(l10n.mass_migration_no_matches(noMatchCount)), + Text(l10n.mass_migration_selected_to_migrate(selectedCount)), + ], + ), + ), + ), + ), + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 16), + itemCount: _resolvedItems.length, + itemBuilder: (context, index) { + final item = _resolvedItems[index]; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6, + ), + child: _buildResolvedItemCard(context, item, index), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: _startMigration, + child: Text( + selectedCount == 0 + ? l10n.mass_migration_finish_review + : l10n.mass_migration_migrate_selected(selectedCount), + ), + ), + ), + ), + ], + ); + } + + Widget _buildApplyingPhase(BuildContext context) { + final l10n = context.l10n; + final selectedItems = _resolvedItems + .where((item) => item.shouldMigrate) + .toList(); + final total = selectedItems.isEmpty ? 1 : selectedItems.length; + final currentItem = selectedItems.isEmpty + ? null + : selectedItems[_currentIndex.clamp(0, selectedItems.length - 1)]; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.mass_migration_migrating_selected( + widget.destinationSource.name ?? '', + ), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + LinearProgressIndicator( + value: selectedItems.isEmpty ? 1 : (_currentIndex + 1) / total, + ), + const SizedBox(height: 8), + Text( + selectedItems.isEmpty + ? l10n.mass_migration_no_items_selected + : l10n.mass_migration_migrating_item(_currentIndex + 1, total), + ), + const SizedBox(height: 16), + if (currentItem != null) + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MassMigrationCover( + libraryItem: currentItem.sourceItem, + source: widget.sourceGroup.source, + ), + const SizedBox(width: 12), + const Icon(Icons.arrow_forward_rounded), + const SizedBox(width: 12), + MassMigrationCover( + remoteItem: currentItem.selectedCandidate, + source: widget.destinationSource, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentItem.sourceItem.name ?? + l10n.mass_migration_unknown_title, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 6), + Text( + currentItem.selectedCandidate?.name ?? + l10n.mass_migration_unknown_match, + ), + ], + ), + ), + ], + ), + ), + ), + if (_isWaitingForNextItem) ...[ + const SizedBox(height: 12), + Text(l10n.mass_migration_waiting_next_migration), + ], + ], + ), + ); + } + + Widget _buildSummaryPhase(BuildContext context) { + final l10n = context.l10n; + final total = widget.sourceGroup.items.length; + final matchedCount = _resolvedItems.where((item) => item.hasMatch).length; + final successTone = _failedItems.isEmpty ? Colors.green : Colors.orange; + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).cardColor, + boxShadow: [ + BoxShadow( + color: Theme.of(context).shadowColor.withValues(alpha: 0.08), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: successTone.withValues(alpha: 0.14), + borderRadius: BorderRadius.circular(16), + ), + child: Icon( + _failedItems.isEmpty + ? Icons.task_alt_rounded + : Icons.done_all_rounded, + color: successTone, + size: 30, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.mass_migration_complete, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + _failedItems.isEmpty + ? l10n.mass_migration_complete_success_message + : l10n.mass_migration_complete_partial_message, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 12), + Text( + l10n.mass_migration_route_summary( + widget.sourceGroup.sourceName, + widget.destinationSource.name ?? '', + ), + style: Theme.of(context).textTheme.labelLarge, + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + _SummaryStatCard( + label: l10n.mass_migration_processed, + value: '$total', + ), + _SummaryStatCard( + label: l10n.mass_migration_matched, + value: '$matchedCount', + ), + _SummaryStatCard( + label: l10n.mass_migration_migrated, + value: '$_migratedCount', + ), + _SummaryStatCard( + label: l10n.mass_migration_skipped, + value: '$_skippedCount', + ), + _SummaryStatCard( + label: l10n.mass_migration_failed, + value: '${_failedItems.length}', + ), + ], + ), + if (_failedItems.isNotEmpty) ...[ + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(18), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.mass_migration_failed_items, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 12), + for (final item in _failedItems) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + const Icon( + Icons.error_outline_rounded, + size: 18, + color: Colors.orange, + ), + const SizedBox(width: 8), + Expanded(child: Text(item)), + ], + ), + ), + ], + ), + ), + ], + const SizedBox(height: 24), + FilledButton.icon( + onPressed: _exitMassMigrationFlow, + icon: const Icon(Icons.exit_to_app_rounded), + label: Text(l10n.mass_migration_exit), + ), + ], + ), + ); + } + + Widget _buildResolvedItemCard( + BuildContext context, + MassMigrationResolvedItem item, + int index, + ) { + final l10n = context.l10n; + final destinationChapterNames = + item.destinationPreview?.chapters + ?.map( + (chapter) => chapter.name ?? l10n.mass_migration_unknown_chapter, + ) + .toList() ?? + const []; + final sourceChapterNames = item.sourceItem.chapters + .map((chapter) => chapter.name ?? l10n.mass_migration_unknown_chapter) + .toList(); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MassMigrationCover( + libraryItem: item.sourceItem, + source: widget.sourceGroup.source, + ), + const SizedBox(width: 12), + const Icon(Icons.arrow_forward_rounded), + const SizedBox(width: 12), + MassMigrationCover( + remoteItem: item.selectedCandidate, + source: widget.destinationSource, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.sourceItem.name ?? + l10n.mass_migration_unknown_title, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 6), + Text( + item.hasMatch + ? (item.selectedCandidate?.name ?? + l10n.mass_migration_unknown_match) + : l10n.mass_migration_no_destination_match, + ), + const SizedBox(height: 6), + Text( + [ + l10n.mass_migration_source_chapter_count( + item.sourceItem.chapters.length, + ), + l10n.mass_migration_destination_chapter_count( + destinationChapterNames.length, + ), + if ((item.searchResult.usedQuery ?? '').isNotEmpty) + l10n.mass_migration_query( + item.searchResult.usedQuery!, + ), + ].join(' • '), + ), + ], + ), + ), + ], + ), + if ((item.errorMessage ?? '').isNotEmpty) ...[ + const SizedBox(height: 8), + Text(item.errorMessage!), + ], + const SizedBox(height: 12), + Row( + children: [ + ChoiceChip( + label: Text(l10n.mass_migration_skip), + selected: !item.shouldMigrate, + onSelected: (_) => _updateShouldMigrate(index, false), + ), + const SizedBox(width: 8), + ChoiceChip( + label: Text(l10n.migrate), + selected: item.shouldMigrate, + onSelected: item.canMigrate + ? (_) => _updateShouldMigrate(index, true) + : null, + ), + const Spacer(), + if (item.searchResult.candidates.length > 1) + TextButton( + onPressed: _loadingCandidateIndexes.contains(index) + ? null + : () => _pickAnotherCandidate(index), + child: Text( + _loadingCandidateIndexes.contains(index) + ? l10n.mass_migration_loading + : l10n.mass_migration_choose_another_result, + ), + ), + ], + ), + const SizedBox(height: 8), + MassMigrationChapterSection( + title: l10n.mass_migration_source_chapters, + chapters: sourceChapterNames, + ), + MassMigrationChapterSection( + title: l10n.mass_migration_destination_chapters, + chapters: destinationChapterNames, + ), + ], + ), + ), + ); + } + + Future _processAllItems() async { + for (var i = 0; i < widget.sourceGroup.items.length; i++) { + if (!mounted) return; + setState(() { + _currentIndex = i; + }); + final resolvedItem = await resolveMassMigrationItem( + ref: ref, + manga: widget.sourceGroup.items[i], + destinationSource: widget.destinationSource, + ); + if (!mounted) return; + setState(() { + _resolvedItems.add(resolvedItem); + }); + if (i < widget.sourceGroup.items.length - 1) { + setState(() { + _isWaitingForNextItem = true; + }); + await Future.delayed(const Duration(seconds: 2)); + if (!mounted) return; + setState(() { + _isWaitingForNextItem = false; + }); + } + } + + if (!mounted) return; + setState(() { + _phase = _MassMigrationPhase.review; + _currentIndex = 0; + }); + } + + void _updateShouldMigrate(int index, bool shouldMigrate) { + setState(() { + _resolvedItems[index] = _resolvedItems[index].copyWith( + shouldMigrate: shouldMigrate && _resolvedItems[index].canMigrate, + ); + }); + } + + Future _pickAnotherCandidate(int index) async { + final item = _resolvedItems[index]; + final selected = await showModalBottomSheet( + context: context, + builder: (context) { + return SafeArea( + child: ListView.separated( + itemCount: item.searchResult.candidates.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, candidateIndex) { + final l10n = context.l10n; + final candidate = item.searchResult.candidates[candidateIndex]; + return ListTile( + title: Text( + candidate.name ?? l10n.mass_migration_unknown_title, + ), + subtitle: Text( + [ + if ((candidate.author ?? '').trim().isNotEmpty) + candidate.author!, + if ((candidate.artist ?? '').trim().isNotEmpty) + candidate.artist!, + ].join(' • '), + ), + onTap: () => Navigator.pop(context, candidate), + ); + }, + ), + ); + }, + ); + + if (selected == null || !mounted) return; + + setState(() { + _loadingCandidateIndexes.add(index); + }); + + try { + final preview = await ref.read( + getDetailProvider( + url: selected.link!, + source: widget.destinationSource, + ).future, + ); + if (!mounted) return; + setState(() { + _resolvedItems[index] = item.copyWith( + selectedCandidate: selected, + destinationPreview: preview, + errorMessage: null, + shouldMigrate: true, + keepErrorMessage: false, + ); + }); + } catch (error) { + if (!mounted) return; + setState(() { + _resolvedItems[index] = item.copyWith( + selectedCandidate: selected, + destinationPreview: null, + errorMessage: error.toString(), + shouldMigrate: false, + keepDestinationPreview: false, + keepErrorMessage: false, + ); + }); + } finally { + if (mounted) { + setState(() { + _loadingCandidateIndexes.remove(index); + }); + } + } + } + + Future _startMigration() async { + final selectedItems = _resolvedItems + .where((item) => item.shouldMigrate) + .toList(); + if (selectedItems.isEmpty) { + setState(() { + _skippedCount = widget.sourceGroup.items.length; + _phase = _MassMigrationPhase.summary; + }); + return; + } + + setState(() { + _phase = _MassMigrationPhase.applying; + _migratedCount = 0; + _skippedCount = widget.sourceGroup.items.length - selectedItems.length; + _failedItems.clear(); + _currentIndex = 0; + }); + + for (var i = 0; i < selectedItems.length; i++) { + final item = selectedItems[i]; + if (!mounted) return; + setState(() { + _currentIndex = i; + }); + + try { + await migrateLibraryItem( + ref: ref, + oldManga: item.sourceItem, + selectedManga: item.selectedCandidate!, + preview: item.destinationPreview!, + destinationSource: widget.destinationSource, + ); + if (!mounted) return; + setState(() { + _migratedCount += 1; + }); + } catch (error) { + if (!mounted) return; + setState(() { + _failedItems.add( + item.sourceItem.name ?? context.l10n.mass_migration_unknown_title, + ); + }); + } + } + + if (!mounted) return; + setState(() { + _phase = _MassMigrationPhase.summary; + }); + } + + void _exitMassMigrationFlow() { + final navigator = Navigator.of(context); + var pops = 0; + while (navigator.canPop() && pops < 4) { + navigator.pop(); + pops++; + } + } +} + +class _SummaryStatCard extends StatelessWidget { + const _SummaryStatCard({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 156, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(18), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(value, style: Theme.of(context).textTheme.headlineSmall), + const SizedBox(height: 6), + Text(label, style: Theme.of(context).textTheme.bodyMedium), + ], + ), + ), + ); + } +} diff --git a/lib/modules/mass_migration/mass_migration_source_selection_screen.dart b/lib/modules/mass_migration/mass_migration_source_selection_screen.dart new file mode 100644 index 00000000..acf43cb9 --- /dev/null +++ b/lib/modules/mass_migration/mass_migration_source_selection_screen.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/modules/mass_migration/mass_migration_preview_screen.dart'; +import 'package:mangayomi/modules/mass_migration/models/mass_migration_models.dart'; +import 'package:mangayomi/modules/mass_migration/widgets/mass_migration_widgets.dart'; +import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/router/router.dart'; +import 'package:mangayomi/utils/language.dart'; + +class MassMigrationSourceSelectionScreen extends StatelessWidget { + const MassMigrationSourceSelectionScreen({ + required this.initialManga, + super.key, + }); + + final Manga initialManga; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final sourceGroups = buildMassMigrationSourceGroups( + itemType: initialManga.itemType, + prioritizedManga: initialManga, + ); + + return Scaffold( + appBar: AppBar(title: Text(l10n.mass_migration_title)), + body: sourceGroups.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text(l10n.mass_migration_no_library_items), + ), + ) + : ListView.separated( + itemCount: sourceGroups.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final sourceGroup = sourceGroups[index]; + return ListTile( + leading: MassMigrationSourceIcon(source: sourceGroup.source), + title: Text(sourceGroup.sourceName), + subtitle: Text( + [ + if ((sourceGroup.lang ?? '').isNotEmpty) + completeLanguageName(sourceGroup.lang!), + l10n.mass_migration_item_count(sourceGroup.count), + ].join(' • '), + ), + trailing: const Icon(Icons.chevron_right), + onTap: () { + Navigator.push( + context, + createRoute( + page: MassMigrationPreviewScreen( + sourceGroup: sourceGroup, + ), + ), + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/modules/mass_migration/models/mass_migration_models.dart b/lib/modules/mass_migration/models/mass_migration_models.dart new file mode 100644 index 00000000..f31ed6fc --- /dev/null +++ b/lib/modules/mass_migration/models/mass_migration_models.dart @@ -0,0 +1,177 @@ +import 'package:isar_community/isar.dart'; +import 'package:mangayomi/eval/model/m_manga.dart'; +import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/models/source.dart'; + +class MassMigrationSourceGroup { + const MassMigrationSourceGroup({ + required this.sourceName, + required this.itemType, + required this.items, + this.source, + this.lang, + this.sourceId, + }); + + final String sourceName; + final Source? source; + final String? lang; + final int? sourceId; + final ItemType itemType; + final List items; + + int get count => items.length; +} + +class MassMigrationSearchResult { + const MassMigrationSearchResult({ + required this.queries, + required this.candidates, + this.selected, + this.usedQuery, + }); + + final List queries; + final List candidates; + final MManga? selected; + final String? usedQuery; + + bool get hasMatch => selected != null; +} + +class MassMigrationResolvedItem { + const MassMigrationResolvedItem({ + required this.sourceItem, + required this.searchResult, + this.selectedCandidate, + this.destinationPreview, + this.errorMessage, + this.shouldMigrate = false, + }); + + final Manga sourceItem; + final MassMigrationSearchResult searchResult; + final MManga? selectedCandidate; + final MManga? destinationPreview; + final String? errorMessage; + final bool shouldMigrate; + + bool get hasMatch => selectedCandidate != null; + bool get canMigrate => + selectedCandidate != null && + destinationPreview != null && + (errorMessage == null || errorMessage!.isEmpty); + + MassMigrationResolvedItem copyWith({ + MManga? selectedCandidate, + MManga? destinationPreview, + String? errorMessage, + bool? shouldMigrate, + bool keepSelectedCandidate = true, + bool keepDestinationPreview = true, + bool keepErrorMessage = true, + }) { + return MassMigrationResolvedItem( + sourceItem: sourceItem, + searchResult: searchResult, + selectedCandidate: keepSelectedCandidate + ? selectedCandidate ?? this.selectedCandidate + : selectedCandidate, + destinationPreview: keepDestinationPreview + ? destinationPreview ?? this.destinationPreview + : destinationPreview, + errorMessage: keepErrorMessage + ? errorMessage ?? this.errorMessage + : errorMessage, + shouldMigrate: shouldMigrate ?? this.shouldMigrate, + ); + } +} + +List buildMassMigrationSourceGroups({ + required ItemType itemType, + Manga? prioritizedManga, +}) { + final libraryItems = isar.mangas + .filter() + .favoriteEqualTo(true) + .itemTypeEqualTo(itemType) + .findAllSync(); + + final grouped = >{}; + for (final manga in libraryItems) { + final sourceName = (manga.source ?? '').trim(); + if (sourceName.isEmpty) continue; + grouped.putIfAbsent(sourceName, () => []).add(manga); + } + + final groups = grouped.entries.map((entry) { + final items = [...entry.value] + ..sort( + (left, right) => (left.name ?? '').toLowerCase().compareTo( + (right.name ?? '').toLowerCase(), + ), + ); + final first = items.first; + final source = first.sourceId != null + ? isar.sources.getSync(first.sourceId!) + : isar.sources + .filter() + .nameEqualTo(first.source) + .langEqualTo(first.lang) + .findFirstSync(); + return MassMigrationSourceGroup( + sourceName: entry.key, + source: source, + lang: first.lang, + sourceId: first.sourceId, + itemType: itemType, + items: items, + ); + }).toList(); + + groups.sort((left, right) { + final prioritizedSource = prioritizedManga?.source?.trim().toLowerCase(); + final leftPriority = left.sourceName.toLowerCase() == prioritizedSource; + final rightPriority = right.sourceName.toLowerCase() == prioritizedSource; + if (leftPriority != rightPriority) { + return leftPriority ? -1 : 1; + } + final nameCompare = left.sourceName.toLowerCase().compareTo( + right.sourceName.toLowerCase(), + ); + if (nameCompare != 0) return nameCompare; + return right.count.compareTo(left.count); + }); + + return groups; +} + +List buildMassMigrationDestinationSources({ + required MassMigrationSourceGroup sourceGroup, +}) { + final sources = isar.sources + .filter() + .isAddedEqualTo(true) + .itemTypeEqualTo(sourceGroup.itemType) + .findAllSync() + .where( + (source) => + source.sourceCode != null && + !(source.name == sourceGroup.sourceName && + source.lang == sourceGroup.lang), + ) + .toList(); + + sources.sort((left, right) { + final nameCompare = (left.name ?? '').toLowerCase().compareTo( + (right.name ?? '').toLowerCase(), + ); + if (nameCompare != 0) return nameCompare; + return (left.lang ?? '').toLowerCase().compareTo( + (right.lang ?? '').toLowerCase(), + ); + }); + return sources; +} diff --git a/lib/modules/mass_migration/services/mass_migration_service.dart b/lib/modules/mass_migration/services/mass_migration_service.dart new file mode 100644 index 00000000..b3eaf134 --- /dev/null +++ b/lib/modules/mass_migration/services/mass_migration_service.dart @@ -0,0 +1,488 @@ +import 'dart:collection'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar_community/isar.dart'; +import 'package:mangayomi/eval/model/m_manga.dart'; +import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; +import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/history.dart'; +import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/models/source.dart'; +import 'package:mangayomi/models/track.dart'; +import 'package:mangayomi/models/update.dart'; +import 'package:mangayomi/modules/mass_migration/models/mass_migration_models.dart'; +import 'package:mangayomi/modules/manga/detail/providers/isar_providers.dart'; +import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; +import 'package:mangayomi/services/get_detail.dart'; +import 'package:mangayomi/services/search_.dart'; +import 'package:mangayomi/utils/extensions/string_extensions.dart'; + +Future migrateLibraryItem({ + required WidgetRef ref, + required Manga oldManga, + required MManga selectedManga, + required MManga preview, + required Source destinationSource, +}) async { + final migrationSnapshot = _captureMigrationSnapshot( + ref: ref, + oldManga: oldManga, + ); + _rewriteMigratedItemMetadata( + oldManga: oldManga, + selectedManga: selectedManga, + preview: preview, + destinationSource: destinationSource, + ); + _syncMigratedMangaFromPreview( + oldManga: oldManga, + preview: preview, + destinationSource: destinationSource, + ); + _restoreMigrationProgress(oldManga: oldManga, snapshot: migrationSnapshot); + ref.invalidate(getMangaDetailStreamProvider(mangaId: oldManga.id!)); +} + +class _MigrationSnapshot { + const _MigrationSnapshot({ + required this.chaptersProgress, + this.historyChapter, + this.historyDate, + }); + + final List chaptersProgress; + final String? historyChapter; + final String? historyDate; +} + +_MigrationSnapshot _captureMigrationSnapshot({ + required WidgetRef ref, + required Manga oldManga, +}) { + String? historyChapter; + String? historyDate; + final chaptersProgress = []; + + isar.writeTxnSync(() { + final histories = isar.historys + .filter() + .mangaIdEqualTo(oldManga.id) + .sortByDate() + .findAllSync(); + historyChapter = extractMigrationChapterNumber( + histories.lastOrNull?.chapter.value?.name ?? '', + ); + historyDate = histories.lastOrNull?.date; + for (final history in histories) { + isar.historys.deleteSync(history.id!); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.removeHistory, history.id, '{}', false); + } + for (final chapter in oldManga.chapters) { + chaptersProgress.add(chapter); + isar.updates + .filter() + .mangaIdEqualTo(chapter.mangaId) + .chapterNameEqualTo(chapter.name) + .deleteAllSync(); + isar.chapters.deleteSync(chapter.id!); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.removeChapter, chapter.id, '{}', false); + } + }); + + return _MigrationSnapshot( + chaptersProgress: chaptersProgress, + historyChapter: historyChapter, + historyDate: historyDate, + ); +} + +void _rewriteMigratedItemMetadata({ + required Manga oldManga, + required MManga selectedManga, + required MManga preview, + required Source destinationSource, +}) { + isar.writeTxnSync(() { + oldManga.name = selectedManga.name; + oldManga.link = selectedManga.link; + oldManga.imageUrl = selectedManga.imageUrl; + oldManga.lang = destinationSource.lang; + oldManga.source = destinationSource.name; + oldManga.sourceId = destinationSource.id; + oldManga.artist = preview.artist; + oldManga.author = preview.author; + oldManga.status = preview.status ?? oldManga.status; + oldManga.description = preview.description; + oldManga.genre = preview.genre; + oldManga.updatedAt = DateTime.now().millisecondsSinceEpoch; + isar.mangas.putSync(oldManga); + }); +} + +void _syncMigratedMangaFromPreview({ + required Manga oldManga, + required MManga preview, + required Source destinationSource, +}) { + final genre = + preview.genre + ?.map((entry) => entry.toString().trim()) + .where((entry) => entry.isNotEmpty) + .toSet() + .toList() ?? + []; + + final previewImageUrl = _trimmedOrDefault( + preview.imageUrl, + oldManga.imageUrl, + ); + oldManga + ..imageUrl = previewImageUrl == null + ? null + : previewImageUrl.startsWith('http') + ? previewImageUrl + : '${destinationSource.baseUrl ?? ''}/${previewImageUrl.getUrlWithoutDomain}' + ..name = _trimmedOrDefault(preview.name, oldManga.name) + ..genre = genre.isEmpty ? oldManga.genre ?? [] : genre + ..author = _trimmedOrDefault(preview.author, oldManga.author) ?? '' + ..artist = _trimmedOrDefault(preview.artist, oldManga.artist) ?? '' + ..status = preview.status == Status.unknown + ? oldManga.status + : preview.status ?? Status.unknown + ..description = + _trimmedOrDefault(preview.description, oldManga.description) ?? '' + ..link = _trimmedOrDefault(preview.link, oldManga.link) + ..source = destinationSource.name + ..lang = destinationSource.lang + ..itemType = destinationSource.itemType + ..lastUpdate = DateTime.now().millisecondsSinceEpoch + ..updatedAt = DateTime.now().millisecondsSinceEpoch; + + isar.writeTxnSync(() { + final mangaId = isar.mangas.putSync(oldManga); + final previewChapters = preview.chapters ?? const []; + final chapters = previewChapters + .map( + (previewChapter) => Chapter( + name: previewChapter.name ?? '', + url: previewChapter.url?.trim() ?? '', + dateUpload: previewChapter.dateUpload == null + ? DateTime.now().millisecondsSinceEpoch.toString() + : previewChapter.dateUpload.toString(), + scanlator: previewChapter.scanlator ?? '', + mangaId: mangaId, + updatedAt: DateTime.now().millisecondsSinceEpoch, + isFiller: previewChapter.isFiller, + thumbnailUrl: previewChapter.thumbnailUrl, + description: previewChapter.description, + downloadSize: previewChapter.downloadSize, + duration: previewChapter.duration, + )..manga.value = oldManga, + ) + .toList(); + for (final chapter in chapters.reversed) { + isar.chapters.putSync(chapter); + chapter.manga.saveSync(); + } + }); +} + +void _restoreMigrationProgress({ + required Manga oldManga, + required _MigrationSnapshot snapshot, +}) { + isar.writeTxnSync(() { + for (final oldChapter in snapshot.chaptersProgress) { + final chapter = isar.chapters + .filter() + .mangaIdEqualTo(oldManga.id) + .nameContains( + extractMigrationChapterNumber(oldChapter.name ?? '') ?? '.....', + caseSensitive: false, + ) + .findFirstSync(); + if (chapter != null) { + chapter.isBookmarked = oldChapter.isBookmarked; + chapter.lastPageRead = oldChapter.lastPageRead; + chapter.isRead = oldChapter.isRead; + isar.chapters.putSync(chapter); + } + } + + final historyChapter = isar.chapters + .filter() + .mangaIdEqualTo(oldManga.id) + .nameContains(snapshot.historyChapter ?? '.....', caseSensitive: false) + .findFirstSync(); + if (historyChapter != null) { + isar.historys.putSync( + History( + mangaId: oldManga.id, + date: + snapshot.historyDate ?? + DateTime.now().millisecondsSinceEpoch.toString(), + itemType: oldManga.itemType, + chapterId: historyChapter.id, + )..chapter.value = historyChapter, + ); + } + }); +} + +Future findBestMassMigrationMatch({ + required WidgetRef ref, + required Manga manga, + required Source destinationSource, +}) async { + final queries = buildMassMigrationQueries(manga); + + for (final query in queries) { + final pages = await ref.read( + searchProvider( + source: destinationSource, + page: 1, + query: query, + filterList: const [], + ).future, + ); + final candidates = pages?.list ?? const []; + if (candidates.isEmpty) continue; + return MassMigrationSearchResult( + queries: queries, + usedQuery: query, + candidates: candidates, + selected: _selectBestCandidate( + manga: manga, + queries: queries, + candidates: candidates, + ), + ); + } + + return MassMigrationSearchResult(queries: queries, candidates: const []); +} + +Future resolveMassMigrationItem({ + required WidgetRef ref, + required Manga manga, + required Source destinationSource, +}) async { + try { + final searchResult = await _resolveSearchResult( + ref: ref, + manga: manga, + destinationSource: destinationSource, + ); + return await _resolveMatchedPreview( + ref: ref, + manga: manga, + destinationSource: destinationSource, + searchResult: searchResult, + ); + } catch (error) { + return _buildErroredResolvedItem( + sourceItem: manga, + errorMessage: error.toString(), + ); + } +} + +Future _resolveSearchResult({ + required WidgetRef ref, + required Manga manga, + required Source destinationSource, +}) { + return findBestMassMigrationMatch( + ref: ref, + manga: manga, + destinationSource: destinationSource, + ); +} + +Future _resolveMatchedPreview({ + required WidgetRef ref, + required Manga manga, + required Source destinationSource, + required MassMigrationSearchResult searchResult, +}) async { + final selectedCandidate = searchResult.selected; + if (selectedCandidate == null) { + return MassMigrationResolvedItem( + sourceItem: manga, + searchResult: searchResult, + ); + } + + try { + final preview = await ref.read( + getDetailProvider( + url: selectedCandidate.link!, + source: destinationSource, + ).future, + ); + return MassMigrationResolvedItem( + sourceItem: manga, + searchResult: searchResult, + selectedCandidate: selectedCandidate, + destinationPreview: preview, + shouldMigrate: true, + ); + } catch (error) { + return MassMigrationResolvedItem( + sourceItem: manga, + searchResult: searchResult, + selectedCandidate: selectedCandidate, + errorMessage: error.toString(), + ); + } +} + +MassMigrationResolvedItem _buildErroredResolvedItem({ + required Manga sourceItem, + required String errorMessage, +}) { + return MassMigrationResolvedItem( + sourceItem: sourceItem, + searchResult: MassMigrationSearchResult( + queries: buildMassMigrationQueries(sourceItem), + candidates: const [], + ), + errorMessage: errorMessage, + ); +} + +List buildMassMigrationQueries(Manga manga) { + final queries = {}; + + void addQuery(String? value) { + final cleaned = value?.trim(); + if (cleaned == null || cleaned.isEmpty) return; + queries.add(cleaned); + } + + addQuery(manga.name); + for (final track + in isar.tracks.filter().mangaIdEqualTo(manga.id).findAllSync()) { + addQuery(track.title); + } + + final name = manga.name?.trim(); + if (name != null && name.isNotEmpty) { + addQuery(name.split(RegExp(r'\s*[:\-|/]\s*')).first); + final beforeParenthesis = name.split('(').first.trim(); + if (beforeParenthesis.isNotEmpty && beforeParenthesis != name) { + addQuery(beforeParenthesis); + } + final matches = RegExp(r'\(([^)]+)\)').allMatches(name); + for (final match in matches) { + addQuery(match.group(1)); + } + } + + return queries.toList(); +} + +String? extractMigrationChapterNumber(String chapterName) { + return RegExp( + r'\s*(\d+\.\d+)\s*', + multiLine: true, + ).firstMatch(chapterName)?.group(0) ?? + RegExp(r'\s*(\d+)\s*', multiLine: true).firstMatch(chapterName)?.group(0); +} + +MManga _selectBestCandidate({ + required Manga manga, + required List queries, + required List candidates, +}) { + candidates.sort((left, right) { + final leftScore = _scoreCandidate( + manga: manga, + queries: queries, + candidate: left, + ); + final rightScore = _scoreCandidate( + manga: manga, + queries: queries, + candidate: right, + ); + return rightScore.compareTo(leftScore); + }); + return candidates.first; +} + +double _scoreCandidate({ + required Manga manga, + required List queries, + required MManga candidate, +}) { + final candidateName = _normalizeTitle(candidate.name); + if (candidateName.isEmpty) return 0; + + var score = 0.0; + for (final query in queries) { + final normalizedQuery = _normalizeTitle(query); + if (normalizedQuery.isEmpty) continue; + if (normalizedQuery == candidateName) { + score = score < 100 ? 100 : score; + continue; + } + if (candidateName.contains(normalizedQuery) || + normalizedQuery.contains(candidateName)) { + final ratio = normalizedQuery.length < candidateName.length + ? normalizedQuery.length / candidateName.length + : candidateName.length / normalizedQuery.length; + score = score < (80 * ratio) ? 80 * ratio : score; + } + final tokenScore = _tokenOverlapScore(normalizedQuery, candidateName); + score = score < tokenScore ? tokenScore : score; + } + + final sourceAuthor = _normalizeTitle(manga.author); + final sourceArtist = _normalizeTitle(manga.artist); + final candidateAuthor = _normalizeTitle(candidate.author); + final candidateArtist = _normalizeTitle(candidate.artist); + if (sourceAuthor.isNotEmpty && + (sourceAuthor == candidateAuthor || sourceAuthor == candidateArtist)) { + score += 15; + } + if (sourceArtist.isNotEmpty && + (sourceArtist == candidateArtist || sourceArtist == candidateAuthor)) { + score += 10; + } + + return score; +} + +double _tokenOverlapScore(String left, String right) { + final leftTokens = left.split(' ').where((token) => token.isNotEmpty).toSet(); + final rightTokens = right + .split(' ') + .where((token) => token.isNotEmpty) + .toSet(); + if (leftTokens.isEmpty || rightTokens.isEmpty) return 0; + final overlap = leftTokens.intersection(rightTokens).length; + return (overlap / leftTokens.union(rightTokens).length) * 70; +} + +String _normalizeTitle(String? value) { + return value + ?.toLowerCase() + .replaceAll(RegExp(r'[^a-z0-9\s]'), ' ') + .replaceAll(RegExp(r'\b(the|a|an)\b'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim() ?? + ''; +} + +String? _trimmedOrDefault(String? value, String? defaultValue) { + if (value?.trim().isNotEmpty ?? false) { + return value!.trim(); + } + return defaultValue; +} diff --git a/lib/modules/mass_migration/widgets/mass_migration_widgets.dart b/lib/modules/mass_migration/widgets/mass_migration_widgets.dart new file mode 100644 index 00000000..06ed68f8 --- /dev/null +++ b/lib/modules/mass_migration/widgets/mass_migration_widgets.dart @@ -0,0 +1,170 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mangayomi/eval/model/m_manga.dart'; +import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/models/source.dart'; +import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/utils/cached_network.dart'; +import 'package:mangayomi/utils/constant.dart'; +import 'package:mangayomi/utils/headers.dart'; + +class MassMigrationSourceIcon extends StatelessWidget { + const MassMigrationSourceIcon({required this.source, super.key}); + + final Source? source; + + @override + Widget build(BuildContext context) { + final iconUrl = source?.iconUrl ?? ''; + return Container( + height: 37, + width: 37, + decoration: BoxDecoration( + color: Theme.of(context).secondaryHeaderColor.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(5), + ), + child: iconUrl.isEmpty + ? const Icon(Icons.extension_rounded) + : cachedNetworkImage( + imageUrl: iconUrl, + fit: BoxFit.contain, + width: 37, + height: 37, + errorWidget: const SizedBox( + width: 37, + height: 37, + child: Center(child: Icon(Icons.extension_rounded)), + ), + useCustomNetworkImage: false, + ), + ); + } +} + +class MassMigrationCover extends ConsumerWidget { + const MassMigrationCover({ + this.libraryItem, + this.remoteItem, + this.source, + this.width = 72, + this.height = 104, + super.key, + }); + + final Manga? libraryItem; + final MManga? remoteItem; + final Source? source; + final double width; + final double height; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final customCover = libraryItem?.customCoverImage; + if (customCover != null && customCover.isNotEmpty) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.memory( + Uint8List.fromList(customCover), + width: width, + height: height, + fit: BoxFit.cover, + ), + ); + } + + final imageUrl = toImgUrl( + remoteItem?.imageUrl ?? + libraryItem?.customCoverFromTracker ?? + libraryItem?.imageUrl ?? + '', + ); + final headers = + source == null || + (source!.name?.isEmpty ?? true) || + (source!.lang?.isEmpty ?? true) + ? null + : ref.watch( + headersProvider( + source: source!.name!, + lang: source!.lang!, + sourceId: source!.id, + ), + ); + + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: cachedNetworkImage( + headers: headers, + imageUrl: imageUrl, + width: width, + height: height, + fit: BoxFit.cover, + errorWidget: Container( + width: width, + height: height, + color: Theme.of(context).secondaryHeaderColor.withValues(alpha: 0.4), + child: const Icon(Icons.image_not_supported_outlined), + ), + ), + ); + } +} + +class MassMigrationChapterSection extends StatelessWidget { + const MassMigrationChapterSection({ + required this.title, + required this.chapters, + super.key, + }); + + final String title; + final List chapters; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return ExpansionTile( + tilePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + dense: true, + title: Text('$title (${chapters.length})'), + children: [ + if (chapters.isEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Align( + alignment: Alignment.centerLeft, + child: Text(l10n.mass_migration_no_chapters_found), + ), + ) + else + ...chapters + .take(12) + .map( + (chapter) => ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + title: Text( + chapter, + style: const TextStyle(fontSize: 13), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + if (chapters.length > 12) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + l10n.mass_migration_and_more_chapters(chapters.length - 12), + ), + ), + ), + ], + ); + } +} diff --git a/lib/router/router.dart b/lib/router/router.dart index 3053bd00..9a7d004b 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -16,6 +16,7 @@ import 'package:mangayomi/modules/browse/extension/widgets/create_extension.dart import 'package:mangayomi/modules/browse/sources/sources_filter_screen.dart'; import 'package:mangayomi/modules/calendar/calendar_screen.dart'; import 'package:mangayomi/modules/manga/detail/widgets/migrate_screen.dart'; +import 'package:mangayomi/modules/mass_migration/mass_migration_source_selection_screen.dart'; import 'package:mangayomi/modules/manga/detail/widgets/recommendation_screen.dart'; import 'package:mangayomi/modules/manga/detail/widgets/watch_order_screen.dart'; import 'package:mangayomi/modules/more/data_and_storage/create_backup.dart'; @@ -254,6 +255,11 @@ class RouterNotifier extends ChangeNotifier { name: "migrate", builder: (manga) => MigrationScreen(manga: manga), ), + _genericRoute( + name: "massMigration", + builder: (manga) => + MassMigrationSourceSelectionScreen(initialManga: manga), + ), _genericRoute<(Manga, TrackSearch)>( name: "migrate/tracker", builder: (data) => MigrationScreen(manga: data.$1, trackSearch: data.$2),