Merge pull request #687 from Schnitzel5/feature/mass-migration

added mass migration
This commit is contained in:
Moustapha Kodjo Amadou 2026-04-06 12:57:17 +01:00 committed by GitHub
commit aff7f28476
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 5881 additions and 115 deletions

View file

@ -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<String, String> getHeaders() {
return _extensionCall<Map>(
'getHeaders(`${source.baseUrl ?? ''}`)',
'getHeaders(${jsonEncode(source.baseUrl ?? '')})',
{},
).toMapStringString!;
}
@ -127,14 +128,16 @@ var extention = new DefaultExtension();
Future<MPages> search(String query, int page, List<dynamic> filters) async {
return MPages.fromJson(
await _extensionCallAsync(
'search("$query",$page,${jsonEncode(filterValuesListToJson(filters))})',
'search(${jsonEncode(query)},$page,${jsonEncode(filterValuesListToJson(filters))})',
),
);
}
@override
Future<MManga> 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<List>('getPageList(`$url`)')) {
for (final e in await _extensionCallAsync<List>(
'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<List>(
'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;

View file

@ -154,7 +154,10 @@ const extension = exports.default;
@override
Future<MPages> search(String query, int page, List<dynamic> 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<MManga> 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;

View file

@ -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",

View file

@ -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:

View file

@ -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 => 'الانتقال إلى مصدر آخر';

View file

@ -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 => 'অন্য উৎসলৈ স্থানান্তৰ কৰক';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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 => 'दूसरे स्रोत में माइग्रेट करें';

View file

@ -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';

View file

@ -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';

View file

@ -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 => '別のソースに移行';

View file

@ -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';

View file

@ -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 => 'Перенести на другой источник';

View file

@ -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 => 'ย้ายไปยังแหล่งอื่น';

View file

@ -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ç';

View file

@ -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 => '迁移到另一个来源';

View file

@ -631,6 +631,10 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
value: 3,
child: Text(l10n.migrate),
),
PopupMenuItem<int>(
value: 6,
child: const Text('Mass migration'),
),
if (!isLocalArchive)
PopupMenuItem<int>(
value: 4,
@ -742,6 +746,12 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
botToast("Failed to export metadata: $e");
}
break;
case 6:
context.push(
"/massMigration",
extra: widget.manga,
);
break;
}
},
),

View file

@ -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<void> _migrateManga(MManga preview) async {
String? historyChapter;
String? historyDate;
List<Chapter> 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);
}
}

View file

@ -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,
);
}
}

View file

@ -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),
),
),
),
],
),
);
}
}

View file

@ -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<MassMigrationRunnerScreen> createState() =>
_MassMigrationRunnerScreenState();
}
class _MassMigrationRunnerScreenState
extends ConsumerState<MassMigrationRunnerScreen> {
_MassMigrationPhase _phase = _MassMigrationPhase.matching;
int _currentIndex = 0;
int _migratedCount = 0;
int _skippedCount = 0;
final List<String> _failedItems = [];
final Set<int> _loadingCandidateIndexes = {};
final List<MassMigrationResolvedItem> _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 <String>[];
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<void> _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<void> _pickAnotherCandidate(int index) async {
final item = _resolvedItems[index];
final selected = await showModalBottomSheet<MManga>(
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<void> _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),
],
),
),
);
}
}

View file

@ -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,
),
),
);
},
);
},
),
);
}
}

View file

@ -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<Manga> items;
int get count => items.length;
}
class MassMigrationSearchResult {
const MassMigrationSearchResult({
required this.queries,
required this.candidates,
this.selected,
this.usedQuery,
});
final List<String> queries;
final List<MManga> 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<MassMigrationSourceGroup> buildMassMigrationSourceGroups({
required ItemType itemType,
Manga? prioritizedManga,
}) {
final libraryItems = isar.mangas
.filter()
.favoriteEqualTo(true)
.itemTypeEqualTo(itemType)
.findAllSync();
final grouped = <String, List<Manga>>{};
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<Source> 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;
}

View file

@ -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<void> 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<Chapter> chaptersProgress;
final String? historyChapter;
final String? historyDate;
}
_MigrationSnapshot _captureMigrationSnapshot({
required WidgetRef ref,
required Manga oldManga,
}) {
String? historyChapter;
String? historyDate;
final chaptersProgress = <Chapter>[];
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<MassMigrationSearchResult> 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 <MManga>[];
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<MassMigrationResolvedItem> 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<MassMigrationSearchResult> _resolveSearchResult({
required WidgetRef ref,
required Manga manga,
required Source destinationSource,
}) {
return findBestMassMigrationMatch(
ref: ref,
manga: manga,
destinationSource: destinationSource,
);
}
Future<MassMigrationResolvedItem> _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<String> buildMassMigrationQueries(Manga manga) {
final queries = <String>{};
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<String> queries,
required List<MManga> 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<String> 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;
}

View file

@ -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<String> 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),
),
),
),
],
);
}
}

View file

@ -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<Manga>(
name: "massMigration",
builder: (manga) =>
MassMigrationSourceSelectionScreen(initialManga: manga),
),
_genericRoute<(Manga, TrackSearch)>(
name: "migrate/tracker",
builder: (data) => MigrationScreen(manga: data.$1, trackSearch: data.$2),