Merge branch 'main' into enhance/mpv

This commit is contained in:
Moustapha Kodjo Amadou 2025-08-21 16:35:08 +01:00 committed by GitHub
commit 0452576de6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 2675 additions and 967 deletions

View file

@ -77,6 +77,8 @@
"clean_database_desc": "This will remove all items that are not added to the library!",
"incognito_mode": "Incognito Mode",
"incognito_mode_description": "Pauses reading history",
"downloaded_only": "Downloaded only",
"downloaded_only_description": "Only show downloaded entries in your library",
"download_queue": "Download Queue",
"categories": "Categories",
"statistics": "Statistics",
@ -226,6 +228,10 @@
"sync_finished": "Sync finished",
"sync_failed": "Sync failed",
"sync_button_sync": "Sync progress",
"sync_button_upload": "Upload only",
"sync_button_upload_info": "This operation will fully replace the remote data with local data!",
"sync_button_download": "Download only",
"sync_button_download_info": "This operation will fully replace the local data with remote data!",
"sync_on": "Enable sync",
"sync_auto": "Auto Sync",
"sync_auto_warning": "Auto Sync is currently an experimental feature!",
@ -252,6 +258,7 @@
"anime_extensions": "Anime Extensions",
"manga_extensions": "Manga Extensions",
"novel_extensions": "Novel Extensions",
"extension_settings": "Extension settings",
"anime": "Anime",
"manga": "Manga",
"novel": "Novel",
@ -429,6 +436,13 @@
"include_sensitive_settings": "Include sensitive settings (e.g., tracker login tokens)",
"create": "Create",
"downloads_are_limited_to_wifi": "Downloads are limited to Wi-Fi only",
"recommendations": "Recommendations",
"recommendations_similar": "similar",
"recommendations_weights": "Recommendation Weights",
"recommendations_weights_genre": "Genre Similarity",
"recommendations_weights_setting": "Setting Similarity",
"recommendations_weights_synopsis": "Story Similarity",
"recommendations_weights_theme": "Theme Similarity",
"manga_extensions_repo": "Manga extensions repo",
"anime_extensions_repo": "Anime extensions repo",
"novel_extensions_repo": "Novel extensions repo",

View file

@ -537,6 +537,18 @@ abstract class AppLocalizations {
/// **'Pauses reading history'**
String get incognito_mode_description;
/// No description provided for @downloaded_only.
///
/// In en, this message translates to:
/// **'Downloaded only'**
String get downloaded_only;
/// No description provided for @downloaded_only_description.
///
/// In en, this message translates to:
/// **'Only show downloaded entries in your library'**
String get downloaded_only_description;
/// No description provided for @download_queue.
///
/// In en, this message translates to:
@ -1431,6 +1443,30 @@ abstract class AppLocalizations {
/// **'Sync progress'**
String get sync_button_sync;
/// No description provided for @sync_button_upload.
///
/// In en, this message translates to:
/// **'Upload only'**
String get sync_button_upload;
/// No description provided for @sync_button_upload_info.
///
/// In en, this message translates to:
/// **'This operation will fully replace the remote data with local data!'**
String get sync_button_upload_info;
/// No description provided for @sync_button_download.
///
/// In en, this message translates to:
/// **'Download only'**
String get sync_button_download;
/// No description provided for @sync_button_download_info.
///
/// In en, this message translates to:
/// **'This operation will fully replace the local data with remote data!'**
String get sync_button_download_info;
/// No description provided for @sync_on.
///
/// In en, this message translates to:
@ -1587,6 +1623,12 @@ abstract class AppLocalizations {
/// **'Novel Extensions'**
String get novel_extensions;
/// No description provided for @extension_settings.
///
/// In en, this message translates to:
/// **'Extension settings'**
String get extension_settings;
/// No description provided for @anime.
///
/// In en, this message translates to:
@ -2649,6 +2691,48 @@ abstract class AppLocalizations {
/// **'Downloads are limited to Wi-Fi only'**
String get downloads_are_limited_to_wifi;
/// No description provided for @recommendations.
///
/// In en, this message translates to:
/// **'Recommendations'**
String get recommendations;
/// No description provided for @recommendations_similar.
///
/// In en, this message translates to:
/// **'similar'**
String get recommendations_similar;
/// No description provided for @recommendations_weights.
///
/// In en, this message translates to:
/// **'Recommendation Weights'**
String get recommendations_weights;
/// No description provided for @recommendations_weights_genre.
///
/// In en, this message translates to:
/// **'Genre Similarity'**
String get recommendations_weights_genre;
/// No description provided for @recommendations_weights_setting.
///
/// In en, this message translates to:
/// **'Setting Similarity'**
String get recommendations_weights_setting;
/// No description provided for @recommendations_weights_synopsis.
///
/// In en, this message translates to:
/// **'Story Similarity'**
String get recommendations_weights_synopsis;
/// No description provided for @recommendations_weights_theme.
///
/// In en, this message translates to:
/// **'Theme Similarity'**
String get recommendations_weights_theme;
/// No description provided for @manga_extensions_repo.
///
/// In en, this message translates to:

View file

@ -231,6 +231,13 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get incognito_mode_description => 'يوقف سجل القراءة';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'قائمة الانتظار للتحميل';
@ -699,6 +706,20 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get sync_button_sync => 'مزامنة التقدم';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'تمكين المزامنة';
@ -783,6 +804,9 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get novel_extensions => 'إضافات الروايات';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'أنمي';
@ -1358,6 +1382,27 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get downloads_are_limited_to_wifi => 'التنزيلات مقتصرة على Wi-Fi فقط';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'مستودع إضافات المانجا';

View file

@ -233,6 +233,13 @@ class AppLocalizationsAs extends AppLocalizations {
@override
String get incognito_mode_description => 'পঢ়াৰ ইতিহাস স্থগিত কৰে';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'ডাউনলোড শাৰী';
@ -701,6 +708,20 @@ class AppLocalizationsAs extends AppLocalizations {
@override
String get sync_button_sync => 'Sync progress';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Enable sync';
@ -786,6 +807,9 @@ class AppLocalizationsAs extends AppLocalizations {
@override
String get novel_extensions => 'Novel Extensions';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'এনিমে';
@ -1360,6 +1384,27 @@ class AppLocalizationsAs extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'Downloads are limited to Wi-Fi only';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Manga extensions repo';

View file

@ -233,6 +233,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get incognito_mode_description => 'Pausiert den Leseverlauf';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'Download-Warteschlange';
@ -703,6 +710,20 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get sync_button_sync => 'Jetzt synchronisieren';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Sync aktivieren';
@ -788,6 +809,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get novel_extensions => 'Novel-Erweiterungen';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'Anime';
@ -1369,6 +1393,27 @@ class AppLocalizationsDe extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'Downloads sind nur über WLAN verfügbar';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Manga-Erweiterungs-Repository';

View file

@ -232,6 +232,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get incognito_mode_description => 'Pauses reading history';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'Download Queue';
@ -701,6 +708,20 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sync_button_sync => 'Sync progress';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Enable sync';
@ -786,6 +807,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get novel_extensions => 'Novel Extensions';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'Anime';
@ -1359,6 +1383,27 @@ class AppLocalizationsEn extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'Downloads are limited to Wi-Fi only';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Manga extensions repo';

View file

@ -235,6 +235,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get incognito_mode_description => 'Pausa el historial de lectura';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'Cola de descarga';
@ -705,6 +712,20 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get sync_button_sync => 'Sincronizar progreso';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Habilitar sincronización';
@ -790,6 +811,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get novel_extensions => 'Extensiones de novelas';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'Anime';
@ -1373,6 +1397,27 @@ class AppLocalizationsEs extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'Las descargas están limitadas solo a Wi-Fi';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Repositorio de extensiones de manga';

View file

@ -235,6 +235,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get incognito_mode_description => 'Suspend l\'historique de lecture';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'File de téléchargement';
@ -708,6 +715,20 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get sync_button_sync => 'Synchroniser les progrès';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Activer la synchronisation';
@ -793,6 +814,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get novel_extensions => 'Extensions de romans';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'Animé';
@ -1376,6 +1400,27 @@ class AppLocalizationsFr extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'Les téléchargements sont limités au Wi-Fi uniquement';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Dépôt d\'extensions de mangas';

View file

@ -233,6 +233,13 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get incognito_mode_description => 'पढ़ने का इतिहास रोकता है';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'डाउनलोड कतार';
@ -701,6 +708,20 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get sync_button_sync => 'Sync progress';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Enable sync';
@ -786,6 +807,9 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get novel_extensions => 'Novel Extensions';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'एनीमे';
@ -1361,6 +1385,27 @@ class AppLocalizationsHi extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'Downloads are limited to Wi-Fi only';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Manga extensions repo';

View file

@ -235,6 +235,13 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get incognito_mode_description => 'Menghentikan catatan bacaan';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'Antrian Unduhan';
@ -705,6 +712,20 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get sync_button_sync => 'Sinkronkan progres';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Aktifkan sinkronisasi';
@ -790,6 +811,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get novel_extensions => 'Ekstensi Novel';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'Anime';
@ -1365,6 +1389,27 @@ class AppLocalizationsId extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'Unduhan dibatasi hanya untuk Wi-Fi';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Repositori ekstensi manga';

View file

@ -235,6 +235,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get incognito_mode_description => 'Sospende la cronologia di lettura';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'Coda di download';
@ -705,6 +712,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get sync_button_sync => 'Sincronizza progressi';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Abilita sincronizzazione';
@ -790,6 +811,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get novel_extensions => 'Estensioni romanzo';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'Anime';
@ -1373,6 +1397,27 @@ class AppLocalizationsIt extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'I download sono limitati solo al Wi-Fi';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Repository delle estensioni manga';

View file

@ -235,6 +235,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get incognito_mode_description => 'Pausa o histórico de leitura';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'Fila de download';
@ -705,6 +712,20 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get sync_button_sync => 'Sincronizar progresso';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Ativar sincronização';
@ -790,6 +811,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get novel_extensions => 'Extensões de novels';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'Anime';
@ -1370,6 +1394,27 @@ class AppLocalizationsPt extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'Os downloads estão limitados apenas ao Wi-Fi';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Repositório de extensões de mangás';

View file

@ -234,6 +234,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get incognito_mode_description => 'Пауза в истории чтения';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'Очередь загрузки';
@ -707,6 +714,20 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get sync_button_sync => 'Синхронизировать прогресс';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Включить синхронизацию';
@ -792,6 +813,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get novel_extensions => 'Расширения для романов';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'Аниме';
@ -1372,6 +1396,27 @@ class AppLocalizationsRu extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'Загрузки ограничены только Wi-Fi';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Репозиторий расширений манги';

View file

@ -232,6 +232,13 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get incognito_mode_description => 'หยุดประวัติการอ่านชั่วคราว';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'คิวดาวน์โหลด';
@ -701,6 +708,20 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get sync_button_sync => 'ซิงค์ความคืบหน้า';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'เปิดการซิงค์';
@ -785,6 +806,9 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get novel_extensions => 'ส่วนขยายของนวนิยาย';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'อนิเมะ';
@ -1359,6 +1383,27 @@ class AppLocalizationsTh extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'การดาวน์โหลดจำกัดเฉพาะ Wi-Fi เท่านั้น';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'ที่เก็บส่วนขยายมังงะ';

View file

@ -232,6 +232,13 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get incognito_mode_description => 'Okuma geçmişini duraklatır';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => 'İndirme Kuyruğu';
@ -701,6 +708,20 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get sync_button_sync => 'İlerlemeyi senkronize et';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => 'Senkronizasyonu etkinleştir';
@ -786,6 +807,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get novel_extensions => 'Hikaye Uzantıları';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => 'Anime';
@ -1365,6 +1389,27 @@ class AppLocalizationsTr extends AppLocalizations {
String get downloads_are_limited_to_wifi =>
'İndirmeler yalnızca Wi-Fi ile sınırlıdır';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => 'Manga uzantıları deposu';

View file

@ -227,6 +227,13 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get incognito_mode_description => '暂停阅读历史';
@override
String get downloaded_only => 'Downloaded only';
@override
String get downloaded_only_description =>
'Only show downloaded entries in your library';
@override
String get download_queue => '下载队列';
@ -693,6 +700,20 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get sync_button_sync => '同步进度';
@override
String get sync_button_upload => 'Upload only';
@override
String get sync_button_upload_info =>
'This operation will fully replace the remote data with local data!';
@override
String get sync_button_download => 'Download only';
@override
String get sync_button_download_info =>
'This operation will fully replace the local data with remote data!';
@override
String get sync_on => '启用同步';
@ -775,6 +796,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get novel_extensions => '小说扩展';
@override
String get extension_settings => 'Extension settings';
@override
String get anime => '动画';
@ -1333,6 +1357,27 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get downloads_are_limited_to_wifi => '下载仅限于WiFi';
@override
String get recommendations => 'Recommendations';
@override
String get recommendations_similar => 'similar';
@override
String get recommendations_weights => 'Recommendation Weights';
@override
String get recommendations_weights_genre => 'Genre Similarity';
@override
String get recommendations_weights_setting => 'Setting Similarity';
@override
String get recommendations_weights_synopsis => 'Story Similarity';
@override
String get recommendations_weights_theme => 'Theme Similarity';
@override
String get manga_extensions_repo => '漫画扩展库';

View file

@ -275,6 +275,10 @@ class Settings {
late AudioChannel audioChannels;
int? volumeBoostCap;
bool? downloadedOnlyMode;
late AlgorithmWeights? algorithmWeights;
Settings({
this.id = 227,
@ -399,6 +403,8 @@ class Settings {
this.enableAudioPitchCorrection,
this.audioChannels = AudioChannel.autoSafe,
this.volumeBoostCap,
this.downloadedOnlyMode = false,
this.algorithmWeights,
});
Settings.fromJson(Map<String, dynamic> json) {
@ -630,6 +636,10 @@ class Settings {
audioChannels = AudioChannel
.values[json['audioChannels'] ?? AudioChannel.autoSafe.index];
volumeBoostCap = json['volumeBoostCap'];
downloadedOnlyMode = json['downloadedOnlyMode'];
algorithmWeights = json['algorithmWeights'] != null
? AlgorithmWeights.fromJson(json['algorithmWeights'])
: null;
}
Map<String, dynamic> toJson() => {
@ -776,6 +786,9 @@ class Settings {
'enableAudioPitchCorrection': enableAudioPitchCorrection,
'audioChannels': audioChannels.index,
'volumeBoostCap': volumeBoostCap,
'downloadedOnlyMode': downloadedOnlyMode,
if (algorithmWeights != null)
'algorithmWeights': algorithmWeights!.toJson(),
};
}
@ -1146,6 +1159,35 @@ class PlayerSubtitleSettings {
};
}
@embedded
class AlgorithmWeights {
int? genre;
int? setting;
int? synopsis;
int? theme;
AlgorithmWeights({
this.genre = 30,
this.setting = 15,
this.synopsis = 40,
this.theme = 20,
});
AlgorithmWeights.fromJson(Map<String, dynamic> json) {
genre = json['genre'];
setting = json['setting'];
synopsis = json['synopsis'];
theme = json['theme'];
}
Map<String, dynamic> toJson() => {
'genre': genre,
'setting': setting,
'synopsis': synopsis,
'theme': theme,
};
}
enum ColorFilterBlendMode {
none,
multiply,

View file

@ -124,12 +124,14 @@ class _BrowseScreenState extends ConsumerState<BrowseScreen>
} else {
context.push(
'/globalSearch',
extra:
switch (_tabList[_tabBarController.index]) {
"manga" => ItemType.manga,
"anime" => ItemType.anime,
_ => ItemType.novel,
},
extra: (
null,
switch (_tabList[_tabBarController.index]) {
"manga" => ItemType.manga,
"anime" => ItemType.anime,
_ => ItemType.novel,
},
),
);
}
},

View file

@ -24,15 +24,16 @@ import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class GlobalSearchScreen extends ConsumerStatefulWidget {
final String? search;
final ItemType itemType;
const GlobalSearchScreen({required this.itemType, super.key});
const GlobalSearchScreen({this.search, required this.itemType, super.key});
@override
ConsumerState<GlobalSearchScreen> createState() => _GlobalSearchScreenState();
}
class _GlobalSearchScreenState extends ConsumerState<GlobalSearchScreen> {
String query = "";
String _query = "";
final _textEditingController = TextEditingController();
late final List<Source> sourceList =
ref.read(onlyIncludePinnedSourceStateProvider)
@ -50,8 +51,17 @@ class _GlobalSearchScreenState extends ConsumerState<GlobalSearchScreen> {
.and()
.itemTypeEqualTo(widget.itemType)
.findAllSync();
@override
void initState() {
super.initState();
_textEditingController.text = widget.search ?? "";
}
@override
Widget build(BuildContext context) {
final query = _query.isNotEmpty ? _query : widget.search ?? "";
return Scaffold(
appBar: AppBar(
leading: Container(),
@ -62,27 +72,27 @@ class _GlobalSearchScreenState extends ConsumerState<GlobalSearchScreen> {
Navigator.pop(context);
},
onFieldSubmitted: (value) async {
if (!(query == _textEditingController.text)) {
if (!(_query == _textEditingController.text)) {
setState(() {
query = "";
_query = "";
});
await Future.delayed(const Duration(milliseconds: 10));
setState(() {
query = value;
_query = value;
});
}
},
onSuffixPressed: () {
_textEditingController.clear();
setState(() {
query = "";
_query = "";
});
},
controller: _textEditingController,
),
],
),
body: query.isNotEmpty
body: _query.isNotEmpty || widget.search != null
? SuperListView.builder(
itemCount: sourceList.length,
extentPrecalculationPolicy: SuperPrecalculationPolicy(),
@ -90,7 +100,11 @@ class _GlobalSearchScreenState extends ConsumerState<GlobalSearchScreen> {
final source = sourceList[index];
return SizedBox(
height: 260,
child: SourceSearchScreen(query: query, source: source),
child: SourceSearchScreen(
key: ValueKey(query),
query: query,
source: source,
),
);
},
)

View file

@ -18,10 +18,13 @@ import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/update.dart';
import 'package:mangayomi/modules/library/providers/add_torrent.dart';
import 'package:mangayomi/modules/library/providers/local_archive.dart';
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/modules/manga/detail/providers/update_manga_detail_providers.dart';
import 'package:mangayomi/modules/more/categories/providers/isar_providers.dart';
import 'package:mangayomi/modules/more/providers/downloaded_only_state_provider.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/modules/widgets/bottom_select_bar.dart';
import 'package:mangayomi/modules/widgets/category_selection_dialog.dart';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart';
@ -134,6 +137,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
final withoutCategories = ref.watch(
getAllMangaWithoutCategoriesStreamProvider(itemType: widget.itemType),
);
final downloadedOnly = ref.watch(downloadedOnlyStateProvider);
final mangaAll = ref.watch(
getAllMangaStreamProvider(
categoryId: null,
@ -247,6 +251,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly: downloadedOnly,
);
}
if (tabCount > 0 &&
@ -279,6 +284,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType,
downloadedOnly: downloadedOnly,
);
final withoutCategoryNumberOfItemsList =
_filterAndSortManga(
@ -288,6 +294,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType,
downloadedOnly: downloadedOnly,
);
return DefaultTabController(
@ -370,6 +377,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
categoryId:
entr[i - 1].id!,
settings: settings,
downloadedOnly:
downloadedOnly,
),
],
),
@ -396,6 +405,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
continueReaderBtn,
categoryId: entr[i].id!,
settings: settings,
downloadedOnly:
downloadedOnly,
),
],
),
@ -432,6 +443,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly:
downloadedOnly,
)
: _bodyWithCatories(
categoryId:
@ -454,6 +467,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly:
downloadedOnly,
),
if (withoutCategory.isEmpty)
for (
@ -481,6 +496,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly: downloadedOnly,
),
],
),
@ -501,6 +517,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType,
downloadedOnly: downloadedOnly,
);
return Scaffold(
appBar: _appBar(
@ -526,6 +543,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly: downloadedOnly,
),
);
},
@ -554,159 +572,85 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
return const ProgressCenter();
},
),
bottomNavigationBar: Consumer(
builder: (context, ref, child) {
final isLongPressed = ref.watch(isLongPressedMangaStateProvider);
final color = Theme.of(context).textTheme.bodyLarge!.color!;
bottomNavigationBar: Builder(
builder: (context) {
final mangaIds = ref.watch(mangasListStateProvider);
return AnimatedContainer(
curve: Curves.easeIn,
decoration: BoxDecoration(
color: context.primaryColor.withValues(alpha: 0.2),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
final color = Theme.of(context).textTheme.bodyLarge!.color!;
return BottomSelectBar(
isVisible: ref.watch(isLongPressedStateProvider),
actions: [
BottomSelectButton(
icon: Icon(Icons.label_outline_rounded, color: color),
onPressed: () {
final mangaIdsList = ref.watch(mangasListStateProvider);
final List<Manga> bulkMangas = mangaIdsList
.map((id) => isar.mangas.getSync(id)!)
.toList();
showCategorySelectionDialog(
context: context,
ref: ref,
itemType: widget.itemType,
bulkMangas: bulkMangas,
);
},
),
),
duration: const Duration(milliseconds: 100),
height: isLongPressed ? 70 : 0,
width: context.width(1),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shadowColor: Colors.transparent,
elevation: 0,
backgroundColor: Colors.transparent,
),
onPressed: () {
final mangaIdsList = ref.watch(
mangasListStateProvider,
);
final List<Manga> bulkMangas = mangaIdsList
.map((id) => isar.mangas.getSync(id)!)
.toList();
showCategorySelectionDialog(
context: context,
ref: ref,
itemType: widget.itemType,
bulkMangas: bulkMangas,
);
},
child: Icon(
Icons.label_outline_rounded,
color: color,
),
BottomSelectButton(
icon: Icon(Icons.done_all_sharp, color: color),
onPressed: () {
ref
.read(
mangasSetIsReadStateProvider(
mangaIds: mangaIds,
markAsRead: true,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
),
),
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
ref
.read(
mangasSetIsReadStateProvider(
mangaIds: mangaIds,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
);
},
child: Icon(Icons.done_all_sharp, color: color),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
),
),
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
ref
.read(
mangasSetUnReadStateProvider(
mangaIds: mangaIds,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
);
},
child: Icon(Icons.remove_done_sharp, color: color),
);
},
),
BottomSelectButton(
icon: Icon(Icons.remove_done_sharp, color: color),
onPressed: () {
ref
.read(
mangasSetIsReadStateProvider(
mangaIds: mangaIds,
markAsRead: false,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
),
),
// Expanded(
// child: SizedBox(
// height: 70,
// child: ElevatedButton(
// style: ElevatedButton.styleFrom(
// elevation: 0,
// backgroundColor: Colors.transparent,
// shadowColor: Colors.transparent,
// ),
// onPressed: () {},
// child: Icon(
// Icons.download_outlined,
// color: color,
// )),
// ),
// ),
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
_deleteManga();
},
child: Icon(
Icons.delete_outline_outlined,
color: color,
),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
),
),
],
),
);
},
),
// BottomBarAction(
// icon: Icon(Icons.download_outlined, color: color),
// onPressed: () {}
// ),
BottomSelectButton(
icon: Icon(Icons.delete_outline_outlined, color: color),
onPressed: () => _deleteManga(),
),
],
);
},
),
@ -731,6 +675,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
required bool continueReaderBtn,
required int categoryId,
required Settings settings,
required bool downloadedOnly,
}) {
final mangas = ref.watch(
getAllMangaStreamProvider(
@ -755,6 +700,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType!,
downloadedOnly: downloadedOnly,
);
return CircleAvatar(
backgroundColor: Theme.of(context).focusColor,
@ -791,6 +737,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
required WidgetRef ref,
required DisplayType displayType,
required Settings settings,
required bool downloadedOnly,
}) {
final l10n = l10nLocalizations(context)!;
final mangas = ref.watch(
@ -818,6 +765,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType!,
downloadedOnly: downloadedOnly,
);
if (entries.isNotEmpty) {
final entriesManga = reverse ? entries.reversed.toList() : entries;
@ -875,6 +823,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
required WidgetRef ref,
bool withoutCategories = false,
required Settings settings,
required bool downloadedOnly,
}) {
final sortType = ref
.watch(
@ -907,6 +856,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType ?? 0,
downloadedOnly: downloadedOnly,
);
if (entries.isNotEmpty) {
final entriesManga = reverse ? entries.reversed.toList() : entries;
@ -970,6 +920,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
required int startedFilterType,
required int bookmarkedFilterType,
required int sortType,
bool downloadedOnly = false,
}) {
List<Manga>? mangas;
final searchQuery = _textEditingController.text;
@ -984,7 +935,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
.where((element) {
// Filter by download
List list = [];
if (downloadFilterType == 1) {
if (downloadFilterType == 1 || downloadedOnly) {
for (var chap in element.chapters) {
final modelChapDownload = isar.downloads
.filter()
@ -1206,7 +1157,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
ref.read(mangasListStateProvider.notifier).clear();
ref
.read(isLongPressedMangaStateProvider.notifier)
.read(isLongPressedStateProvider.notifier)
.update(false);
if (mounted) {
Navigator.pop(context);
@ -1859,7 +1810,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
int? categoryId,
Settings settings,
) {
final isLongPressed = ref.watch(isLongPressedMangaStateProvider);
final isLongPressed = ref.watch(isLongPressedStateProvider);
final mangaIdsList = ref.watch(mangasListStateProvider);
final manga = categoryId == null
? ref.watch(
@ -1888,7 +1839,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
ref.read(mangasListStateProvider.notifier).clear();
ref
.read(isLongPressedMangaStateProvider.notifier)
.read(isLongPressedStateProvider.notifier)
.update(!isLongPressed);
},
icon: const Icon(Icons.clear),
@ -1913,7 +1864,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
.selectSome(manga);
}
ref
.read(isLongPressedMangaStateProvider.notifier)
.read(isLongPressedStateProvider.notifier)
.update(false);
} else {
for (var manga in data) {

View file

@ -3,6 +3,7 @@ import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -785,7 +786,7 @@ class MangasListState extends _$MangasListState {
newList.add(value.id!);
}
if (newList.isEmpty) {
ref.read(isLongPressedMangaStateProvider.notifier).update(false);
ref.read(isLongPressedStateProvider.notifier).update(false);
}
state = newList;
}
@ -814,65 +815,24 @@ class MangasListState extends _$MangasListState {
}
}
@riverpod
class IsLongPressedMangaState extends _$IsLongPressedMangaState {
@override
bool build() {
return false;
}
void update(bool value) {
state = value;
}
}
@riverpod
class MangasSetIsReadState extends _$MangasSetIsReadState {
@override
void build({required List<int> mangaIds}) {}
void build({required List<int> mangaIds, required bool markAsRead}) {}
void set() {
final allChapters = <Chapter>[];
final allMangas = <Manga>[];
final now = DateTime.now().millisecondsSinceEpoch;
for (var mangaid in mangaIds) {
final manga = isar.mangas.getSync(mangaid)!;
final chapters = manga.chapters;
if (chapters.isNotEmpty) {
chapters.last.updateTrackChapterRead(ref);
for (var chapter in chapters) {
chapter.isRead = true;
chapter.lastPageRead = "1";
chapter.updatedAt = DateTime.now().millisecondsSinceEpoch;
chapter.manga.value = manga;
allChapters.add(chapter);
}
allMangas.add(manga);
}
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(allChapters);
isar.mangas.putAllSync(allMangas);
});
ref.read(isLongPressedMangaStateProvider.notifier).update(false);
ref.read(mangasListStateProvider.notifier).clear();
}
}
@riverpod
class MangasSetUnReadState extends _$MangasSetUnReadState {
@override
void build({required List<int> mangaIds}) {}
void set() {
final allChapters = <Chapter>[];
final allMangas = <Manga>[];
for (var mangaid in mangaIds) {
final manga = isar.mangas.getSync(mangaid)!;
for (var chapter in manga.chapters) {
chapter.isRead = false;
chapter.updatedAt = DateTime.now().millisecondsSinceEpoch;
if (chapters.isEmpty) continue;
if (markAsRead) chapters.last.updateTrackChapterRead(ref);
for (var chapter in chapters) {
chapter.isRead = markAsRead;
if (markAsRead) chapter.lastPageRead = "1";
chapter.updatedAt = now;
chapter.manga.value = manga;
allChapters.add(chapter);
}
@ -884,7 +844,7 @@ class MangasSetUnReadState extends _$MangasSetUnReadState {
isar.mangas.putAllSync(allMangas);
});
ref.read(isLongPressedMangaStateProvider.notifier).update(false);
ref.read(isLongPressedStateProvider.notifier).update(false);
ref.read(mangasListStateProvider.notifier).clear();
}
}

View file

@ -2517,7 +2517,7 @@ class _SortLibraryMangaStateProviderElement
Settings get settings => (origin as SortLibraryMangaStateProvider).settings;
}
String _$mangasListStateHash() => r'ad1cc419dfd3793bfc8c90f3ce8b7726561dd9ad';
String _$mangasListStateHash() => r'bbd2e3600ec22a774b1774ae3c221815e52bfef6';
/// See also [MangasListState].
@ProviderFor(MangasListState)
@ -2533,32 +2533,17 @@ final mangasListStateProvider =
);
typedef _$MangasListState = AutoDisposeNotifier<List<int>>;
String _$isLongPressedMangaStateHash() =>
r'f77076b0335e92df26a75ea0c338d4214a330184';
/// See also [IsLongPressedMangaState].
@ProviderFor(IsLongPressedMangaState)
final isLongPressedMangaStateProvider =
AutoDisposeNotifierProvider<IsLongPressedMangaState, bool>.internal(
IsLongPressedMangaState.new,
name: r'isLongPressedMangaStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$isLongPressedMangaStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$IsLongPressedMangaState = AutoDisposeNotifier<bool>;
String _$mangasSetIsReadStateHash() =>
r'8f4f7f22ea8e82cf2370fb997033e1a4ec03168f';
r'2a1b1005e2ed5068d36188a3fb969d21b64bfef6';
abstract class _$MangasSetIsReadState
extends BuildlessAutoDisposeNotifier<void> {
late final List<int> mangaIds;
late final bool markAsRead;
void build({
required List<int> mangaIds,
required bool markAsRead,
});
}
@ -2574,9 +2559,11 @@ class MangasSetIsReadStateFamily extends Family<void> {
/// See also [MangasSetIsReadState].
MangasSetIsReadStateProvider call({
required List<int> mangaIds,
required bool markAsRead,
}) {
return MangasSetIsReadStateProvider(
mangaIds: mangaIds,
markAsRead: markAsRead,
);
}
@ -2586,6 +2573,7 @@ class MangasSetIsReadStateFamily extends Family<void> {
) {
return call(
mangaIds: provider.mangaIds,
markAsRead: provider.markAsRead,
);
}
@ -2610,8 +2598,11 @@ class MangasSetIsReadStateProvider
/// See also [MangasSetIsReadState].
MangasSetIsReadStateProvider({
required List<int> mangaIds,
required bool markAsRead,
}) : this._internal(
() => MangasSetIsReadState()..mangaIds = mangaIds,
() => MangasSetIsReadState()
..mangaIds = mangaIds
..markAsRead = markAsRead,
from: mangasSetIsReadStateProvider,
name: r'mangasSetIsReadStateProvider',
debugGetCreateSourceHash:
@ -2622,6 +2613,7 @@ class MangasSetIsReadStateProvider
allTransitiveDependencies:
MangasSetIsReadStateFamily._allTransitiveDependencies,
mangaIds: mangaIds,
markAsRead: markAsRead,
);
MangasSetIsReadStateProvider._internal(
@ -2632,9 +2624,11 @@ class MangasSetIsReadStateProvider
required super.debugGetCreateSourceHash,
required super.from,
required this.mangaIds,
required this.markAsRead,
}) : super.internal();
final List<int> mangaIds;
final bool markAsRead;
@override
void runNotifierBuild(
@ -2642,6 +2636,7 @@ class MangasSetIsReadStateProvider
) {
return notifier.build(
mangaIds: mangaIds,
markAsRead: markAsRead,
);
}
@ -2650,13 +2645,16 @@ class MangasSetIsReadStateProvider
return ProviderOverride(
origin: this,
override: MangasSetIsReadStateProvider._internal(
() => create()..mangaIds = mangaIds,
() => create()
..mangaIds = mangaIds
..markAsRead = markAsRead,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
mangaIds: mangaIds,
markAsRead: markAsRead,
),
);
}
@ -2669,13 +2667,16 @@ class MangasSetIsReadStateProvider
@override
bool operator ==(Object other) {
return other is MangasSetIsReadStateProvider && other.mangaIds == mangaIds;
return other is MangasSetIsReadStateProvider &&
other.mangaIds == mangaIds &&
other.markAsRead == markAsRead;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, mangaIds.hashCode);
hash = _SystemHash.combine(hash, markAsRead.hashCode);
return _SystemHash.finish(hash);
}
@ -2686,6 +2687,9 @@ class MangasSetIsReadStateProvider
mixin MangasSetIsReadStateRef on AutoDisposeNotifierProviderRef<void> {
/// The parameter `mangaIds` of this provider.
List<int> get mangaIds;
/// The parameter `markAsRead` of this provider.
bool get markAsRead;
}
class _MangasSetIsReadStateProviderElement
@ -2695,153 +2699,8 @@ class _MangasSetIsReadStateProviderElement
@override
List<int> get mangaIds => (origin as MangasSetIsReadStateProvider).mangaIds;
}
String _$mangasSetUnReadStateHash() =>
r'09ddd287b110fd76494f9f56bd5cf76f58936f1f';
abstract class _$MangasSetUnReadState
extends BuildlessAutoDisposeNotifier<void> {
late final List<int> mangaIds;
void build({
required List<int> mangaIds,
});
}
/// See also [MangasSetUnReadState].
@ProviderFor(MangasSetUnReadState)
const mangasSetUnReadStateProvider = MangasSetUnReadStateFamily();
/// See also [MangasSetUnReadState].
class MangasSetUnReadStateFamily extends Family<void> {
/// See also [MangasSetUnReadState].
const MangasSetUnReadStateFamily();
/// See also [MangasSetUnReadState].
MangasSetUnReadStateProvider call({
required List<int> mangaIds,
}) {
return MangasSetUnReadStateProvider(
mangaIds: mangaIds,
);
}
@override
MangasSetUnReadStateProvider getProviderOverride(
covariant MangasSetUnReadStateProvider provider,
) {
return call(
mangaIds: provider.mangaIds,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'mangasSetUnReadStateProvider';
}
/// See also [MangasSetUnReadState].
class MangasSetUnReadStateProvider
extends AutoDisposeNotifierProviderImpl<MangasSetUnReadState, void> {
/// See also [MangasSetUnReadState].
MangasSetUnReadStateProvider({
required List<int> mangaIds,
}) : this._internal(
() => MangasSetUnReadState()..mangaIds = mangaIds,
from: mangasSetUnReadStateProvider,
name: r'mangasSetUnReadStateProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$mangasSetUnReadStateHash,
dependencies: MangasSetUnReadStateFamily._dependencies,
allTransitiveDependencies:
MangasSetUnReadStateFamily._allTransitiveDependencies,
mangaIds: mangaIds,
);
MangasSetUnReadStateProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.mangaIds,
}) : super.internal();
final List<int> mangaIds;
@override
void runNotifierBuild(
covariant MangasSetUnReadState notifier,
) {
return notifier.build(
mangaIds: mangaIds,
);
}
@override
Override overrideWith(MangasSetUnReadState Function() create) {
return ProviderOverride(
origin: this,
override: MangasSetUnReadStateProvider._internal(
() => create()..mangaIds = mangaIds,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
mangaIds: mangaIds,
),
);
}
@override
AutoDisposeNotifierProviderElement<MangasSetUnReadState, void>
createElement() {
return _MangasSetUnReadStateProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is MangasSetUnReadStateProvider && other.mangaIds == mangaIds;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, mangaIds.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin MangasSetUnReadStateRef on AutoDisposeNotifierProviderRef<void> {
/// The parameter `mangaIds` of this provider.
List<int> get mangaIds;
}
class _MangasSetUnReadStateProviderElement
extends AutoDisposeNotifierProviderElement<MangasSetUnReadState, void>
with MangasSetUnReadStateRef {
_MangasSetUnReadStateProviderElement(super.provider);
@override
List<int> get mangaIds => (origin as MangasSetUnReadStateProvider).mangaIds;
bool get markAsRead => (origin as MangasSetIsReadStateProvider).markAsRead;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -9,6 +9,7 @@ import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/modules/library/providers/isar_providers.dart';
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/utils/constant.dart';
@ -52,7 +53,7 @@ class _LibraryGridViewWidgetState extends State<LibraryGridViewWidget> {
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
final isLongPressed = ref.watch(isLongPressedMangaStateProvider);
final isLongPressed = ref.watch(isLongPressedStateProvider);
final itemType = widget.itemType;
final gridSize = ref.watch(
@ -430,7 +431,7 @@ class _LibraryGridViewWidgetState extends State<LibraryGridViewWidget> {
) {
if (!isLongPressed) {
ref.read(mangasListStateProvider.notifier).update(entry);
ref.read(isLongPressedMangaStateProvider.notifier).update(!isLongPressed);
ref.read(isLongPressedStateProvider.notifier).update(!isLongPressed);
} else {
ref.read(mangasListStateProvider.notifier).update(entry);
}

View file

@ -9,6 +9,7 @@ import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/modules/library/providers/isar_providers.dart';
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/utils/constant.dart';
@ -44,7 +45,7 @@ class LibraryListViewWidget extends StatelessWidget {
bool isLocalArchive = entry.isLocalArchive ?? false;
return Consumer(
builder: (context, ref, child) {
final isLongPressed = ref.watch(isLongPressedMangaStateProvider);
final isLongPressed = ref.watch(isLongPressedStateProvider);
return Material(
borderRadius: BorderRadius.circular(5),
color: Colors.transparent,
@ -80,7 +81,7 @@ class LibraryListViewWidget extends StatelessWidget {
ref.read(mangasListStateProvider.notifier).update(entry);
ref
.read(isLongPressedMangaStateProvider.notifier)
.read(isLongPressedStateProvider.notifier)
.update(!isLongPressed);
} else {
ref.read(mangasListStateProvider.notifier).update(entry);
@ -91,7 +92,7 @@ class LibraryListViewWidget extends StatelessWidget {
ref.read(mangasListStateProvider.notifier).update(entry);
ref
.read(isLongPressedMangaStateProvider.notifier)
.read(isLongPressedStateProvider.notifier)
.update(!isLongPressed);
} else {
ref.read(mangasListStateProvider.notifier).update(entry);

View file

@ -11,6 +11,7 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/update.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/providers/downloaded_only_state_provider.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/modules/widgets/loading_icon.dart';
@ -23,7 +24,7 @@ import 'package:mangayomi/router/router.dart';
import 'package:mangayomi/services/fetch_sources_list.dart';
import 'package:mangayomi/services/sync_server.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
final libLocationRegex = RegExp(r"^/(Manga|Anime|Novel)Library$");
@ -239,10 +240,16 @@ class _MainScreenState extends ConsumerState<MainScreen> {
}
final incognitoMode = ref.watch(incognitoModeStateProvider);
final isLongPressed = ref.watch(isLongPressedMangaStateProvider);
final downloadedOnly = ref.watch(downloadedOnlyStateProvider);
final isLongPressed = ref.watch(isLongPressedStateProvider);
return Column(
children: [
if (!isReadingScreen)
_DownloadedOnlyBar(
downloadedOnly: downloadedOnly,
l10n: l10n,
),
if (!isReadingScreen)
_IncognitoModeBar(incognitoMode: incognitoMode, l10n: l10n),
Flexible(
@ -526,6 +533,45 @@ class _MainScreenState extends ConsumerState<MainScreen> {
}
}
class _DownloadedOnlyBar extends StatelessWidget {
const _DownloadedOnlyBar({required this.downloadedOnly, required this.l10n});
final bool downloadedOnly;
final dynamic l10n;
@override
Widget build(BuildContext context) {
return Material(
child: AnimatedContainer(
height: downloadedOnly
? Platform.isAndroid || Platform.isIOS
? MediaQuery.of(context).padding.top * 2
: 50
: 0,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 150),
color: context.secondaryColor,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
l10n.downloaded_only,
style: TextStyle(
color: Colors.white,
fontFamily: GoogleFonts.aBeeZee().fontFamily,
),
),
),
],
),
),
);
}
}
class _IncognitoModeBar extends StatelessWidget {
const _IncognitoModeBar({required this.incognitoMode, required this.l10n});

View file

@ -22,8 +22,10 @@ import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.d
import 'package:mangayomi/modules/manga/detail/widgets/tracker_search_widget.dart';
import 'package:mangayomi/modules/manga/detail/widgets/tracker_widget.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/more/providers/algorithm_weights_state_provider.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart';
import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart';
import 'package:mangayomi/modules/widgets/bottom_select_bar.dart';
import 'package:mangayomi/modules/widgets/category_selection_dialog.dart';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
@ -624,6 +626,11 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
value: 3,
child: Text(l10n.migrate),
),
if (!isLocalArchive)
PopupMenuItem<int>(
value: 4,
child: Text(l10n.extension_settings),
),
];
},
onSelected: (value) {
@ -651,6 +658,16 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
case 3:
context.push("/migrate", extra: widget.manga);
break;
case 4:
final source = getSource(
widget.manga!.lang!,
widget.manga!.source!,
);
context.push(
'/extension_detail',
extra: source,
);
break;
}
},
),
@ -811,8 +828,8 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
],
),
),
bottomNavigationBar: Consumer(
builder: (context, ref, child) {
bottomNavigationBar: Builder(
builder: (context) {
final chap = ref.watch(chaptersListStateProvider);
bool getLength1 = chap.length == 1;
bool checkFirstBookmarked =
@ -821,340 +838,241 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
chap.isNotEmpty && chap.first.isRead! && getLength1;
final l10n = l10nLocalizations(context)!;
final color = Theme.of(context).textTheme.bodyLarge!.color!;
return AnimatedContainer(
curve: Curves.easeIn,
decoration: BoxDecoration(
color: context.primaryColor.withValues(alpha: 0.2),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
return BottomSelectBar(
isVisible: isLongPressed,
actions: [
BottomSelectButton(
icon: Icon(
checkFirstBookmarked
? Icons.bookmark_remove_outlined
: Icons.bookmark_add_outlined,
color: color,
),
onPressed: () {
final chapters = ref.watch(chaptersListStateProvider);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
for (var chapter in chapters) {
chapter.isBookmarked = !chapter.isBookmarked!;
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref.read(chaptersListStateProvider.notifier).clear();
},
),
),
duration: const Duration(milliseconds: 100),
height: isLongPressed ? 70 : 0,
width: context.width(1),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
final chapters = ref.watch(
chaptersListStateProvider,
);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
for (var chapter in chapters) {
chapter.isBookmarked = !chapter.isBookmarked!;
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Icon(
checkFirstBookmarked
? Icons.bookmark_remove_outlined
: Icons.bookmark_add_outlined,
color: color,
),
),
),
BottomSelectButton(
icon: Icon(
checkReadBookmarked
? Icons.remove_done_sharp
: Icons.done_all_sharp,
color: color,
),
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
final chapters = ref.watch(
chaptersListStateProvider,
);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
for (var chapter in chapters) {
chapter.isRead = !chapter.isRead!;
if (!chapter.isRead!) {
chapter.lastPageRead = "1";
}
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
if (chapter.isRead!) {
chapter.updateTrackChapterRead(ref);
}
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
isar.mangas.putSync(widget.manga!);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Icon(
checkReadBookmarked
? Icons.remove_done_sharp
: Icons.done_all_sharp,
color: color,
),
),
),
),
if (getLength1)
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
int index = chapters.indexOf(chap.first);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
chapters[index + 1].updateTrackChapterRead(ref);
for (
var i = index + 1;
i < chapters.length;
i++
) {
final chapter = chapters[i];
if (!chapter.isRead!) {
chapter.isRead = true;
chapter.lastPageRead = "1";
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
}
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
isar.mangas.putSync(widget.manga!);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Stack(
children: [
Icon(Icons.done_outlined, color: color),
Positioned(
bottom: 0,
right: 0,
child: Icon(
Icons.arrow_downward_outlined,
size: 11,
color: color,
),
),
],
),
),
),
),
if (!isLocalArchive)
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
for (var chapter in ref.watch(
chaptersListStateProvider,
)) {
final entries = isar.downloads
.filter()
.idEqualTo(chapter.id)
.findAllSync();
if (entries.isEmpty ||
!entries.first.isDownload!) {
ref.read(
addDownloadToQueueProvider(
chapter: chapter,
),
);
}
}
ref.watch(processDownloadsProvider());
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Icon(Icons.download_outlined, color: color),
),
),
),
if (isLocalArchive)
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
final selectedChapters = ref.watch(
chaptersListStateProvider,
);
final totalChapters =
widget.manga!.chapters.length;
final isLastChapters =
selectedChapters.length == totalChapters;
final isAnime = widget.itemType == ItemType.anime;
final entryType = isAnime
? l10n.episode
: l10n.chapter;
final pluralEntryType = isAnime
? l10n.episodes
: l10n.chapters;
final mediaType = isAnime
? l10n.anime
: l10n.manga;
final warningMessage = l10n
.last_entry_delete_warning(
totalChapters,
entryType,
pluralEntryType,
mediaType,
);
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.delete_chapters),
content: isLastChapters
? Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Icon(
Icons.warning_amber_rounded,
color: Colors.orange,
),
const SizedBox(width: 12),
Expanded(
child: Text(
warningMessage,
style: TextStyle(
color: Colors.red,
),
),
),
],
)
: null,
actions: [
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () async {
final navigator = Navigator.of(
context,
);
await isar.writeTxn(() async {
final idsToDelete =
selectedChapters
.map((c) => c.id!)
.toList();
await isar.chapters.deleteAll(
idsToDelete,
);
});
if (!mounted) return;
ref
.read(
isLongPressedStateProvider
.notifier,
)
.update(false);
ref
.read(
chaptersListStateProvider
.notifier,
)
.clear();
navigator.pop();
if (isLastChapters) {
navigator.pop();
Future.delayed(
const Duration(
milliseconds: 350,
),
() {
isar.writeTxn(
() => isar.mangas.delete(
widget.manga!.id!,
),
);
},
);
}
},
child: Text(l10n.delete),
),
],
),
],
);
},
);
},
onPressed: () {
final chapters = ref.watch(chaptersListStateProvider);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
for (var chapter in chapters) {
chapter.isRead = !chapter.isRead!;
if (!chapter.isRead!) {
chapter.lastPageRead = "1";
}
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
if (chapter.isRead!) {
chapter.updateTrackChapterRead(ref);
}
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
isar.mangas.putSync(widget.manga!);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref.read(chaptersListStateProvider.notifier).clear();
},
),
if (getLength1)
BottomSelectButton(
icon: Stack(
children: [
Icon(Icons.done_outlined, color: color),
Positioned(
bottom: 0,
right: 0,
child: Icon(
Icons.delete_outline_outlined,
Icons.arrow_downward_outlined,
size: 11,
color: color,
),
),
),
],
),
],
),
onPressed: () {
int index = chapters.indexOf(chap.first);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
chapters[index + 1].updateTrackChapterRead(ref);
for (var i = index + 1; i < chapters.length; i++) {
final chapter = chapters[i];
if (!chapter.isRead!) {
chapter.isRead = true;
chapter.lastPageRead = "1";
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
}
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
isar.mangas.putSync(widget.manga!);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref.read(chaptersListStateProvider.notifier).clear();
},
),
if (!isLocalArchive)
BottomSelectButton(
icon: Icon(Icons.download_outlined, color: color),
onPressed: () {
for (var chapter in ref.watch(
chaptersListStateProvider,
)) {
final entries = isar.downloads
.filter()
.idEqualTo(chapter.id)
.findAllSync();
if (entries.isEmpty || !entries.first.isDownload!) {
ref.read(
addDownloadToQueueProvider(chapter: chapter),
);
}
}
ref.watch(processDownloadsProvider());
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref.read(chaptersListStateProvider.notifier).clear();
},
),
if (isLocalArchive)
BottomSelectButton(
icon: Icon(Icons.delete_outline_outlined, color: color),
onPressed: () {
final selectedChapters = ref.watch(
chaptersListStateProvider,
);
final totalChapters = widget.manga!.chapters.length;
final isLastChapters =
selectedChapters.length == totalChapters;
final isAnime = widget.itemType == ItemType.anime;
final entryType = isAnime ? l10n.episode : l10n.chapter;
final pluralEntryType = isAnime
? l10n.episodes
: l10n.chapters;
final mediaType = isAnime ? l10n.anime : l10n.manga;
final warningMessage = l10n.last_entry_delete_warning(
totalChapters,
entryType,
pluralEntryType,
mediaType,
);
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.delete_chapters),
content: isLastChapters
? Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Icon(
Icons.warning_amber_rounded,
color: Colors.orange,
),
const SizedBox(width: 12),
Expanded(
child: Text(
warningMessage,
style: TextStyle(color: Colors.red),
),
),
],
)
: null,
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () async {
final navigator = Navigator.of(context);
await isar.writeTxn(() async {
final idsToDelete = selectedChapters
.map((c) => c.id!)
.toList();
await isar.chapters.deleteAll(
idsToDelete,
);
});
if (!mounted) return;
ref
.read(
isLongPressedStateProvider
.notifier,
)
.update(false);
ref
.read(
chaptersListStateProvider
.notifier,
)
.clear();
navigator.pop();
if (isLastChapters) {
navigator.pop();
Future.delayed(
const Duration(milliseconds: 350),
() {
isar.writeTxn(
() => isar.mangas.delete(
widget.manga!.id!,
),
);
},
);
}
},
child: Text(l10n.delete),
),
],
),
],
);
},
);
},
),
],
);
},
),
@ -1694,6 +1612,37 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
),
),
),
const SizedBox(height: 15),
SizedBox(
width: context.width(1),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: OutlinedButton.icon(
style: ButtonStyle(
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
),
),
onPressed: () {
final algorithmWeights = ref.read(
algorithmWeightsStateProvider,
);
context.push(
"/recommendations",
extra: (
widget.manga!.name,
widget.manga!.itemType,
algorithmWeights,
),
);
},
label: Text(l10n.recommendations),
icon: Icon(Icons.arrow_right_alt_outlined),
),
),
),
if (!context.isTablet)
Column(
children: [

View file

@ -0,0 +1,383 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/recommendation.dart';
import 'package:mangayomi/utils/constant.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:marquee/marquee.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class RecommendationScreen extends StatefulWidget {
final String name;
final ItemType itemType;
final AlgorithmWeights algorithmWeights;
const RecommendationScreen({
super.key,
required this.name,
required this.itemType,
required this.algorithmWeights,
});
@override
State<RecommendationScreen> createState() => _RecommendationScreenState();
}
class _RecommendationScreenState extends State<RecommendationScreen> {
String _errorMessage = "";
bool _isLoading = true;
List<RecommendationResult>? data;
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
try {
_errorMessage = "";
data = await getRecommendations(
widget.name,
widget.itemType,
widget.algorithmWeights,
);
if (mounted) {
setState(() {
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(title: Text(l10n.recommendations)),
body: Padding(
padding: EdgeInsetsGeometry.all(5),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Builder(
builder: (context) {
if (_errorMessage.isNotEmpty) {
return Center(child: Text(_errorMessage));
}
if (data != null && data!.isNotEmpty) {
return SuperListView.builder(
extentPrecalculationPolicy: SuperPrecalculationPolicy(),
itemCount: data!.length,
itemBuilder: (context, index) {
final recommendation = data![index];
return ListTile(
onTap: () => context.push(
'/globalSearch',
extra: (
recommendation.titleEnglish ??
recommendation.titleRomaji ??
recommendation.titleNative,
widget.itemType,
),
),
title: Row(
children: [
if (recommendation.imgURLs.isNotEmpty)
_thumbnailPreview(
context,
recommendation.imgURLs.first,
),
const SizedBox(width: 15),
recommendation.description != null
? Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
_buildTitle(
recommendation.titleEnglish ??
recommendation.titleRomaji ??
recommendation.titleNative ??
"",
context,
),
Text(
recommendation.description!,
style: const TextStyle(
fontSize: 11,
),
overflow: TextOverflow.clip,
),
],
),
)
: Flexible(
child: _buildTitle(
recommendation.titleEnglish ??
recommendation.titleRomaji ??
recommendation.titleNative ??
"",
context,
),
),
],
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
child: recommendation.genres.isEmpty
? const SizedBox(height: 15)
: context.isTablet
? Wrap(
children: [
for (
var i = 0;
i < recommendation.genres.length;
i++
)
Padding(
padding: const EdgeInsets.only(
left: 2,
right: 2,
bottom: 5,
),
child: SizedBox(
height: 30,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors
.grey
.withValues(
alpha: 0.2,
),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
5,
),
),
),
onPressed: null,
child: Text(
recommendation.genres[i],
style: TextStyle(
fontSize: 11.5,
color: context.isLight
? Colors.black
: Colors.white,
),
),
),
),
),
],
)
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment:
MainAxisAlignment.start,
children: [
for (
var i = 0;
i <
recommendation
.genres
.length;
i++
)
Padding(
padding:
const EdgeInsets.only(
left: 2,
right: 2,
bottom: 5,
),
child: SizedBox(
height: 30,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors
.grey
.withValues(
alpha: 0.2,
),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
5,
),
),
),
onPressed: () {},
child: Text(
recommendation
.genres[i],
style: TextStyle(
fontSize: 11.5,
color: context.isLight
? Colors.black
: Colors.white,
),
),
),
),
),
],
),
),
),
const SizedBox(width: 15),
Text(
"${recommendation.score}% ${l10n.recommendations_similar}",
style: TextStyle(
background: Paint()
..color = Theme.of(context)
.scaffoldBackgroundColor
.withValues(alpha: 0.75)
..strokeWidth = 30.0
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
);
}
return Center(child: Text(l10n.no_result));
},
),
),
);
}
Widget _buildTitle(String text, BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// Make sure that (constraints.maxWidth - (35 + 5)) is strictly positive.
final double availableWidth = constraints.maxWidth - (35 + 5);
final textPainter =
TextPainter(
text: TextSpan(text: text, style: const TextStyle(fontSize: 13)),
maxLines: 1,
textDirection: TextDirection.ltr,
)..layout(
maxWidth: availableWidth > 0 ? availableWidth : 1.0,
); // - Download icon size (download_page_widget.dart, Widget Build SizedBox width: 35)
final isOverflowing = textPainter.didExceedMaxLines;
if (isOverflowing) {
return SizedBox(
height: 20,
child: Marquee(
text: text,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
blankSpace: 40.0,
velocity: 30.0,
pauseAfterRound: const Duration(seconds: 1),
startPadding: 10.0,
),
);
} else {
return Text(
text,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
);
}
},
);
}
Widget _thumbnailPreview(BuildContext context, String? imageUrl) {
final imageProvider = CustomExtendedNetworkImageProvider(
toImgUrl(imageUrl ?? ""),
);
return Padding(
padding: const EdgeInsets.all(3),
child: GestureDetector(
onTap: () {
_openImage(context, imageProvider);
},
child: SizedBox(
width: 100,
height: 150,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(5)),
image: DecorationImage(image: imageProvider, fit: BoxFit.cover),
),
),
),
),
);
}
void _openImage(BuildContext context, ImageProvider imageProvider) {
showDialog(
context: context,
builder: (context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: PhotoViewGallery.builder(
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
itemCount: 1,
builder: (context, index) {
return PhotoViewGalleryPageOptions(
imageProvider: imageProvider,
minScale: PhotoViewComputedScale.contained,
maxScale: 2.0,
);
},
loadingBuilder: (context, event) {
return const ProgressCenter();
},
),
),
],
),
);
},
);
}
}
class SuperPrecalculationPolicy extends ExtentPrecalculationPolicy {
@override
bool shouldPrecalculateExtents(ExtentPrecalculationContext context) {
return context.numberOfItems < 100;
}
}

View file

@ -642,7 +642,7 @@ class _MangaChapterPageGalleryState
reverse: _isReverseHorizontal,
physics: const ClampingScrollPhysics(),
canScrollPage: (_) {
return _horizontalScaleValue == 1.0;
return true;
},
itemBuilder: (context, index) {
if (index < _uChapDataPreload.length &&
@ -695,9 +695,7 @@ class _MangaChapterPageGalleryState
reverse: _isReverseHorizontal,
physics: const ClampingScrollPhysics(),
canScrollPage: (gestureDetails) {
return gestureDetails != null
? !(gestureDetails.totalScale! > 1.0)
: true;
return true;
},
itemBuilder: (BuildContext context, int index) {
if (_uChapDataPreload[index]

View file

@ -1,15 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/modules/more/widgets/downloaded_only_widget.dart';
import 'package:mangayomi/modules/more/widgets/incognito_mode_widget.dart';
import 'package:mangayomi/modules/more/widgets/list_tile_widget.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
class MoreScreen extends StatelessWidget {
class MoreScreen extends ConsumerStatefulWidget {
const MoreScreen({super.key});
@override
ConsumerState<MoreScreen> createState() => MoreScreenState();
}
class MoreScreenState extends ConsumerState<MoreScreen> {
@override
Widget build(BuildContext context) {
final l10n = l10nLocalizations(context);
final hiddenItems = ref.watch(hideItemsStateProvider);
return Scaffold(
body: SingleChildScrollView(
child: Column(
@ -37,8 +46,17 @@ class MoreScreen extends StatelessWidget {
// onChanged: (value) {},
// ),
// ),
const DownloadedOnlyWidget(),
const IncognitoModeWidget(),
const Divider(),
if (hiddenItems.contains("/history"))
ListTileWidget(
onTap: () {
context.push('/history');
},
icon: Icons.history,
title: l10n!.history,
),
ListTileWidget(
onTap: () {
context.push('/downloadQueue');

View file

@ -0,0 +1,35 @@
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'algorithm_weights_state_provider.g.dart';
@riverpod
class AlgorithmWeightsState extends _$AlgorithmWeightsState {
@override
AlgorithmWeights build() {
return isar.settings.getSync(227)!.algorithmWeights ?? AlgorithmWeights();
}
void set(AlgorithmWeights value) {
final settings = isar.settings.getSync(227)!;
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings
..algorithmWeights = state
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
void setWeights({int? genre, int? setting, int? synopsis, int? theme}) {
set(
AlgorithmWeights(
genre: genre ?? state.genre,
setting: setting ?? state.setting,
synopsis: synopsis ?? state.synopsis,
theme: theme ?? state.theme,
),
);
}
}

View file

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'algorithm_weights_state_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$algorithmWeightsStateHash() =>
r'5c20cb9b195a73161b485e082ad024b138c3da9c';
/// See also [AlgorithmWeightsState].
@ProviderFor(AlgorithmWeightsState)
final algorithmWeightsStateProvider = AutoDisposeNotifierProvider<
AlgorithmWeightsState, AlgorithmWeights>.internal(
AlgorithmWeightsState.new,
name: r'algorithmWeightsStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$algorithmWeightsStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AlgorithmWeightsState = AutoDisposeNotifier<AlgorithmWeights>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -0,0 +1,24 @@
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'downloaded_only_state_provider.g.dart';
@riverpod
class DownloadedOnlyState extends _$DownloadedOnlyState {
@override
bool build() {
return isar.settings.getSync(227)!.downloadedOnlyMode ?? false;
}
void setDownloadedOnly(bool value) {
final settings = isar.settings.getSync(227)!;
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings
..downloadedOnlyMode = state
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}

View file

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'downloaded_only_state_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$downloadedOnlyStateHash() =>
r'09c451617c435ca59554546f5d3090d20c961bfe';
/// See also [DownloadedOnlyState].
@ProviderFor(DownloadedOnlyState)
final downloadedOnlyStateProvider =
AutoDisposeNotifierProvider<DownloadedOnlyState, bool>.internal(
DownloadedOnlyState.new,
name: r'downloadedOnlyStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$downloadedOnlyStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$DownloadedOnlyState = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -7,7 +7,7 @@ part 'incognito_mode_state_provider.g.dart';
class IncognitoModeState extends _$IncognitoModeState {
@override
bool build() {
return isar.settings.getSync(227)!.incognitoMode!;
return isar.settings.getSync(227)!.incognitoMode ?? false;
}
void setIncognitoMode(bool value) {

View file

@ -7,7 +7,7 @@ part of 'incognito_mode_state_provider.dart';
// **************************************************************************
String _$incognitoModeStateHash() =>
r'149c4dcbc434fb6efc883e196392320bdc7c0821';
r'3858256a820eef632d3df57533f2aad14f555b22';
/// See also [IncognitoModeState].
@ProviderFor(IncognitoModeState)

View file

@ -61,7 +61,6 @@ class _CustomNavigationSettingsState
[
"/more",
"/browse",
"/history",
].any((element) => element == navigation)
? null
: (value) {

View file

@ -1,14 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/more/providers/algorithm_weights_state_provider.dart';
import 'package:mangayomi/modules/more/settings/general/providers/general_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
class GeneralScreen extends ConsumerWidget {
class GeneralScreen extends ConsumerStatefulWidget {
const GeneralScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<GeneralScreen> createState() => _GeneralStateScreen();
}
class _GeneralStateScreen extends ConsumerState<GeneralScreen> {
int _genre = 0;
int _setting = 0;
int _synopsis = 0;
int _theme = 0;
@override
void initState() {
super.initState();
final algorithmWeights = ref.read(algorithmWeightsStateProvider);
_genre = algorithmWeights.genre!;
_setting = algorithmWeights.setting!;
_synopsis = algorithmWeights.synopsis!;
_theme = algorithmWeights.theme!;
}
@override
Widget build(BuildContext context) {
final l10n = l10nLocalizations(context);
final enableDiscordRpc = ref.watch(enableDiscordRpcStateProvider);
final hideDiscordRpcInIncognito = ref.watch(
@ -24,6 +48,199 @@ class GeneralScreen extends ConsumerWidget {
body: SingleChildScrollView(
child: Column(
children: [
Container(
margin: const EdgeInsets.all(20.0),
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
border: Border.all(width: 3.0, color: context.primaryColor),
borderRadius: BorderRadius.all(Radius.circular(5.0)),
),
child: Column(
children: [
Row(
children: [
Text(
context.l10n.recommendations_weights,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 20),
OutlinedButton.icon(
onPressed: () {
final defaultWeights = AlgorithmWeights();
setState(() {
_genre = defaultWeights.genre!;
_setting = defaultWeights.setting!;
_synopsis = defaultWeights.synopsis!;
_theme = defaultWeights.theme!;
});
ref
.read(algorithmWeightsStateProvider.notifier)
.set(defaultWeights);
},
label: Text(context.l10n.reset),
icon: const Icon(Icons.restore),
),
],
),
const SizedBox(height: 10),
Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.recommendations_weights_genre),
Text(
(_genre / 100).toStringAsFixed(2),
style: TextStyle(
fontSize: 11,
color: context.secondaryColor,
),
),
const SizedBox(height: 20),
SliderTheme(
data: SliderTheme.of(context).copyWith(
overlayShape: const RoundSliderOverlayShape(
overlayRadius: 5.0,
),
),
child: Slider.adaptive(
min: 0,
max: 100,
value: _genre.toDouble(),
onChanged: (value) {
HapticFeedback.vibrate();
setState(() {
_genre = value.toInt();
});
},
onChangeEnd: (value) => ref
.read(algorithmWeightsStateProvider.notifier)
.setWeights(genre: _genre),
),
),
],
),
),
const SizedBox(height: 10),
Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.recommendations_weights_setting),
Text(
(_setting / 100).toStringAsFixed(2),
style: TextStyle(
fontSize: 11,
color: context.secondaryColor,
),
),
const SizedBox(height: 20),
SliderTheme(
data: SliderTheme.of(context).copyWith(
overlayShape: const RoundSliderOverlayShape(
overlayRadius: 5.0,
),
),
child: Slider.adaptive(
min: 0,
max: 100,
value: _setting.toDouble(),
onChanged: (value) {
HapticFeedback.vibrate();
setState(() {
_setting = value.toInt();
});
},
onChangeEnd: (value) => ref
.read(algorithmWeightsStateProvider.notifier)
.setWeights(setting: _setting),
),
),
],
),
),
const SizedBox(height: 10),
Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.recommendations_weights_synopsis),
Text(
(_synopsis / 100).toStringAsFixed(2),
style: TextStyle(
fontSize: 11,
color: context.secondaryColor,
),
),
const SizedBox(height: 20),
SliderTheme(
data: SliderTheme.of(context).copyWith(
overlayShape: const RoundSliderOverlayShape(
overlayRadius: 5.0,
),
),
child: Slider.adaptive(
min: 0,
max: 100,
value: _synopsis.toDouble(),
onChanged: (value) {
HapticFeedback.vibrate();
setState(() {
_synopsis = value.toInt();
});
},
onChangeEnd: (value) => ref
.read(algorithmWeightsStateProvider.notifier)
.setWeights(synopsis: _synopsis),
),
),
],
),
),
const SizedBox(height: 10),
Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.recommendations_weights_theme),
Text(
(_theme / 100).toStringAsFixed(2),
style: TextStyle(
fontSize: 11,
color: context.secondaryColor,
),
),
const SizedBox(height: 20),
SliderTheme(
data: SliderTheme.of(context).copyWith(
overlayShape: const RoundSliderOverlayShape(
overlayRadius: 5.0,
),
),
child: Slider.adaptive(
min: 0,
max: 100,
value: _theme.toDouble(),
onChanged: (value) {
HapticFeedback.vibrate();
setState(() {
_theme = value.toInt();
});
},
onChangeEnd: (value) => ref
.read(algorithmWeightsStateProvider.notifier)
.setWeights(theme: _theme),
),
),
],
),
),
],
),
),
SwitchListTile(
value: enableDiscordRpc,
title: Text(l10n.enable_discord_rpc),

View file

@ -116,7 +116,9 @@ class SyncScreen extends ConsumerWidget {
title: Text(l10n.sync_auto),
subtitle: Text(
autoSyncOptions.entries
.where((o) => o.value == syncPreference.autoSyncFrequency)
.where(
(o) => o.value == syncPreference.autoSyncFrequency,
)
.first
.key,
style: TextStyle(
@ -150,29 +152,35 @@ class SyncScreen extends ConsumerWidget {
SwitchListTile(
value: syncPreference.syncHistories,
title: Text(context.l10n.sync_enable_histories),
onChanged: syncPreference.syncOn ? (value) {
ref
.read(SynchingProvider(syncId: 1).notifier)
.setSyncHistories(value);
} : null,
onChanged: syncPreference.syncOn
? (value) {
ref
.read(SynchingProvider(syncId: 1).notifier)
.setSyncHistories(value);
}
: null,
),
SwitchListTile(
value: syncPreference.syncUpdates,
title: Text(context.l10n.sync_enable_updates),
onChanged: syncPreference.syncOn ? (value) {
ref
.read(SynchingProvider(syncId: 1).notifier)
.setSyncUpdates(value);
} : null,
onChanged: syncPreference.syncOn
? (value) {
ref
.read(SynchingProvider(syncId: 1).notifier)
.setSyncUpdates(value);
}
: null,
),
SwitchListTile(
value: syncPreference.syncSettings,
title: Text(context.l10n.sync_enable_settings),
onChanged: syncPreference.syncOn ? (value) {
ref
.read(SynchingProvider(syncId: 1).notifier)
.setSyncSettings(value);
} : null,
onChanged: syncPreference.syncOn
? (value) {
ref
.read(SynchingProvider(syncId: 1).notifier)
.setSyncSettings(value);
}
: null,
),
Padding(
padding: const EdgeInsets.only(
@ -288,6 +296,40 @@ class SyncScreen extends ConsumerWidget {
Text(l10n.sync_button_sync),
],
),
const SizedBox(width: 20),
Column(
children: [
IconButton(
onPressed: !syncPreference.syncOn || !isLogged
? null
: () => _showConfirmDialog(context, ref, true),
icon: Icon(
Icons.file_upload_outlined,
color: !syncPreference.syncOn || !isLogged
? context.secondaryColor
: context.primaryColor,
),
),
Text(l10n.sync_button_upload),
],
),
const SizedBox(width: 20),
Column(
children: [
IconButton(
onPressed: !syncPreference.syncOn || !isLogged
? null
: () => _showConfirmDialog(context, ref, false),
icon: Icon(
Icons.file_download_outlined,
color: !syncPreference.syncOn || !isLogged
? context.secondaryColor
: context.primaryColor,
),
),
Text(l10n.sync_button_download),
],
),
],
),
const SizedBox(height: 20),
@ -298,172 +340,220 @@ class SyncScreen extends ConsumerWidget {
),
);
}
}
void _showDialogLogin(
BuildContext context,
WidgetRef ref,
SyncPreference syncPreference,
) {
final serverController = TextEditingController(text: syncPreference.server);
final emailController = TextEditingController(text: syncPreference.email);
final passwordController = TextEditingController();
String server = "";
String email = "";
String password = "";
String errorMessage = "";
bool isLoading = false;
bool obscureText = true;
final l10n = l10nLocalizations(context)!;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
Future<void> _showConfirmDialog(
BuildContext context,
WidgetRef ref,
bool isUpload,
) async {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(
l10n.login_into("SyncServer"),
style: const TextStyle(fontSize: 30),
content: Text(
isUpload
? context.l10n.sync_button_upload_info
: context.l10n.sync_button_download_info,
style: TextStyle(fontWeight: FontWeight.bold),
),
content: SizedBox(
height: 400,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: serverController,
autofocus: true,
onChanged: (value) => setState(() {
server = value;
}),
decoration: InputDecoration(
hintText: l10n.sync_server,
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(),
),
),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.cancel),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: emailController,
autofocus: true,
onChanged: (value) => setState(() {
email = value;
}),
decoration: InputDecoration(
hintText: l10n.email_adress,
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(),
),
),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: passwordController,
obscureText: obscureText,
onChanged: (value) => setState(() {
password = value;
}),
decoration: InputDecoration(
hintText: l10n.sync_password,
suffixIcon: IconButton(
onPressed: () => setState(() {
obscureText = !obscureText;
}),
icon: Icon(
obscureText
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
),
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(),
),
),
),
),
const SizedBox(height: 10),
Text(errorMessage, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: context.width(1),
height: 50,
child: ElevatedButton(
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
final res = await ref
.read(syncServerProvider(syncId: 1).notifier)
.login(l10n, server, email, password);
if (!res.$1) {
setState(() {
isLoading = false;
errorMessage = res.$2;
});
} else {
if (context.mounted) {
Navigator.pop(context);
}
}
},
child: isLoading
? const CircularProgressIndicator()
: Text(l10n.login),
),
),
const SizedBox(width: 15),
ElevatedButton(
onPressed: () {
ref
.read(syncServerProvider(syncId: 1).notifier)
.startSync(
context.l10n,
false,
upload: isUpload,
download: !isUpload,
);
Navigator.pop(context);
},
child: Text(context.l10n.dialog_confirm),
),
],
),
),
],
);
},
),
);
);
}
void _showDialogLogin(
BuildContext context,
WidgetRef ref,
SyncPreference syncPreference,
) {
final serverController = TextEditingController(text: syncPreference.server);
final emailController = TextEditingController(text: syncPreference.email);
final passwordController = TextEditingController();
String server = "";
String email = "";
String password = "";
String errorMessage = "";
bool isLoading = false;
bool obscureText = true;
final l10n = l10nLocalizations(context)!;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text(
l10n.login_into("SyncServer"),
style: const TextStyle(fontSize: 30),
),
content: SizedBox(
height: 400,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: serverController,
autofocus: true,
onChanged: (value) => setState(() {
server = value;
}),
decoration: InputDecoration(
hintText: l10n.sync_server,
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(),
),
),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: emailController,
autofocus: true,
onChanged: (value) => setState(() {
email = value;
}),
decoration: InputDecoration(
hintText: l10n.email_adress,
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(),
),
),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: passwordController,
obscureText: obscureText,
onChanged: (value) => setState(() {
password = value;
}),
decoration: InputDecoration(
hintText: l10n.sync_password,
suffixIcon: IconButton(
onPressed: () => setState(() {
obscureText = !obscureText;
}),
icon: Icon(
obscureText
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
),
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(),
),
),
),
),
const SizedBox(height: 10),
Text(errorMessage, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: context.width(1),
height: 50,
child: ElevatedButton(
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
final res = await ref
.read(
syncServerProvider(syncId: 1).notifier,
)
.login(l10n, server, email, password);
if (!res.$1) {
setState(() {
isLoading = false;
errorMessage = res.$2;
});
} else {
if (context.mounted) {
Navigator.pop(context);
}
}
},
child: isLoading
? const CircularProgressIndicator()
: Text(l10n.login),
),
),
),
],
),
),
);
},
),
);
}
}

View file

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/more/providers/downloaded_only_state_provider.dart';
import 'package:mangayomi/modules/more/widgets/list_tile_widget.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
class DownloadedOnlyWidget extends ConsumerWidget {
const DownloadedOnlyWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = l10nLocalizations(context);
final downloadedOnly = ref.watch(downloadedOnlyStateProvider);
return ListTileWidget(
onTap: () => ref
.read(downloadedOnlyStateProvider.notifier)
.setDownloadedOnly(!downloadedOnly),
icon: Icons.cloud_off_outlined,
subtitle: l10n!.downloaded_only_description,
title: l10n.downloaded_only,
trailing: Switch(
value: downloadedOnly,
onChanged: (value) => ref
.read(downloadedOnlyStateProvider.notifier)
.setDownloadedOnly(value),
),
);
}
}

View file

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
/// Bar, that appears at the bottom of the screen when long-pressing (selecting)
/// a Manga/Anime/Novel or Chapter/Episode
class BottomSelectBar extends StatelessWidget {
final bool isVisible;
final List<BottomSelectButton> actions;
const BottomSelectBar({
super.key,
required this.isVisible,
required this.actions,
});
@override
Widget build(BuildContext context) {
return AnimatedContainer(
curve: Curves.easeIn,
decoration: BoxDecoration(
color: context.primaryColor.withValues(alpha: 0.2),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
duration: const Duration(milliseconds: 100),
height: isVisible ? 70 : 0,
width: context.width(1),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: actions,
),
);
}
}
/// Button for the BottomSelectBar
class BottomSelectButton extends StatelessWidget {
final Widget icon;
final VoidCallback onPressed;
const BottomSelectButton({
super.key,
required this.icon,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: onPressed,
child: icon,
),
),
);
}
}

View file

@ -7,6 +7,7 @@ import 'package:mangayomi/models/category.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
import 'package:mangayomi/modules/library/widgets/list_tile_manga_category.dart';
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/modules/manga/detail/widgets/chapter_filter_list_tile_widget.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
@ -151,7 +152,7 @@ void showCategorySelectionDialog({
if (isBulk) {
ref.read(mangasListStateProvider.notifier).clear();
ref
.read(isLongPressedMangaStateProvider.notifier)
.read(isLongPressedStateProvider.notifier)
.update(false);
}
});

View file

@ -3,6 +3,7 @@ import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/models/track_search.dart';
@ -13,6 +14,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/manga/detail/widgets/recommendation_screen.dart';
import 'package:mangayomi/modules/more/data_and_storage/create_backup.dart';
import 'package:mangayomi/modules/more/data_and_storage/data_and_storage.dart';
import 'package:mangayomi/modules/more/settings/appearance/custom_navigation_settings.dart';
@ -174,9 +176,9 @@ class RouterNotifier extends ChangeNotifier {
name: "extension_detail",
builder: (source) => ExtensionDetail(source: source),
),
_genericRoute<ItemType>(
_genericRoute<(String?, ItemType)>(
name: "globalSearch",
builder: (itemType) => GlobalSearchScreen(itemType: itemType),
builder: (data) => GlobalSearchScreen(search: data.$1, itemType: data.$2),
),
_genericRoute(name: "about", child: const AboutScreen()),
_genericRoute(name: "track", child: const TrackScreen()),
@ -246,6 +248,14 @@ class RouterNotifier extends ChangeNotifier {
name: "migrate/tracker",
builder: (data) => MigrationScreen(manga: data.$1, trackSearch: data.$2),
),
_genericRoute<(String, ItemType, AlgorithmWeights)>(
name: "recommendations",
builder: (data) => RecommendationScreen(
name: data.$1,
itemType: data.$2,
algorithmWeights: data.$3,
),
),
];
GoRoute _genericRoute<T>({

View file

@ -6,7 +6,7 @@ part of 'aniskip.dart';
// RiverpodGenerator
// **************************************************************************
String _$aniSkipHash() => r'2e5d19b025a2207ff64da7bf7908450ea9e5ff8c';
String _$aniSkipHash() => r'887869b54e2e151633efd46da83bde845e14f421';
/// See also [AniSkip].
@ProviderFor(AniSkip)

View file

@ -0,0 +1,146 @@
import 'dart:convert';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/services/http/m_client.dart';
Future<List<RecommendationResult>?> getRecommendations(
String name,
ItemType itemType,
AlgorithmWeights algorithmWeights,
) async {
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
try {
final mediaId = await _getSuggest(http, name, itemType);
return _getRecommendation(
http,
mediaId ?? name,
itemType,
algorithmWeights,
);
} catch (_) {
return null;
}
}
Future<List<RecommendationResult>?> _getRecommendation(
InterceptedClient http,
String mediaId,
ItemType itemType,
AlgorithmWeights algorithmWeights,
) async {
final url =
"https://anibrain.ai/api/-/recommender/recs/${itemType != ItemType.anime ? "manga" : "anime"}";
final res = await http.get(
Uri.parse(url),
headers: {
"priority": "u=1, i",
"Referer": "https://anibrain.ai/",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
},
params: {
"filterCountry": '[]',
"filterFormat": '${_fillerType(itemType).map((e) => '"$e"').toList()}',
"filterGenre": '{}',
"filterTag": '{"max":{},"min":{}}',
"filterRelease": '[1930,${DateTime.now().year}]',
"filterScore": 0,
"algorithmWeights": _algorithmWeights(algorithmWeights),
"mediaId": mediaId,
"mediaType": _mediaType(itemType),
"adult": false,
"page": 1,
},
);
final data = json.decode(res.body) as Map<String, dynamic>;
return (data["data"] as List?)
?.map((e) => RecommendationResult.fromJson(e))
.toList();
}
Future<String?> _getSuggest(
InterceptedClient http,
String name,
ItemType itemType,
) async {
final url =
"https://anibrain.ai/api/-/recommender/autosuggest?searchValue=$name&mediaType=${_mediaType(itemType)}&adult=false";
final res = await http.get(
Uri.parse(url),
headers: {
"priority": "u=1, i",
"Referer": "https://anibrain.ai/recommender/manga",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
},
);
final data = json.decode(res.body) as Map<String, dynamic>;
final list = (data["data"] as List?)?.map((e) => e["id"]);
return list?.firstOrNull;
}
String _algorithmWeights(AlgorithmWeights algorithmWeights) {
final genre = ((algorithmWeights.genre ?? 30) / 100).toStringAsFixed(2);
final setting = ((algorithmWeights.setting ?? 15) / 100).toStringAsFixed(2);
final synopsis = ((algorithmWeights.synopsis ?? 40) / 100).toStringAsFixed(2);
final theme = ((algorithmWeights.theme ?? 20) / 100).toStringAsFixed(2);
return '{"genre":$genre,"setting":$setting,"synopsis":$synopsis,"theme":$theme}';
}
String _mediaType(ItemType itemType) {
return switch (itemType) {
ItemType.manga => "MANGA",
ItemType.anime => "ANIME",
ItemType.novel => "NOVEL",
};
}
List<String> _fillerType(ItemType itemType) {
return switch (itemType) {
ItemType.manga => ["MANGA"],
ItemType.anime => ["movie", "ona", "tv"],
ItemType.novel => ["NOVEL"],
};
}
class RecommendationResult {
final String id;
final int? anilistId;
final int? myanimelistId;
final int score;
final String? titleRomaji;
final String? titleEnglish;
final String? titleNative;
final String? description;
final List<String> imgURLs;
final List<String> genres;
RecommendationResult({
required this.id,
this.anilistId,
this.myanimelistId,
required this.score,
this.titleRomaji,
this.titleEnglish,
this.titleNative,
this.description,
required this.imgURLs,
required this.genres,
});
factory RecommendationResult.fromJson(Map<String, dynamic> json) {
return RecommendationResult(
id: json["id"],
anilistId: json["anilistId"],
myanimelistId: json["myanimelistId"],
score: json["score"],
titleRomaji: json["titleRomaji"],
titleEnglish: json["titleEnglish"],
titleNative: json["titleNative"],
description: json["description"],
imgURLs: json["imgURLs"]?.cast<String>() ?? [],
genres: json["genres"]?.cast<String>() ?? [],
);
}
}

View file

@ -41,7 +41,7 @@ class SyncServer extends _$SyncServer {
String username,
String password,
) async {
server = server[server.length - 1] == '/'
server = server.isNotEmpty && server[server.length - 1] == '/'
? server.substring(0, server.length - 1)
: server;
try {
@ -67,7 +67,12 @@ class SyncServer extends _$SyncServer {
}
}
Future<void> startSync(AppLocalizations l10n, bool silent) async {
Future<void> startSync(
AppLocalizations l10n,
bool silent, {
bool upload = false,
bool download = false,
}) async {
if (!silent) {
botToast(l10n.sync_starting, second: 500);
}
@ -75,27 +80,46 @@ class SyncServer extends _$SyncServer {
final syncPreference = ref.read(synchingProvider(syncId: syncId));
final syncNotifier = ref.read(synchingProvider(syncId: syncId).notifier);
final resultManga = await _syncManga(l10n, syncNotifier);
final resultManga = await _syncManga(
l10n,
syncNotifier,
download: download,
upload: upload,
);
if (!resultManga) {
botToast(l10n.sync_failed, second: 5);
return;
}
if (syncPreference.syncHistories) {
final resultHistory = await _syncHistory(l10n, syncNotifier);
final resultHistory = await _syncHistory(
l10n,
syncNotifier,
download: download,
upload: upload,
);
if (!resultHistory) {
botToast(l10n.sync_failed, second: 5);
return;
}
}
if (syncPreference.syncUpdates) {
final resultUpdate = await _syncUpdate(l10n, syncNotifier);
final resultUpdate = await _syncUpdate(
l10n,
syncNotifier,
download: download,
upload: upload,
);
if (!resultUpdate) {
botToast(l10n.sync_failed, second: 5);
return;
}
}
if (syncPreference.syncSettings) {
final resultSettings = await _syncSettings(l10n);
final resultSettings = await _syncSettings(
l10n,
download: download,
upload: upload,
);
if (!resultSettings) {
botToast(l10n.sync_failed, second: 5);
return;
@ -111,8 +135,13 @@ class SyncServer extends _$SyncServer {
}
}
Future<bool> _syncManga(AppLocalizations l10n, Synching syncNotifier) async {
final mangaData = _getMangaData();
Future<bool> _syncManga(
AppLocalizations l10n,
Synching syncNotifier, {
bool upload = false,
bool download = false,
}) async {
final mangaData = _getMangaData(upload: upload, download: download);
final accessToken = _getAccessToken();
var response = await http.post(
Uri.parse('${_getServer()}$_syncMangaUrl'),
@ -127,11 +156,20 @@ class SyncServer extends _$SyncServer {
return false;
}
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
await _upsertCategories(jsonData, syncNotifier);
await _upsertManga(jsonData, syncNotifier);
await _upsertChapters(jsonData, syncNotifier);
await _upsertTracks(jsonData, syncNotifier);
if (!upload) {
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
await _upsertCategories(jsonData, syncNotifier);
await _upsertManga(jsonData, syncNotifier);
await _upsertChapters(jsonData, syncNotifier);
await _upsertTracks(jsonData, syncNotifier);
} else {
await syncNotifier.clearChangedParts([
ActionType.removeCategory,
ActionType.removeItem,
ActionType.removeChapter,
ActionType.removeTrack,
], true);
}
syncNotifier.setLastSyncManga(DateTime.now().millisecondsSinceEpoch);
@ -140,9 +178,11 @@ class SyncServer extends _$SyncServer {
Future<bool> _syncHistory(
AppLocalizations l10n,
Synching syncNotifier,
) async {
final historyData = _getHistoryData();
Synching syncNotifier, {
bool upload = false,
bool download = false,
}) async {
final historyData = _getHistoryData(upload: upload, download: download);
final accessToken = _getAccessToken();
var response = await http.post(
Uri.parse('${_getServer()}$_syncHistoryUrl'),
@ -157,16 +197,25 @@ class SyncServer extends _$SyncServer {
return false;
}
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
await _upsertHistories(jsonData, syncNotifier);
if (!upload) {
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
await _upsertHistories(jsonData, syncNotifier);
} else {
await syncNotifier.clearChangedParts([ActionType.removeHistory], true);
}
syncNotifier.setLastSyncHistory(DateTime.now().millisecondsSinceEpoch);
return true;
}
Future<bool> _syncUpdate(AppLocalizations l10n, Synching syncNotifier) async {
final updateData = _getUpdateData();
Future<bool> _syncUpdate(
AppLocalizations l10n,
Synching syncNotifier, {
bool upload = false,
bool download = false,
}) async {
final updateData = _getUpdateData(upload: upload, download: download);
final accessToken = _getAccessToken();
var response = await http.post(
Uri.parse('${_getServer()}$_syncUpdateUrl'),
@ -181,16 +230,24 @@ class SyncServer extends _$SyncServer {
return false;
}
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
await _upsertUpdates(jsonData, syncNotifier);
if (!upload) {
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
await _upsertUpdates(jsonData, syncNotifier);
} else {
await syncNotifier.clearChangedParts([ActionType.removeUpdate], true);
}
syncNotifier.setLastSyncUpdate(DateTime.now().millisecondsSinceEpoch);
return true;
}
Future<bool> _syncSettings(AppLocalizations l10n) async {
final settingsData = _getSettingsData();
Future<bool> _syncSettings(
AppLocalizations l10n, {
bool upload = false,
bool download = false,
}) async {
final settingsData = _getSettingsData(download: download);
final accessToken = _getAccessToken();
var response = await http.post(
Uri.parse('${_getServer()}$_syncSettingsUrl'),
@ -205,8 +262,10 @@ class SyncServer extends _$SyncServer {
return false;
}
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
await _upsertSettings(jsonData);
if (!upload) {
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
await _upsertSettings(jsonData);
}
return true;
}
@ -424,36 +483,61 @@ class SyncServer extends _$SyncServer {
});
}
String _getMangaData() {
String _getMangaData({bool upload = false, bool download = false}) {
Map<String, dynamic> data = {};
data["categories"] = _getCategories();
data["deleted_categories"] = _getDeletedObjects(ActionType.removeCategory);
data["manga"] = _getManga();
data["deleted_manga"] = _getDeletedObjects(ActionType.removeItem);
data["chapters"] = _getChapters();
data["deleted_chapters"] = _getDeletedObjects(ActionType.removeChapter);
data["tracks"] = _getTracks();
data["deleted_tracks"] = _getDeletedObjects(ActionType.removeTrack);
data["categories"] = download ? [] : _getCategories();
data["deleted_categories"] = download
? []
: _getDeletedObjects(ActionType.removeCategory);
data["manga"] = download ? [] : _getManga();
data["deleted_manga"] = download
? []
: _getDeletedObjects(ActionType.removeItem);
data["chapters"] = download ? [] : _getChapters();
data["deleted_chapters"] = download
? []
: _getDeletedObjects(ActionType.removeChapter);
data["tracks"] = download ? [] : _getTracks();
data["deleted_tracks"] = download
? []
: _getDeletedObjects(ActionType.removeTrack);
if (upload) {
data["resetAll"] = true;
}
return jsonEncode(data);
}
String _getHistoryData() {
String _getHistoryData({bool upload = false, bool download = false}) {
Map<String, dynamic> data = {};
data["histories"] = _getHistories();
data["deleted_histories"] = _getDeletedObjects(ActionType.removeHistory);
data["histories"] = download ? [] : _getHistories();
data["deleted_histories"] = download
? []
: _getDeletedObjects(ActionType.removeHistory);
if (upload) {
data["resetAll"] = true;
}
return jsonEncode(data);
}
String _getUpdateData() {
String _getUpdateData({bool upload = false, bool download = false}) {
Map<String, dynamic> data = {};
data["updates"] = _getUpdates();
data["deleted_updates"] = _getDeletedObjects(ActionType.removeUpdate);
data["updates"] = download ? [] : _getUpdates();
data["deleted_updates"] = download
? []
: _getDeletedObjects(ActionType.removeUpdate);
if (upload) {
data["resetAll"] = true;
}
return jsonEncode(data);
}
String _getSettingsData() {
String _getSettingsData({bool download = false}) {
Map<String, dynamic> data = {};
data["settings"] = isar.settings.getSync(227)!..updatedAt ??= DateTime.now().millisecondsSinceEpoch..cookiesList = [];
if (!download) {
data["settings"] = isar.settings.getSync(227)!
..updatedAt ??= DateTime.now().millisecondsSinceEpoch
..cookiesList = [];
}
return jsonEncode(data);
}

View file

@ -6,7 +6,7 @@ part of 'anilist.dart';
// RiverpodGenerator
// **************************************************************************
String _$anilistHash() => r'fafb964252b3a5741e981cb8c2f0f2090b3b86ae';
String _$anilistHash() => r'c786a526fdacc875e4a7e00886b2bda546eafeae';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -160,7 +160,7 @@ class Kitsu extends _$Kitsu {
mediaId: jsonRes['id'],
summary: jsonRes['synopsis'] ?? "",
totalChapter: (jsonRes[totalChapter] ?? 0),
coverUrl: jsonRes['posterImage']['original'] ?? "",
coverUrl: jsonRes['posterImage']?['original'] ?? "",
title: jsonRes['canonicalTitle'],
startDate: "",
publishingType: (jsonRes["subtype"] ?? ""),
@ -192,22 +192,23 @@ class Kitsu extends _$Kitsu {
final mediaId = jsonRes['id'] is String
? int.parse(jsonRes['id'])
: jsonRes['id'];
final score = jsonRes['attributes']['averageRating'] is String
? double.parse(jsonRes['attributes']['averageRating'])
: jsonRes['attributes']['averageRating'];
final attributes = jsonRes['attributes'];
final score = attributes['averageRating'] is String
? double.parse(attributes['averageRating'])
: attributes['averageRating'];
return TrackSearch(
libraryId: mediaId,
syncId: syncId,
trackingUrl: _mediaUrl(isManga ? 'manga' : 'anime', mediaId),
mediaId: mediaId,
summary: jsonRes['attributes']['synopsis'] ?? "",
totalChapter: (jsonRes['attributes'][totalChapter] ?? 0),
coverUrl: jsonRes['attributes']['posterImage']['original'] ?? "",
title: jsonRes['attributes']['canonicalTitle'],
summary: attributes['synopsis'] ?? "",
totalChapter: (attributes[totalChapter] ?? 0),
coverUrl: attributes['posterImage']?['original'] ?? "",
title: attributes['canonicalTitle'],
startDate: "",
score: score,
publishingType: (jsonRes['attributes']['subtype'] ?? ""),
publishingStatus: jsonRes['attributes']['endDate'] == null
publishingType: (attributes['subtype'] ?? ""),
publishingStatus: attributes['endDate'] == null
? "Publishing"
: "Finished",
);
@ -250,7 +251,7 @@ class Kitsu extends _$Kitsu {
trackingUrl: _mediaUrl(type, id),
summary: included['synopsis'] ?? "",
totalChapter: included[totalChapter] ?? 0,
coverUrl: included['posterImage']['original'] ?? "",
coverUrl: included['posterImage']?['original'] ?? "",
title: included['canonicalTitle'],
startDate: "",
publishingType: (included["subtype"] ?? ""),

View file

@ -159,7 +159,7 @@ class MyAnimeList extends _$MyAnimeList {
mediaId: res["id"],
summary: res["synopsis"] ?? "",
totalChapter: res[contentUnit],
coverUrl: res["main_picture"]["large"] ?? "",
coverUrl: res["main_picture"]?["large"] ?? "",
title: res["title"],
startDate: res["start_date"] ?? "",
publishingType: res["media_type"].toString().replaceAll("_", " "),
@ -197,7 +197,7 @@ class MyAnimeList extends _$MyAnimeList {
mediaId: node["id"],
summary: node["synopsis"] ?? "",
totalChapter: node[contentUnit],
coverUrl: node["main_picture"]["large"] ?? "",
coverUrl: node["main_picture"]?["large"] ?? "",
title: node["title"],
score: (node["mean"] as num?)?.toDouble(),
startDate: node["start_date"] ?? "",
@ -237,7 +237,7 @@ class MyAnimeList extends _$MyAnimeList {
mediaId: node["id"],
summary: node["synopsis"] ?? "",
totalChapter: node[contentUnit],
coverUrl: node["main_picture"]["large"] ?? "",
coverUrl: node["main_picture"]?["large"] ?? "",
title: node["title"],
score: (node["mean"] as num?)?.toDouble(),
startDate: node["start_date"] ?? "",