From edaf975e21cf81d46115e506775005a9d18a9f49 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 17 Mar 2026 02:34:25 +0100 Subject: [PATCH 01/11] Translations update from Hosted Weblate (#1523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (208 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/zh_Hant/ * Translated using Weblate (Italian) Currently translated at 94.7% (197 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/it/ * Translated using Weblate (Italian) Currently translated at 95.1% (198 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/it/ * Translated using Weblate (Arabic) Currently translated at 74.5% (155 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ar/ * Translated using Weblate (Korean) Currently translated at 30.7% (64 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ko/ * Translated using Weblate (Russian) Currently translated at 100.0% (208 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ru/ * Translated using Weblate (Italian) Currently translated at 95.1% (198 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/it/ * Translated using Weblate (Arabic) Currently translated at 74.5% (155 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ar/ * Translated using Weblate (Korean) Currently translated at 30.7% (64 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ko/ * Translated using Weblate (Italian) Currently translated at 95.6% (199 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/it/ * Translated using Weblate (Arabic) Currently translated at 74.5% (155 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ar/ * Translated using Weblate (Arabic) Currently translated at 74.5% (155 of 208 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ar/ --------- Co-authored-by: ɴᴇᴋᴏ Co-authored-by: Giulia Co-authored-by: pasquale ruotolo Co-authored-by: ZiadSenior Co-authored-by: 안세훈 Co-authored-by: amigo browser Co-authored-by: Cuong Tran --- .../commonMain/moko-resources/ar/strings.xml | 44 ++++++- .../commonMain/moko-resources/it/strings.xml | 107 +++++++++++++++--- .../commonMain/moko-resources/ko/strings.xml | 74 ++++++++++-- .../commonMain/moko-resources/ru/strings.xml | 2 + .../moko-resources/zh-rTW/strings.xml | 11 ++ 5 files changed, 210 insertions(+), 28 deletions(-) diff --git a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml index 9a4930c69..e85a1a754 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml @@ -1,6 +1,6 @@ - البحث عن مصدر + البحث داخل المصدر تبديل NSFW فقط وضع التحديد بالجملة الأسئلة الشائعة والأدلة @@ -11,7 +11,7 @@ 6 ساعات 12 ساعة 1 يوم - إذا تم تعيين إلى \"يدوي\"، إما تحديث كل مانغا أو باستخدام \"فهرس التنزيلات غير صحيح\" في \"المتقدم\" + إذا تم تعيين إلى \"يدوي\"، إما قم بتحديث كل مانجا أو باستخدام \"إعادة الفهرسة للتنزيلات\" في \"المتقدم\" تنظيف التنزيلات غير صالحة العثور على جميع التنزيلات والملفات والمجلدات التي لم يتم حفظها في مكتبتك وإزالتها تم تنظيف التنزيلات غير صالحة @@ -120,6 +120,44 @@ نسخ الصفحة الثانية إلى الحافظة نسخ الصفحة المدمجة إلى الحافظة عدم التحميل التلقائي للتحديث - إزالة المدخلات الأصلية من المكتبة؟ + إزالة العناصر الأصلية من المكتبة؟ الدعم: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + فتح مجلد + تحديث + مانجا غير مكتملة + فصل غير مكتملة + عناصر خارج المكتبة + نطاق نقر أصغر + فشل فتح المجلد + ضم الأصابع للتكبير + إلغاء تفعيل التكبير + وضع التكبير للصور العريضة + تكبير مزدوج + تكبير ذكي للشاشات العريضة + تنفيذ التكبير الذكي ل \"شرائح طويلة مع فواصل\" + 16:9 + 20:9 + 4:3 + 3:2 + محاذاة الغلاف للأعلى + إظهار الغلاف محاذي للأعلى بجانب بيانات المانجا + دمار + لن يتم عرض العناصر في الفئات المستبعدة حتى لو كانت موجودة أيضًا في الفئات المشمولة. + جاري التحديث + عرض الفئات الفارغة أثناء البحث/التصفية + مزامنة المانجا عند إضافتها إلى المكتبة + يقوم تلقائيًا بجلب البيانات الوصفية وقائمة الفصول الخاصة بمانجا جديدة عند إضافتها إلى المكتبة + فتح قائمة المزامنة عند الإضافة للمكتبة + WebDAV + رابط سيرفر WebDAV + ادخل رابط سيرفر WebDAV + اسم المستخدم + ادخل اسم مستخدم WebDAV الخاص بك + كلمة المرور + ادخل كلمة المرور حساب WebDAV الخاص بك + مجلد بيانات WebDAV + سيقوم التطبيق بتخزين البيانات في هذا المجلد + البيانات التي تم تنزيلها + الفصول والسجل من قاعدة البيانات + تثبيت يدوي diff --git a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml index 0d1cc6c70..b0802f505 100644 --- a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml @@ -1,8 +1,8 @@ - Cerca nella sorgente + Cerca nella fonte Attiva/disattiva solo NSFW - Modalità selezione Bulk + Modalità selezione multipla FAQ e guide Intervallo di rinnovo download cache Manuale @@ -11,34 +11,34 @@ 6 ore 12 ore 1 giorno - Se impostato su \"manual\", aggiorna ogni manga o usando l\'azione \"Invalidate downloads index\" in \"Avanzate\" + Se impostato su \"manual\", aggiorna ogni manga o usa l\'azione \"Invalidate downloads index\" in \"Avanzate\" Pulisci download non validi Trova e rimuovi tutti i download, file, cartelle che non vengono salvate nella tua libreria Scaricamenti non validi puliti Evitare di utilizzare Installer Privato perché non è ancora stabile, porterà a un avvio e riavvio dell\'app molto lento. - Cosa succederà (presto) - Consenti di farlo - Salta esso + In arrivo (a breve) + Consenti + Salta Consenti tutto Salta tutto Prova ora - In arrivo (presto) aggiornamenti! + Aggiornamenti in arrivo! Modifiche - Più come questi... + Opere simili... Troppe fonti nel tuo feed, non puoi aggiungere più del limite (20) Attuale: %1$s Tema basato sulla copertina Punto Tonale Neutro - Vibrante + Acceso Espressivo Arcobaleno Insalata Di Frutta Monocromatico Fedeltà Contenuto - Cerca la sorgente - Ricerca libreria + Cerca fonte + Cerca nella libreria Cloudflare Zucchero filato Matrice @@ -55,8 +55,8 @@ Sposta in alto Sposta in basso Ricarica il colore della copertina della libreria - Ricerche salvate (Si prega di aggiungere al Feed) - Ricerche salvate (Si prega di eliminare) + Ricerche salvate (Tieni premuto per aggiungere al feed) + Ricerche salvate (Tieni premuto per eliminare) Icona sorgente Usa icona lingua Conferma Colore @@ -78,7 +78,7 @@ Vuoi ordinare i feed in modo alfabetico? Stile tema personalizzato Stile tema basato su copertina - Scegli un colore del seme per il tema personalizzato + Scegli un colore base per il tema personalizzato Casuale Tramonto Lampone @@ -122,5 +122,82 @@ Superare Gomma da masticare Springfield - installazione manuale + Installazione manuale + Apri cartella + Aggiorna + Manga incompleti + Capitolo incompleto + Account attivo + Login fallito + Sezione Discord + Account Discord + Nessun account trovato + Personalizzazione Discord + Messaggio personalizzato + Messaggio personalizzato che verrà mostrato su Discord + ⛔ Non disturbare + 🌙 Inattivo + Modalità incognita Discord + 🟢 Online + Mostra pulsanti + Mostra progressi + Mostra pulsante download + Mostra pulsante Discord + Voci non in libreria + Mostra tempo di attività + Leggi su %s + Mostra capitolo corrente + Mostra tempo + Mostra pulsante per scaricare l\'applicazione + Doppio zoom + Allinea copertina in alto + Aggiornamento in corso + Nome utente + Inserisci il nome utente del tuo account WebDAV + Password + Inserisci la password del tuo account WebDAV + Cartella dati WebDAV + L\'applicazione salverà i dati in questa cartella + WebDAV Server URL + Inserisci l\'indirizzo server di WebDAV + Dati scaricati + Usa ricerca intelligente + Sponsorizzami + Discord + Stato di Discord + Mostra Titoli Capitolo + Mostra titolo capitolo corrente + Servizi speciali + Mostra pulsante per entrare nel server Discord + Collegamenti + Apri per configurare Discord RPC + Impossibile aprire cartella + Mostra categorie vuote durante ricerca/filtraggio + Sincronizzare i manga quando vengono aggiunti alla libreria + Capitoli e cronologia dal database + Pianificazione aggiornamento automatico libreria fallita: %s + Pianificazione backup automatico fallito: %s + Disattiva ingrandimento + Modalità zoom panoramico + 16:9 + 20:9 + 4:3 + 3:2 + Mostra copertina allineata in alto accanto alle informazioni sul manga + Doom + Le voci nelle categorie escluse non verranno mostrate anche se presenti nelle categorie incluse. + Recupera automaticamente i metadati e l\'elenco dei capitoli di un nuovo manga quando lo aggiungi alla libreria + WebDAV + Se abilitato, seleziona automaticamente il risultato con la corrispondenza migliore. Altrimenti, mostra tutti i risultati della ricerca per la selezione manuale. + Discord, altri in arrivo... + Questo servizio Discord funziona tramite registrazione dei token. Utilizzalo a tuo rischio e pericolo. I token vengono salvati localmente sul tuo dispositivo e non vengono condivisi da %s. + Discord Rich Presence + Abilita Discord Rich Presence + Ignora se la modalità di navigazione in incognito è attiva + Le voci in categorie incluse non verranno visualizzate su Discord + Komikku - Un\'app per leggere manga + Visualizza i pulsanti per scaricare l\'app o unirti al server Discord + Regolazione automatica per la visualizzazione su schermo panoramico + Apri il menu tracciamento quando aggiungi una voce alla libreria + Naviga diff --git a/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml index 2a3c7bc6e..9f12eb296 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml @@ -1,13 +1,67 @@ - 1 시간 - 2 시간 - 6 시간 - 12 시간 - 1 일 - 솜사탕 - - 매트릭스 - 모카 - 사파이어 + 1 시간 + 2 시간 + 6 시간 + 12 시간 + 1 일 + 솜사탕 + + 매트릭스 + 모카 + 사파이어 + 라이브러리에서 원본 작품을 삭제하시겠습니까? + 소스 검색 + 소스 내 검색 + 라이브러리 검색 + NSFW만 표시 + 일괄 선택 모드 + FAQ 및 가이드 + 위로 이동 + 아래로 이동 + 색상 확인 + 숨기기 + 숨겨진 카테고리 표시 + 파노라마 표지 + 폴더 열기 + 업데이트 + 미완결 manga + 미완결 챕터 + 라이브러리 없는 작품 + 1페이지 클립보드에 복사 + 2페이지 클립보드에 복사 + 결합 페이지 클립보드에 복사 + 탭 영역 축소 + 폴더 열기 실패 + 핀치로 줌 + 줌인 비활성화 + 넓은 이미지 줌 모드 + 16:9 + 20:9 + 4:3 + 3:2 + 커스텀 테마 스타일 + Manga 정보 + 표지 기반 테마 + 표지 기반 테마 스타일 + 파노라마 표지 + 넓은 이미지인 경우 가로 모드로 표지 표시 + 파노라마 표지 흐름 + 글로벌 검색 & 제안에 파노라마 표지 표시 + 표지를 상단에 정렬 + Manga 정보와 표지를 상단에 정렬하여 표시 + 파노라마 편안한 그리드 + 추천 펼치기 + 비활성화하면 추천을 자동으로 불러오거나 펼쳐서 표시하지 않습니다 + 오버플로우 메뉴 추천 + 추천 버튼을 엔트리 페이지 대신 오버플로우 메뉴에 배치 + 홈 메뉴 추천 + 추천을 탐색하는 동안 홈으로 바로 이동하는 아이콘 표시 + Cloudflare + 팔레트 개인화 + 커스텀 테마용 시드 색상 선택 + 커스텀 테마 + 업데이트 중 배너 표시 + 소스 아이콘 + 언어 아이콘 사용 diff --git a/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml index e36b78c81..5301b91d8 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml @@ -206,4 +206,6 @@ Тайтлы не в библиотеке Синхронизировать тайтлы при добавлении в библиотеку Автоматически загружает метаданные и список глав при добавлении в библиотеку + Использовать умный поиск + Если включено, автоматически выбирает наиболее совпадающий результат. Иначе, показывает все результаты поиска для ручного выбора. diff --git a/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml b/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml index f9d00b4f5..9fde8c2ed 100644 --- a/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -197,4 +197,15 @@ 未收藏項目 新增至書庫時自動同步 將新漫畫加入書庫時,自動取得其元資料及章節列表 + WebDAV + WebDAV 伺服器 URL + 輸入 WebDAV 伺服器地址 + 使用者名稱 + 輸入你的 WebDAV 帳戶使用者名稱 + 密碼 + 輸入你的 WebDAV 帳戶密碼 + WebDAV 資料夾 + 應用將把資料儲存在這個資料夾中 + 使用智慧搜尋 + 若已啟用,自動選取最佳匹配結果,否則顯示所有搜尋結果供手動選擇 From 6f06974524283c25228b66804fc75b3f785e8ea1 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:09:48 +0700 Subject: [PATCH 02/11] feat: split "sync on add" into separate metadata and chapter fetch preferences (#1563) * feat: split "sync on add" into separate metadata and chapter fetch preferences * refactor: remove redundant SnackbarHostState and error snackbars when auto sync entries * clean other languages strings --- .../interactor/SyncChaptersWithSource.kt | 2 +- .../settings/screen/SettingsLibraryScreen.kt | 11 +++-- .../ui/browse/BulkFavoriteScreenModel.kt | 44 +++++++++++-------- .../source/browse/BrowseSourceScreenModel.kt | 23 +++++----- .../library/service/LibraryPreferences.kt | 3 +- .../commonMain/moko-resources/ar/strings.xml | 6 ++- .../moko-resources/base/strings.xml | 6 ++- .../commonMain/moko-resources/fil/strings.xml | 6 ++- .../commonMain/moko-resources/it/strings.xml | 2 - .../commonMain/moko-resources/ru/strings.xml | 6 ++- .../moko-resources/zh-rTW/strings.xml | 6 ++- 11 files changed, 69 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt index f7d9a9d73..f4d41a3d9 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt @@ -200,7 +200,7 @@ class SyncChaptersWithSource( bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers, ) - // Try to to use the fetch date of the original entry to not pollute 'Updates' tab + // Try to use the fetch date of the original entry to not pollute 'Updates' tab deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let { chapter = chapter.copy(dateFetch = it) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index ccd66c659..47cb647ab 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -293,9 +293,14 @@ object SettingsLibraryScreen : SearchableSettings { title = stringResource(KMR.strings.pref_show_empty_categories_search), ), Preference.PreferenceItem.SwitchPreference( - preference = libraryPreferences.syncOnAdd(), - title = stringResource(KMR.strings.pref_sync_manga_on_add), - subtitle = stringResource(KMR.strings.pref_sync_manga_on_add_description), + preference = libraryPreferences.fetchMetadataOnAdd(), + title = stringResource(KMR.strings.pref_fetch_manga_metadata_on_add), + subtitle = stringResource(KMR.strings.pref_fetch_manga_metadata_on_add_description), + ), + Preference.PreferenceItem.SwitchPreference( + preference = libraryPreferences.fetchChaptersOnAdd(), + title = stringResource(KMR.strings.pref_fetch_manga_chapters_on_add), + subtitle = stringResource(KMR.strings.pref_fetch_manga_chapters_on_add_description), ), // KMK <-- ), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index 4000dbbeb..1c29aaf72 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.browse -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -60,7 +59,6 @@ class BulkFavoriteScreenModel( private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), private val addTracks: AddTracks = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), - val snackbarHostState: SnackbarHostState = SnackbarHostState(), ) : StateScreenModel(initialState) { fun backHandler() { @@ -83,7 +81,7 @@ class BulkFavoriteScreenModel( } /** - * @param toSelectedState set to true to only Select, set to false to only Unselect + * @param toSelectedState set to `true` to only Select, set to `false` to only Unselect */ fun toggleSelection(manga: Manga, toSelectedState: Boolean? = null) { mutableState.update { state -> @@ -131,7 +129,7 @@ class BulkFavoriteScreenModel( if (entryWithDuplicates != null) { val (index, manga, duplicates) = entryWithDuplicates if (state.value.selection.size == 1) { - // If only one manga is selected, show the multiple duplicates dialog. + // If only one manga is selected, show the multiple-duplicates dialog. setDialog(Dialog.AddDuplicateManga(manga, duplicates)) } else { setDialog(Dialog.BulkAllowDuplicate(manga, duplicates, index)) @@ -256,17 +254,22 @@ class BulkFavoriteScreenModel( setMangaDefaultChapterFlags.await(manga) addTracks.bindEnhancedTrackers(manga, source) updateManga.awaitUpdateFavorite(manga.id, true) - if (libraryPreferences.syncOnAdd().get()) { + val fetchMetadataOnAdd = libraryPreferences.fetchMetadataOnAdd().get() + val fetchChaptersOnAdd = libraryPreferences.fetchChaptersOnAdd().get() + if (fetchMetadataOnAdd || fetchChaptersOnAdd) { val sManga = manga.toSManga() - val remoteManga = source.getMangaDetails(sManga) - val chapters = source.getChapterList(sManga) - // Use `manga` instead of `new` so its title got updated with source's `getMangaDetails` - updateManga.awaitUpdateFromSource(manga, remoteManga, false, coverCache) - syncChaptersWithSource.await(chapters, manga, source, false) + if (fetchMetadataOnAdd) { + val remoteMetadata = source.getMangaDetails(sManga) + // Use `manga` instead of `new` so its title got updated with source's `getMangaDetails` + updateManga.awaitUpdateFromSource(manga, remoteMetadata, false, coverCache) + } + if (fetchChaptersOnAdd) { + val chapters = source.getChapterList(sManga) + syncChaptersWithSource.await(chapters, manga, source, false) + } } } catch (e: Exception) { logcat(LogPriority.ERROR, e) - snackbarHostState.showSnackbar(message = "Failed to sync manga: ${e.message}") } } } @@ -351,18 +354,23 @@ class BulkFavoriteScreenModel( } updateManga.await(new.toMangaUpdate()) - if (new.favorite && libraryPreferences.syncOnAdd().get()) { + val fetchMetadataOnAdd = libraryPreferences.fetchMetadataOnAdd().get() + val fetchChaptersOnAdd = libraryPreferences.fetchChaptersOnAdd().get() + if (new.favorite && (fetchMetadataOnAdd || fetchChaptersOnAdd)) { withIOContext { try { val sManga = manga.toSManga() - val remoteManga = source.getMangaDetails(sManga) - val chapters = source.getChapterList(sManga) - // Use `manga` instead of `new` so its title got updated with source's `getMangaDetails` - updateManga.awaitUpdateFromSource(manga, remoteManga, false, coverCache) - syncChaptersWithSource.await(chapters, manga, source, false) + if (fetchMetadataOnAdd) { + val remoteMetadata = source.getMangaDetails(sManga) + // Use `manga` instead of `new` so its title got updated with source's `getMangaDetails` + updateManga.awaitUpdateFromSource(manga, remoteMetadata, false, coverCache) + } + if (fetchChaptersOnAdd) { + val chapters = source.getChapterList(sManga) + syncChaptersWithSource.await(chapters, manga, source, false) + } } catch (e: Exception) { logcat(LogPriority.ERROR, e) - snackbarHostState.showSnackbar(message = "Failed to sync manga: ${e.message}") } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index 75b8219c9..d44f87764 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.browse import android.content.res.Configuration import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -120,7 +119,6 @@ open class BrowseSourceScreenModel( private val toggleIncognito: ToggleIncognito = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), - val snackbarHostState: SnackbarHostState = SnackbarHostState(), // KMK <-- // SY --> @@ -424,20 +422,23 @@ open class BrowseSourceScreenModel( updateManga.await(new.toMangaUpdate()) // KMK --> - if (new.favorite && libraryPreferences.syncOnAdd().get()) { + val fetchMetadataOnAdd = libraryPreferences.fetchMetadataOnAdd().get() + val fetchChaptersOnAdd = libraryPreferences.fetchChaptersOnAdd().get() + if (new.favorite && (fetchMetadataOnAdd || fetchChaptersOnAdd)) { withIOContext { try { val sManga = manga.toSManga() - val remoteManga = source.getMangaDetails(sManga) - val chapters = source.getChapterList(sManga) - // Use `manga` instead of `new` so its title got updated with source's `getMangaDetails` - updateManga.awaitUpdateFromSource(manga, remoteManga, false, coverCache) - syncChaptersWithSource.await(chapters, manga, source, false) + if (fetchMetadataOnAdd) { + val remoteMetadata = source.getMangaDetails(sManga) + // Use `manga` instead of `new` so its title got updated with source's `getMangaDetails` + updateManga.awaitUpdateFromSource(manga, remoteMetadata, false, coverCache) + } + if (fetchChaptersOnAdd) { + val chapters = source.getChapterList(sManga) + syncChaptersWithSource.await(chapters, manga, source, false) + } } catch (e: Exception) { logcat(LogPriority.ERROR, e) - screenModelScope.launch { - snackbarHostState.showSnackbar(message = "Failed to sync manga: ${e.message}") - } } } } diff --git a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt index 5209d95b9..053debf13 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -74,7 +74,8 @@ class LibraryPreferences( fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false) // KMK --> - fun syncOnAdd() = preferenceStore.getBoolean("sync_on_add", false) + fun fetchMetadataOnAdd() = preferenceStore.getBoolean("fetch_metadata_on_add", false) + fun fetchChaptersOnAdd() = preferenceStore.getBoolean("fetch_chapters_on_add", false) // KMK <-- fun showContinueReadingButton() = preferenceStore.getBoolean( diff --git a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml index e85a1a754..df7a724f5 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml @@ -145,9 +145,11 @@ لن يتم عرض العناصر في الفئات المستبعدة حتى لو كانت موجودة أيضًا في الفئات المشمولة. جاري التحديث عرض الفئات الفارغة أثناء البحث/التصفية - مزامنة المانجا عند إضافتها إلى المكتبة - يقوم تلقائيًا بجلب البيانات الوصفية وقائمة الفصول الخاصة بمانجا جديدة عند إضافتها إلى المكتبة فتح قائمة المزامنة عند الإضافة للمكتبة + تحديث بيانات المانجا عند الإضافة للمكتبة + جلب العنوان والوصف والعلامات وغيرها عند إضافة المانجا إلى المكتبة + جلب الفصول عند الإضافة للمكتبة + تحديث قائمة الفصول مباشرة بعد إضافة المانجا إلى المكتبة WebDAV رابط سيرفر WebDAV ادخل رابط سيرفر WebDAV diff --git a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml index 62f531c5b..4bfb09902 100644 --- a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml @@ -96,8 +96,10 @@ Entries in excluded categories will not be shown even if they are also in included categories. Updating Show empty categories during search/filtering - Sync manga when adding to library - Automatically fetches the metadata and chapter list of a new manga when adding it to the library + Fetch metadata when adding to library + Automatically fetches the metadata of a new manga when adding it to the library + Fetch chapters when adding to library + Automatically fetches the chapter list of a new manga when adding it to the library Refresh extension page to update list. More extensions... diff --git a/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml index 37cd80a83..35461dd78 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml @@ -172,12 +172,14 @@ Di natapos na kabanata Mga entry na wala sa aklatan Kaswal - I-sync ang manga kapag nagdadagdag sa aklatan - Awtomatikong kinukuha ang metadata at listahan ng kabanata ng isang bagong manga kapag idinagdag ito sa aklatan Server URL ng WebDAV Ipasok ang address ng WebDAV server Ipasok ang iyong username ng WebDAV account Ipasok ang iyong password ng WebDAV account Ise-save ng aplikasyon ang data sa folder na ito Rich Presence ng Discord + Kunin ang metadata ng manga kapag idinadagdag sa aklatan + Awtomatikong kunin at i-update ang metadata ng manga kapag idinagdag mo ito sa iyong aklatan + Kunin ang mga kabanata kapag idinadagdag ang manga sa aklatan + Awtomatikong i-scan ang source para sa mga kabanata kapag idinagdag ang manga sa iyong aklatan diff --git a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml index b0802f505..b1f921db7 100644 --- a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml @@ -173,7 +173,6 @@ Apri per configurare Discord RPC Impossibile aprire cartella Mostra categorie vuote durante ricerca/filtraggio - Sincronizzare i manga quando vengono aggiunti alla libreria Capitoli e cronologia dal database Pianificazione aggiornamento automatico libreria fallita: %s Pianificazione backup automatico fallito: %s @@ -186,7 +185,6 @@ Mostra copertina allineata in alto accanto alle informazioni sul manga Doom Le voci nelle categorie escluse non verranno mostrate anche se presenti nelle categorie incluse. - Recupera automaticamente i metadati e l\'elenco dei capitoli di un nuovo manga quando lo aggiungi alla libreria WebDAV Se abilitato, seleziona automaticamente il risultato con la corrispondenza migliore. Altrimenti, mostra tutti i risultati della ricerca per la selezione manuale. Discord, altri in arrivo... diff --git a/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml index 5301b91d8..86bbcd3c5 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml @@ -204,8 +204,10 @@ Незаконченные тайтлы Незаконченные главы Тайтлы не в библиотеке - Синхронизировать тайтлы при добавлении в библиотеку - Автоматически загружает метаданные и список глав при добавлении в библиотеку Использовать умный поиск Если включено, автоматически выбирает наиболее совпадающий результат. Иначе, показывает все результаты поиска для ручного выбора. + Получать метаданные манги при добавлении + Автоматически загружать обложку, описание и другую информацию при добавлении манги в библиотеку + Получать главы при добавлении + Автоматически обновлять список глав при добавлении манги в библиотеку diff --git a/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml b/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml index 9fde8c2ed..53fbe39e0 100644 --- a/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -195,8 +195,6 @@ 尚未讀完 未讀完章節 未收藏項目 - 新增至書庫時自動同步 - 將新漫畫加入書庫時,自動取得其元資料及章節列表 WebDAV WebDAV 伺服器 URL 輸入 WebDAV 伺服器地址 @@ -208,4 +206,8 @@ 應用將把資料儲存在這個資料夾中 使用智慧搜尋 若已啟用,自動選取最佳匹配結果,否則顯示所有搜尋結果供手動選擇 + 新增至書櫃時更新漫畫資料 + 在將作品加入書櫃後,自動抓取標題、作者等最新漫畫資訊 + 新增至書櫃時更新章節列表 + 在將作品加入書櫃後,自動抓取最新章節列表 From fd60bc7f13649bf2e5eba3b57996739c4103702e Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:52:51 +0700 Subject: [PATCH 03/11] fix(MangaDex): Fix nullability issues in track creation for MangaDex (#1527) * fix(trackers-mangadex): Handle nullability in `createMdListTrack` * Return null instead of throwing exception in initial track creation * Improve error handling in `createMdListTrack` * Change exception to return null when track not found * handle nullability in MangaDex tracking --- .../tachiyomi/ui/manga/MangaScreenModel.kt | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 216f00e46..e0ad1faf4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -75,6 +75,7 @@ import eu.kanade.tachiyomi.util.system.toast import exh.debug.DebugToggles import exh.eh.EHentaiUpdateHelper import exh.log.xLogD +import exh.log.xLogE import exh.md.utils.FollowStatus import exh.metadata.metadata.RaisedSearchMetadata import exh.metadata.metadata.base.FlatMetadata @@ -1851,7 +1852,7 @@ class MangaScreenModel( } // KMK: auto track MangaDex mdTrack.id !in tracks.map { it.trackerId } -> { - tracks + createMdListTrack() + createMdListTrack()?.let { tracks + it } ?: tracks } else -> tracks } @@ -1880,21 +1881,27 @@ class MangaScreenModel( } // SY --> - private suspend fun createMdListTrack(): Track { - val state = successState!! - val mdManga = state.manga.takeIf { it.source in mangaDexSourceIds } - ?: state.mergedData?.manga?.values?.find { it.source in mangaDexSourceIds } - ?: throw IllegalArgumentException("Could not create initial track") - val track = trackerManager.mdList.createInitialTracker(state.manga, mdManga) - .toDomainTrack(false)!! - insertTrack.await(track) - /* KMK --> - return TrackItem( - getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id }, - trackerManager.mdList, - ) - KMK <-- */ - return getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id } + private suspend fun createMdListTrack(): Track? { + try { + val state = successState!! + val mdManga = state.manga.takeIf { it.source in mangaDexSourceIds } + ?: state.mergedData?.manga?.values?.find { it.source in mangaDexSourceIds } + ?: throw IllegalArgumentException("Entry does not belong to MangaDex") + val track = trackerManager.mdList.createInitialTracker(state.manga, mdManga) + .toDomainTrack(false) + ?: throw IllegalStateException("Could not create initial track") + insertTrack.await(track) + /* KMK --> + return TrackItem( + getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id }, + trackerManager.mdList, + ) + KMK <-- */ + return getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id } + } catch (e: Exception) { + xLogE("Failed to create MangaDex track", e) + return null + } } // SY <-- From 26d3be95e3313f2a2250d4241a165c20fd4838ca Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:01:49 +0700 Subject: [PATCH 04/11] feat(UI): Mask WebDAV password while typing (#1564) * Refactor * feat(UI): Mask WebDAV password while typing * fix: allow space as password * use coroutines for asynchronous setting --- .../settings/screen/SettingsAdvancedScreen.kt | 4 +- .../settings/screen/SettingsBrowseScreen.kt | 2 +- .../settings/screen/SettingsDataScreen.kt | 44 ++++++++++++------- .../settings/screen/SettingsSecurityScreen.kt | 11 +++-- .../kanade/tachiyomi/data/sync/SyncManager.kt | 4 +- .../data/sync/service/WebDavSyncService.kt | 2 +- 6 files changed, 42 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index 8878b3ba2..8cc4ef491 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -738,7 +738,7 @@ object SettingsAdvancedScreen : SearchableSettings { subtitle = stringResource(SYMR.strings.data_saver_image_quality_summary), enabled = dataSaver != DataSaver.NONE, ), - kotlin.run { + run { val dataSaverImageFormatJpeg by sourcePreferences.dataSaverImageFormatJpeg() .collectAsState() Preference.PreferenceItem.SwitchPreference( @@ -808,7 +808,7 @@ object SettingsAdvancedScreen : SearchableSettings { title = stringResource(SYMR.strings.log_level), subtitle = stringResource(SYMR.strings.log_level_summary), ), - kotlin.run { + run { var enableEncryptDatabase by rememberSaveable { mutableStateOf(false) } if (enableEncryptDatabase) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt index 6d6b64450..e96eb1e72 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt @@ -85,7 +85,7 @@ object SettingsBrowseScreen : SearchableSettings { enabled = sourcePreferences.relatedMangas().get(), ), // KMK <-- - kotlin.run { + run { val count by sourcePreferences.sourcesTabCategories().collectAsState() Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.action_edit_categories), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index aba5089e4..150c2b004 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -48,6 +48,7 @@ import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import eu.kanade.domain.sync.SyncPreferences import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.screen.SettingsSecurityScreen.PasswordDialog import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.more.settings.screen.data.StorageInfo @@ -562,7 +563,7 @@ object SettingsDataScreen : SearchableSettings { SyncManager.SyncService.SYNCYOMI.value to stringResource(SYMR.strings.syncyomi), SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(SYMR.strings.google_drive), // KMK --> - SyncManager.SyncService.WebDAV.value to stringResource(KMR.strings.web_dav), + SyncManager.SyncService.WEB_DAV.value to stringResource(KMR.strings.web_dav), // KMK <-- ), title = stringResource(SYMR.strings.pref_sync_service), @@ -606,7 +607,7 @@ object SettingsDataScreen : SearchableSettings { SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences) SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences() // KMK --> - SyncManager.SyncService.WebDAV -> getWebDavPreferences(syncPreferences) + SyncManager.SyncService.WEB_DAV -> getWebDavPreferences(syncPreferences) // KMK <-- } @@ -759,7 +760,9 @@ object SettingsDataScreen : SearchableSettings { title = stringResource(SYMR.strings.pref_sync_api_key), subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ), onConfirm = { - syncPreferences.clientAPIKey().set(it) + scope.launch { + syncPreferences.clientAPIKey().set(it) + } true }, icon = null, @@ -808,17 +811,28 @@ object SettingsDataScreen : SearchableSettings { true }, ), - Preference.PreferenceItem.EditTextPreference( - preference = syncPreferences.webDavPassword(), - title = stringResource(KMR.strings.pref_webdav_password), - subtitle = stringResource(KMR.strings.pref_webdav_password_summ), - onValueChanged = { newValue -> - scope.launch { - syncPreferences.webDavPassword().set(newValue) - } - true - }, - ), + run { + var dialogOpen by remember { mutableStateOf(false) } + if (dialogOpen) { + PasswordDialog( + onDismissRequest = { dialogOpen = false }, + onReturnPassword = { password -> + dialogOpen = false + scope.launch { + syncPreferences.webDavPassword().set(password.replace("\n", "")) + } + }, + title = KMR.strings.pref_webdav_password, + ) + } + Preference.PreferenceItem.TextPreference( + title = stringResource(KMR.strings.pref_webdav_password), + subtitle = stringResource(KMR.strings.pref_webdav_password_summ), + onClick = { + dialogOpen = true + }, + ) + }, Preference.PreferenceItem.EditTextPreference( preference = syncPreferences.webDavFolder(), title = stringResource(KMR.strings.pref_webdav_folder), @@ -832,7 +846,7 @@ object SettingsDataScreen : SearchableSettings { ), ) } - // MK <-- + // KMK <-- @Composable private fun getSyncNowPref(): Preference.PreferenceGroup { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt index 9df011f14..5b4ee07be 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt @@ -155,7 +155,7 @@ object SettingsSecurityScreen : SearchableSettings { enabled = passwordProtectDownloads, ), - kotlin.run { + run { var dialogOpen by remember { mutableStateOf(false) } if (dialogOpen) { PasswordDialog( @@ -183,7 +183,7 @@ object SettingsSecurityScreen : SearchableSettings { securityPreferences.cbzPassword().set("") }, ), - kotlin.run { + run { val navigator = LocalNavigator.currentOrThrow val count by securityPreferences.authenticatorTimeRanges().collectAsState() Preference.PreferenceItem.TextPreference( @@ -199,7 +199,7 @@ object SettingsSecurityScreen : SearchableSettings { }, ) }, - kotlin.run { + run { val selection by securityPreferences.authenticatorDays().collectAsState() var dialogOpen by remember { mutableStateOf(false) } if (dialogOpen) { @@ -332,13 +332,16 @@ object SettingsSecurityScreen : SearchableSettings { fun PasswordDialog( onDismissRequest: () -> Unit, onReturnPassword: (String) -> Unit, + // KMK --> + title: StringResource = SYMR.strings.cbz_archive_password, + // KMK <-- ) { var password by rememberSaveable { mutableStateOf("") } var passwordVisibility by remember { mutableStateOf(false) } AlertDialog( onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(SYMR.strings.cbz_archive_password)) }, + title = { Text(text = stringResource(title)) }, text = { TextField( value = password, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt index a4dfb1815..ec289d2e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt @@ -57,7 +57,7 @@ class SyncManager( SYNCYOMI(1), GOOGLE_DRIVE(2), // KMK --> - WebDAV(3), + WEB_DAV(3), // KMK <-- ; @@ -142,7 +142,7 @@ class SyncManager( } // KMK --> - SyncService.WebDAV -> { + SyncService.WEB_DAV -> { WebDavSyncService(context, json, syncPreferences, notifier) } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/WebDavSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/WebDavSyncService.kt index cac0a49d9..40b3366a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/WebDavSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/WebDavSyncService.kt @@ -40,7 +40,7 @@ class WebDavSyncService( private val url: String = syncPreferences.webDavUrl().get().trim() private val folder: String = syncPreferences.webDavFolder().get().trim('/') private val username: String = syncPreferences.webDavUsername().get().trim() - private val password: String = syncPreferences.webDavPassword().get().trim() + private val password: String = syncPreferences.webDavPassword().get() private val credentials: String = Credentials.basic(username, password) private fun buildWebDavFileUrl(fileName: String = "backup.proto"): String { From 6f020399d8734dac6f5ee212d750751eeebe9d2e Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:22:27 +0700 Subject: [PATCH 05/11] chore(ci): Add SSH key setup for Codeberg repository mirroring (#1595) --- .github/workflows/codeberg_mirror.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/codeberg_mirror.yml b/.github/workflows/codeberg_mirror.yml index e5292f8b4..e1532f75c 100644 --- a/.github/workflows/codeberg_mirror.yml +++ b/.github/workflows/codeberg_mirror.yml @@ -16,6 +16,11 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.CODEBERG_SSH }}" >> ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa - uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1 with: target_repo_url: "git@codeberg.org:cuong-tran/komikku.git" From ee7fb26790faec7d148d8585391c1af55a57d31e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:18:16 +0700 Subject: [PATCH 06/11] fix: remove expect/actual for LocalSource in single-target KMP module (#1602) Agent-Logs-Url: https://github.com/komikku-app/komikku/sessions/5bf2689f-d906-47af-881b-4556f828a3a6 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cuong-tran <16017808+cuong-tran@users.noreply.github.com> --- .../kotlin/tachiyomi/source/local/LocalSource.kt | 2 +- .../commonMain/kotlin/tachiyomi/source/local/LocalSource.kt | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 0272cb8ec..090d313b0 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -49,7 +49,7 @@ import java.nio.charset.StandardCharsets import kotlin.time.Duration.Companion.days import tachiyomi.domain.source.model.Source as DomainSource -actual class LocalSource( +class LocalSource( private val context: Context, private val fileSystem: LocalSourceFileSystem, private val coverManager: LocalCoverManager, diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt deleted file mode 100644 index d592f1c96..000000000 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ /dev/null @@ -1,6 +0,0 @@ -package tachiyomi.source.local - -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.UnmeteredSource - -expect class LocalSource : CatalogueSource, UnmeteredSource From 80ea323281fd37b4f6ff4ac7833e2fe3ce516fc7 Mon Sep 17 00:00:00 2001 From: MediocreLegion <162182192+MediocreLegion@users.noreply.github.com> Date: Wed, 22 Apr 2026 03:31:05 -0300 Subject: [PATCH 07/11] fix(delegate): migrate NH to the v2 api (#1582) * fix(delegate): migrate NH to the v2 api * bump versionId * remove versionId requirement * gemini review suggestions * remove dead code * remove unused imports * linging * unneeded comment * fix(delegate): migrate NH to the v2 api (jobobby04/TachiyomiSY#1581) * fix(delegate): migrate NH to the v2 api * remove extra comment * remove redundant data * linting * Code cleanup --------- Co-authored-by: Jobobby04 (cherry picked from commit eec1236b8b5904b46d79bb716e29f073c6fe9b34) * close `response` * Avoid empty server list --------- Co-authored-by: Jobobby04 Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- .../tachiyomi/source/online/all/NHentai.kt | 99 +++++++------------ .../adapters/NHentaiDescriptionAdapter.kt | 4 +- .../metadata/NHentaiSearchMetadata.kt | 33 +------ 3 files changed, 45 insertions(+), 91 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt index 435274666..c5e4544f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt @@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.NamespaceSource import eu.kanade.tachiyomi.source.online.UrlImportableSource -import eu.kanade.tachiyomi.util.asJsoup import exh.metadata.metadata.NHentaiSearchMetadata import exh.metadata.metadata.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedTag @@ -30,7 +29,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import okhttp3.CacheControl import okhttp3.Response -import org.jsoup.nodes.Document +import tachiyomi.core.common.util.lang.withIOContext class NHentai(delegate: HttpSource, val context: Context) : DelegatedHttpSource(delegate), @@ -72,12 +71,8 @@ class NHentai(delegate: HttpSource, val context: Context) : } override suspend fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) { - val body = input.body.string() - val server = MEDIA_SERVER_REGEX.find(body)?.groupValues?.get(1)?.toInt() ?: 1 - val json = GALLERY_JSON_REGEX.find(body)!!.groupValues[1].replace( - UNICODE_ESCAPE_REGEX, - ) { it.groupValues[1].toInt(radix = 16).toChar().toString() } - val jsonResponse = jsonParser.decodeFromString(json) + if (nhConfig == null) getNhConfig() + val jsonResponse = jsonParser.decodeFromString(input.body.string()) with(metadata) { nhId = jsonResponse.id @@ -88,8 +83,6 @@ class NHentai(delegate: HttpSource, val context: Context) : mediaId = jsonResponse.mediaId - mediaServer = server - jsonResponse.title?.let { title -> japaneseTitle = title.japanese shortTitle = title.pretty @@ -98,16 +91,11 @@ class NHentai(delegate: HttpSource, val context: Context) : preferredTitle = this@NHentai.preferredTitle - jsonResponse.images?.let { images -> - coverImageType = input.asJsoup(body).parseCoverType() - ?: images.cover?.type - images.pages.mapNotNull { - it.type - }.let { - pageImageTypes = it - } - thumbnailImageType = images.thumbnail?.type - } + coverImageUrl = + jsonResponse.cover?.path?.let { "$thumbServer/$it" } + ?: jsonResponse.thumbnail?.path?.let { "$thumbServer/$it" } + + pageImagePreviewUrls = jsonResponse.pages.mapNotNull { it.thumbnail } scanlator = jsonResponse.scanlator?.trimOrNull() @@ -128,17 +116,13 @@ class NHentai(delegate: HttpSource, val context: Context) : } } - // KMK --> - /** - * Site JSON is saying cover of type `w` but instead it's using cover like `cover.jpg.webp` - */ - private fun Document.parseCoverType(): String? { - return selectFirst("#cover > a > img")?.attr("data-src") - ?.substringAfterLast('/') - ?.substringAfter('.') - ?.first()?.toString() - } - // KMK <-- + @Serializable + data class JsonConfig( + @SerialName("image_servers") + val imageServers: List = emptyList(), + @SerialName("thumb_servers") + val thumbServers: List = emptyList(), + ) @Serializable data class JsonResponse( @@ -146,7 +130,8 @@ class NHentai(delegate: HttpSource, val context: Context) : @SerialName("media_id") val mediaId: String? = null, val title: JsonTitle? = null, - val images: JsonImages? = null, + val cover: JsonPage? = null, + val thumbnail: JsonPage? = null, val scanlator: String? = null, @SerialName("upload_date") val uploadDate: Long? = null, @@ -155,6 +140,7 @@ class NHentai(delegate: HttpSource, val context: Context) : val numPages: Int? = null, @SerialName("num_favorites") val numFavorites: Long? = null, + val pages: List = emptyList(), ) @Serializable @@ -164,21 +150,12 @@ class NHentai(delegate: HttpSource, val context: Context) : val pretty: String? = null, ) - @Serializable - data class JsonImages( - val pages: List = emptyList(), - val cover: JsonPage? = null, - val thumbnail: JsonPage? = null, - ) - @Serializable data class JsonPage( - @SerialName("t") - val type: String? = null, - @SerialName("w") + val path: String? = null, val width: Long? = null, - @SerialName("h") val height: Long? = null, + val thumbnail: String? = null, ) @Serializable @@ -203,20 +180,16 @@ class NHentai(delegate: HttpSource, val context: Context) : } override suspend fun getPagePreviewList(manga: SManga, chapters: List, page: Int): PagePreviewPage { + if (nhConfig == null) getNhConfig() val metadata = fetchOrLoadMetadata(manga.id()) { client.newCall(mangaDetailsRequest(manga)).awaitSuccess() } return PagePreviewPage( page, - metadata.pageImageTypes.mapIndexed { index, s -> + metadata.pageImagePreviewUrls.mapIndexed { index, path -> PagePreviewInfo( index + 1, - imageUrl = thumbnailUrlFromType( - metadata.mediaId!!, - metadata.mediaServer ?: 1, - index + 1, - s, - )!!, + imageUrl = "$thumbServer/$path", ) }, false, @@ -224,15 +197,23 @@ class NHentai(delegate: HttpSource, val context: Context) : ) } - private fun thumbnailUrlFromType( - mediaId: String, - mediaServer: Int, - page: Int, - t: String, - ) = NHentaiSearchMetadata.typeToExtension(t)?.let { - "https://t$mediaServer.nhentai.net/galleries/$mediaId/${page}t.$it" + var nhConfig: JsonConfig? = null + suspend fun getNhConfig() { + try { + val body = withIOContext { client.newCall(GET("https://nhentai.net/api/v2/config", headers)).awaitSuccess() } + .use { it.body.string() } + nhConfig = jsonParser.decodeFromString(body) + } catch (_: Exception) { + nhConfig = JsonConfig( + (1..4).map { n -> "https://i$n.nhentai.net" }, + (1..4).map { n -> "https://t$n.nhentai.net" }, + ) + } } + val thumbServer + get() = nhConfig?.thumbServers?.randomOrNull() ?: "https://t1.nhentai.net" + override suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response { return client.newCachelessCallWithProgress( if (cacheControl != null) { @@ -248,10 +229,6 @@ class NHentai(delegate: HttpSource, val context: Context) : private val jsonParser = Json { ignoreUnknownKeys = true } - - private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);") - private val MEDIA_SERVER_REGEX = Regex("media_server\\s*:\\s*(\\d+)") - private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})") private const val TITLE_PREF = "Display manga title as:" } } diff --git a/app/src/main/java/exh/ui/metadata/adapters/NHentaiDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/NHentaiDescriptionAdapter.kt index cf7515229..8835612d3 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/NHentaiDescriptionAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/NHentaiDescriptionAdapter.kt @@ -81,8 +81,8 @@ fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) { binding.pages.text = context.pluralStringResource( SYMR.plurals.num_pages, - meta.pageImageTypes.size, - meta.pageImageTypes.size, + meta.pageImagePreviewUrls.size, + meta.pageImagePreviewUrls.size, ) // KMK --> binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor, 4.dpToPx) diff --git a/source-api/src/commonMain/kotlin/exh/metadata/metadata/NHentaiSearchMetadata.kt b/source-api/src/commonMain/kotlin/exh/metadata/metadata/NHentaiSearchMetadata.kt index 10ebb29d7..70553d3b3 100644 --- a/source-api/src/commonMain/kotlin/exh/metadata/metadata/NHentaiSearchMetadata.kt +++ b/source-api/src/commonMain/kotlin/exh/metadata/metadata/NHentaiSearchMetadata.kt @@ -29,15 +29,12 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() { var mediaId: String? = null - var mediaServer: Int? = null - var japaneseTitle by titleDelegate(TITLE_TYPE_JAPANESE) var englishTitle by titleDelegate(TITLE_TYPE_ENGLISH) var shortTitle by titleDelegate(TITLE_TYPE_SHORT) - var coverImageType: String? = null - var pageImageTypes: List = emptyList() - var thumbnailImageType: String? = null + var coverImageUrl: String? = null + var pageImagePreviewUrls: List = emptyList() var scanlator: String? = null @@ -46,16 +43,6 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() { override fun createMangaInfo(manga: SManga): SManga { val key = nhId?.let { nhIdToPath(it) } - val cover = if (mediaId != null) { - // Default media server for cover is always 1 (see in page header) - val server = mediaServer ?: 1 - typeToExtension(coverImageType)?.let { - "https://t$server.nhentai.net/galleries/$mediaId/cover.$it" - } - } else { - null - } - val title = when (preferredTitle) { TITLE_TYPE_SHORT -> shortTitle ?: englishTitle ?: japaneseTitle ?: manga.title 0, TITLE_TYPE_ENGLISH -> englishTitle ?: japaneseTitle ?: shortTitle ?: manga.title @@ -88,7 +75,7 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() { return manga.copy( url = key ?: manga.url, - thumbnail_url = cover ?: manga.thumbnail_url, + thumbnail_url = coverImageUrl ?: manga.thumbnail_url, title = title, artist = group ?: manga.artist, author = artist ?: manga.artist, @@ -116,9 +103,8 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() { getItem(japaneseTitle) { stringResource(SYMR.strings.japanese_title) }, getItem(englishTitle) { stringResource(SYMR.strings.english_title) }, getItem(shortTitle) { stringResource(SYMR.strings.short_title) }, - getItem(coverImageType) { stringResource(SYMR.strings.cover_image_file_type) }, - getItem(pageImageTypes.size) { stringResource(SYMR.strings.page_count) }, - getItem(thumbnailImageType) { stringResource(SYMR.strings.thumbnail_image_file_type) }, + getItem(coverImageUrl) { stringResource(SYMR.strings.thumbnail_url) }, + getItem(pageImagePreviewUrls.size) { stringResource(SYMR.strings.page_count) }, getItem(scanlator) { stringResource(MR.strings.scanlator) }, ) } @@ -137,15 +123,6 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() { private const val NHENTAI_GROUP_NAMESPACE = "group" const val NHENTAI_CATEGORIES_NAMESPACE = "category" - fun typeToExtension(t: String?) = - when (t) { - "p" -> "png" - "j" -> "jpg" - "g" -> "gif" - "w" -> "webp" - else -> null - } - fun nhUrlToId(url: String) = url.split("/").last { it.isNotBlank() }.toLong() From 8bb8d7ccb726b3764030f830cb0034d164321a41 Mon Sep 17 00:00:00 2001 From: Luca Auer Date: Sat, 14 Feb 2026 19:43:31 +0100 Subject: [PATCH 08/11] Add missing indexes to improve database query performance (mihonapp/mihon#2950) (cherry picked from commit cae9fbf3213987e7d263845431dfac10a2ecb3b0) --- data/src/main/sqldelight/tachiyomi/data/chapters.sq | 1 + .../sqldelight/tachiyomi/data/excluded_scanlators.sq | 1 + data/src/main/sqldelight/tachiyomi/data/history.sq | 1 + data/src/main/sqldelight/tachiyomi/data/manga_sync.sq | 2 ++ data/src/main/sqldelight/tachiyomi/data/mangas.sq | 1 + .../main/sqldelight/tachiyomi/data/mangas_categories.sq | 3 +++ data/src/main/sqldelight/tachiyomi/migrations/45.sqm | 9 +++++++++ 7 files changed, 18 insertions(+) create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/45.sqm diff --git a/data/src/main/sqldelight/tachiyomi/data/chapters.sq b/data/src/main/sqldelight/tachiyomi/data/chapters.sq index 447cddab8..f5f72b051 100644 --- a/data/src/main/sqldelight/tachiyomi/data/chapters.sq +++ b/data/src/main/sqldelight/tachiyomi/data/chapters.sq @@ -22,6 +22,7 @@ CREATE TABLE chapters( CREATE INDEX chapters_manga_id_index ON chapters(manga_id); CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0; +CREATE INDEX idx_chapters_url ON chapters(url); CREATE TRIGGER update_last_modified_at_chapters AFTER UPDATE ON chapters diff --git a/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq b/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq index 2af2f4199..37ed6489e 100644 --- a/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq +++ b/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq @@ -6,6 +6,7 @@ CREATE TABLE excluded_scanlators( ); CREATE INDEX excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id); +CREATE INDEX idx_excluded_scanlators_scanlator ON excluded_scanlators(scanlator); insert: INSERT INTO excluded_scanlators(manga_id, scanlator) diff --git a/data/src/main/sqldelight/tachiyomi/data/history.sq b/data/src/main/sqldelight/tachiyomi/data/history.sq index 01ba77e86..569f44967 100644 --- a/data/src/main/sqldelight/tachiyomi/data/history.sq +++ b/data/src/main/sqldelight/tachiyomi/data/history.sq @@ -10,6 +10,7 @@ CREATE TABLE history( ); CREATE INDEX history_history_chapter_id_index ON history(chapter_id); +CREATE INDEX idx_history_last_read ON history(last_read); getHistoryByMangaId: SELECT diff --git a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq b/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq index 44fa0570b..d14b7125c 100644 --- a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq +++ b/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq @@ -20,6 +20,8 @@ CREATE TABLE manga_sync( ON DELETE CASCADE ); +CREATE INDEX idx_manga_sync_manga_id ON manga_sync(manga_id); + delete: DELETE FROM manga_sync WHERE manga_id = :mangaId AND sync_id = :syncId; diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index b0b3b4bc1..bf04d800e 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -34,6 +34,7 @@ CREATE TABLE mangas( CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; CREATE INDEX mangas_url_index ON mangas(url); +CREATE INDEX idx_mangas_source ON mangas(source); CREATE TRIGGER update_last_favorited_at_mangas AFTER UPDATE OF favorite ON mangas diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq b/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq index 6d918c6e5..be01fb371 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq @@ -8,6 +8,9 @@ CREATE TABLE mangas_categories( ON DELETE CASCADE ); +CREATE INDEX idx_mangas_categories_manga_id ON mangas_categories(manga_id); +CREATE INDEX idx_mangas_categories_category_id ON mangas_categories(category_id); + CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories BEGIN UPDATE mangas diff --git a/data/src/main/sqldelight/tachiyomi/migrations/45.sqm b/data/src/main/sqldelight/tachiyomi/migrations/45.sqm new file mode 100644 index 000000000..bdeb188fc --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/45.sqm @@ -0,0 +1,9 @@ +-- Migration to add performance indexes + +CREATE INDEX IF NOT EXISTS idx_mangas_source ON mangas(source); +CREATE INDEX IF NOT EXISTS idx_chapters_url ON chapters(url); +CREATE INDEX IF NOT EXISTS idx_mangas_categories_manga_id ON mangas_categories(manga_id); +CREATE INDEX IF NOT EXISTS idx_mangas_categories_category_id ON mangas_categories(category_id); +CREATE INDEX IF NOT EXISTS idx_excluded_scanlators_scanlator ON excluded_scanlators(scanlator); +CREATE INDEX IF NOT EXISTS idx_manga_sync_manga_id ON manga_sync(manga_id); +CREATE INDEX IF NOT EXISTS idx_history_last_read ON history(last_read); From bf9b66358139926ea12964265f082c2051aee4b6 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:02:00 +0700 Subject: [PATCH 09/11] Optimize database indexes and clean up schema (#1603) * Update `excluded_scanlators` index to a composite index on `manga_id` and `scanlator`. * Convert `idx_history_last_read` into a partial index for entries where `last_read > 0`. * Remove redundant `idx_manga_sync_manga_id` index. * Remove unused `last_modified_at` column and its associated trigger from `mangas_categories`. * Sync migration files with schema changes. --- .../main/sqldelight/tachiyomi/data/excluded_scanlators.sq | 3 +-- data/src/main/sqldelight/tachiyomi/data/history.sq | 2 +- data/src/main/sqldelight/tachiyomi/data/manga_sync.sq | 2 -- data/src/main/sqldelight/tachiyomi/migrations/45.sqm | 6 +++--- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq b/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq index 37ed6489e..aced54fb2 100644 --- a/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq +++ b/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq @@ -5,8 +5,7 @@ CREATE TABLE excluded_scanlators( ON DELETE CASCADE ); -CREATE INDEX excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id); -CREATE INDEX idx_excluded_scanlators_scanlator ON excluded_scanlators(scanlator); +CREATE INDEX idx_excluded_scanlators_manga_id_scanlator ON excluded_scanlators(manga_id, scanlator); insert: INSERT INTO excluded_scanlators(manga_id, scanlator) diff --git a/data/src/main/sqldelight/tachiyomi/data/history.sq b/data/src/main/sqldelight/tachiyomi/data/history.sq index 569f44967..8677577f4 100644 --- a/data/src/main/sqldelight/tachiyomi/data/history.sq +++ b/data/src/main/sqldelight/tachiyomi/data/history.sq @@ -10,7 +10,7 @@ CREATE TABLE history( ); CREATE INDEX history_history_chapter_id_index ON history(chapter_id); -CREATE INDEX idx_history_last_read ON history(last_read); +CREATE INDEX idx_history_last_read ON history(last_read) WHERE last_read > 0; getHistoryByMangaId: SELECT diff --git a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq b/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq index d14b7125c..44fa0570b 100644 --- a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq +++ b/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq @@ -20,8 +20,6 @@ CREATE TABLE manga_sync( ON DELETE CASCADE ); -CREATE INDEX idx_manga_sync_manga_id ON manga_sync(manga_id); - delete: DELETE FROM manga_sync WHERE manga_id = :mangaId AND sync_id = :syncId; diff --git a/data/src/main/sqldelight/tachiyomi/migrations/45.sqm b/data/src/main/sqldelight/tachiyomi/migrations/45.sqm index bdeb188fc..2614232b4 100644 --- a/data/src/main/sqldelight/tachiyomi/migrations/45.sqm +++ b/data/src/main/sqldelight/tachiyomi/migrations/45.sqm @@ -4,6 +4,6 @@ CREATE INDEX IF NOT EXISTS idx_mangas_source ON mangas(source); CREATE INDEX IF NOT EXISTS idx_chapters_url ON chapters(url); CREATE INDEX IF NOT EXISTS idx_mangas_categories_manga_id ON mangas_categories(manga_id); CREATE INDEX IF NOT EXISTS idx_mangas_categories_category_id ON mangas_categories(category_id); -CREATE INDEX IF NOT EXISTS idx_excluded_scanlators_scanlator ON excluded_scanlators(scanlator); -CREATE INDEX IF NOT EXISTS idx_manga_sync_manga_id ON manga_sync(manga_id); -CREATE INDEX IF NOT EXISTS idx_history_last_read ON history(last_read); +DROP INDEX IF EXISTS excluded_scanlators_manga_id_index; +CREATE INDEX IF NOT EXISTS idx_excluded_scanlators_manga_id_scanlator ON excluded_scanlators(manga_id, scanlator); +CREATE INDEX IF NOT EXISTS idx_history_last_read ON history(last_read) WHERE last_read > 0; From d8eb71e0041dfd6e654a461d9056977b62db3822 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:19:54 +0700 Subject: [PATCH 10/11] chore(UI): Use remember getUiModel (#1605) * use remember getUiModel * update headerIndexes to use remember for state management * Refactor LibraryUpdateErrorScreen to improve state management * Move header index calculation from the screen model to the presentation layer. * Optimize UI model and header index memoization using `remember(state)`. * Remove redundant `LaunchedEffect` and simplify scroll-to-index logic. * Refactor LibraryUpdateErrorScreen to enhance state management with derivedStateOf --- .../LibraryUpdateErrorScreen.kt | 28 ++++++++++--------- .../presentation/updates/UpdatesScreen.kt | 5 +++- .../LibraryUpdateErrorScreenModel.kt | 5 ---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt index 9c8e3e0c3..db940cafd 100644 --- a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -39,6 +38,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions +import eu.kanade.presentation.libraryUpdateError.components.LibraryUpdateErrorUiModel import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems import eu.kanade.presentation.manga.components.Button import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem @@ -75,31 +75,33 @@ fun LibraryUpdateErrorScreen( val scope = rememberCoroutineScope() val listState = rememberLazyListState() - val headerIndexes = remember { mutableStateOf>(emptyList()) } - LaunchedEffect(state) { - headerIndexes.value = state.getHeaderIndexes() + val uiModels = remember(state) { state.getUiModel() } + val headerIndexes = remember(state) { + uiModels.withIndex() + .filter { it.value is LibraryUpdateErrorUiModel.Header } + .map { it.index } } - val enableScrollToTop by remember { + val enableScrollToTop by remember(listState) { derivedStateOf { listState.firstVisibleItemIndex > 0 } } - val enableScrollToBottom by remember { + val enableScrollToBottom by remember(listState) { derivedStateOf { listState.canScrollForward } } - val enableScrollToPrevious by remember { + val enableScrollToPrevious by remember(headerIndexes, listState) { derivedStateOf { - headerIndexes.value.any { it < listState.firstVisibleItemIndex } + headerIndexes.any { it < listState.firstVisibleItemIndex } } } - val enableScrollToNext by remember { + val enableScrollToNext by remember(headerIndexes, listState) { derivedStateOf { - headerIndexes.value.any { it > listState.firstVisibleItemIndex } + headerIndexes.any { it > listState.firstVisibleItemIndex } } } @@ -141,7 +143,7 @@ fun LibraryUpdateErrorScreen( scrollToPrevious = { scope.launch { listState.scrollToItem( - state.getHeaderIndexes() + headerIndexes .filter { it < listState.firstVisibleItemIndex } .maxOrNull() ?: 0, ) @@ -150,7 +152,7 @@ fun LibraryUpdateErrorScreen( scrollToNext = { scope.launch { listState.scrollToItem( - state.getHeaderIndexes() + headerIndexes .filter { it > listState.firstVisibleItemIndex } .minOrNull() ?: 0, ) @@ -173,7 +175,7 @@ fun LibraryUpdateErrorScreen( state = listState, ) { libraryUpdateErrorUiItems( - uiModels = state.getUiModel(), + uiModels = uiModels, selectionMode = state.selectionMode, onErrorSelected = onErrorSelected, onClick = onClick, diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index 94c30a50a..b6ec128db 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -112,6 +112,9 @@ fun UpdateScreen( else -> { val scope = rememberCoroutineScope() var isRefreshing by remember { mutableStateOf(false) } + // KMK --> + val uiModels = remember(state.items) { state.getUiModel() } + // KMK <-- PullRefresh( refreshing = isRefreshing, @@ -134,8 +137,8 @@ fun UpdateScreen( updatesLastUpdatedItem(lastUpdated) updatesUiItems( - uiModels = state.getUiModel(), // KMK --> + uiModels = uiModels, expandedState = state.expandedState, collapseToggle = collapseToggle, usePanoramaCover = usePanoramaCover, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt index 44ef6bae0..aecec1655 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt @@ -189,11 +189,6 @@ data class LibraryUpdateErrorScreenState( } return uiModels } - - fun getHeaderIndexes(): List = getUiModel() - .withIndex() - .filter { it.value is LibraryUpdateErrorUiModel.Header } - .map { it.index } } @Immutable From bdf81596e9040eee0c3a4c32d81a05e13049ec71 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:30:55 +0700 Subject: [PATCH 11/11] chore: Avoid race conditions and improve bulk selection (#1606) * avoid race conditions * Improve Bulk selection * Consolidate selection mode updates and list clearing into a single state update. --- .../browse/components/BulkFavoriteDialogs.kt | 8 ++------ .../ui/browse/BulkFavoriteScreenModel.kt | 19 +++++++++---------- .../manga/MigrateMangaScreenModel.kt | 9 ++++----- .../LibraryUpdateErrorScreenModel.kt | 9 ++++----- .../ui/updates/UpdatesScreenModel.kt | 13 ++++++++----- .../java/exh/recs/BrowseRecommendsScreen.kt | 2 +- 6 files changed, 28 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BulkFavoriteDialogs.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BulkFavoriteDialogs.kt index 2dfce18a0..c6351677a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BulkFavoriteDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BulkFavoriteDialogs.kt @@ -67,7 +67,6 @@ fun Screen.BulkFavoriteDialogs( AddDuplicateMangaDialog( dialog = dialog, navigator = navigator, - state = bulkFavoriteState, onDismiss = bulkFavoriteScreenModel::dismissDialog, stopRunning = bulkFavoriteScreenModel::stopRunning, toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, @@ -134,10 +133,9 @@ private fun Screen.ShowMigrateDialog( private fun AddDuplicateMangaDialog( dialog: Dialog.AddDuplicateManga, navigator: Navigator?, - state: BulkFavoriteScreenModel.State, onDismiss: () -> Unit, stopRunning: () -> Unit, - toggleSelectionMode: () -> Unit, + toggleSelectionMode: (Boolean) -> Unit, addFavorite: (Manga) -> Unit, showMigrateDialog: (manga: Manga, duplicate: Manga) -> Unit, ) { @@ -147,9 +145,7 @@ private fun AddDuplicateMangaDialog( duplicates = dialog.duplicates, onDismissRequest = onDismiss, onConfirm = { - if (state.selectionMode) { - toggleSelectionMode() - } + toggleSelectionMode(false) addFavorite(dialog.manga) }, onOpenManga = { navigator?.push(MangaScreen(it.id)) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index 1c29aaf72..1b11acb36 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -62,18 +62,17 @@ class BulkFavoriteScreenModel( ) : StateScreenModel(initialState) { fun backHandler() { - toggleSelectionMode() + toggleSelectionMode(false) } fun toggleSelectionMode(newMode: Boolean? = null) { - if (state.value.selectionMode) { - clearSelection() + mutableState.update { state -> + val mode = newMode ?: !state.selectionMode + state.copy( + selectionMode = mode, + selection = if (mode) state.selection else persistentListOf(), + ) } - mutableState.update { it.copy(selectionMode = newMode ?: !it.selectionMode) } - } - - private fun clearSelection() { - mutableState.update { it.copy(selection = persistentListOf()) } } fun select(manga: Manga) { @@ -152,7 +151,7 @@ class BulkFavoriteScreenModel( val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection if (mangaList.isEmpty()) { stopRunning() - toggleSelectionMode() + toggleSelectionMode(false) return@launch } val categories = getCategories() @@ -241,7 +240,7 @@ class BulkFavoriteScreenModel( } stopRunning() } - toggleSelectionMode(newMode = false) + toggleSelectionMode(false) } private fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt index 768eaf21c..d9e2a8f3b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt @@ -149,11 +149,10 @@ class MigrateMangaScreenModel( selectedMangaIds.addOrRemove(it.manga.id, selected) it.copy(selected = selected) } + selectedPositions[0] = -1 + selectedPositions[1] = -1 state.copy(titleList = newItems.toImmutableList()) } - - selectedPositions[0] = -1 - selectedPositions[1] = -1 } fun invertSelection() { @@ -162,10 +161,10 @@ class MigrateMangaScreenModel( selectedMangaIds.addOrRemove(it.manga.id, !it.selected) it.copy(selected = !it.selected) } + selectedPositions[0] = -1 + selectedPositions[1] = -1 state.copy(titleList = newItems.toImmutableList()) } - selectedPositions[0] = -1 - selectedPositions[1] = -1 } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt index aecec1655..b63cf8ab7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt @@ -129,11 +129,10 @@ class LibraryUpdateErrorScreenModel( selectedErrorIds.addOrRemove(it.error.errorId, selected) it.copy(selected = selected) } + selectedPositions[0] = -1 + selectedPositions[1] = -1 state.copy(items = newItems) } - - selectedPositions[0] = -1 - selectedPositions[1] = -1 } fun invertSelection() { @@ -142,10 +141,10 @@ class LibraryUpdateErrorScreenModel( selectedErrorIds.addOrRemove(it.error.errorId, !it.selected) it.copy(selected = !it.selected) } + selectedPositions[0] = -1 + selectedPositions[1] = -1 state.copy(items = newItems) } - selectedPositions[0] = -1 - selectedPositions[1] = -1 } @OptIn(DelicateCoroutinesApi::class) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index 5ed4f222f..5fbaba32d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -461,11 +461,12 @@ class UpdatesScreenModel( selectedChapterIds.addOrRemove(it.update.chapterId, selected) it.copy(selected = selected) } + // KMK --> + selectedPositions[0] = -1 + selectedPositions[1] = -1 + // KMK <-- state.copy(items = newItems.toPersistentList()) } - - selectedPositions[0] = -1 - selectedPositions[1] = -1 } fun invertSelection() { @@ -474,10 +475,12 @@ class UpdatesScreenModel( selectedChapterIds.addOrRemove(it.update.chapterId, !it.selected) it.copy(selected = !it.selected) } + // KMK --> + selectedPositions[0] = -1 + selectedPositions[1] = -1 + // KMK <-- state.copy(items = newItems.toPersistentList()) } - selectedPositions[0] = -1 - selectedPositions[1] = -1 } fun setDialog(dialog: Dialog?) { diff --git a/app/src/main/java/exh/recs/BrowseRecommendsScreen.kt b/app/src/main/java/exh/recs/BrowseRecommendsScreen.kt index a560a4da3..2d91fd2ff 100644 --- a/app/src/main/java/exh/recs/BrowseRecommendsScreen.kt +++ b/app/src/main/java/exh/recs/BrowseRecommendsScreen.kt @@ -124,7 +124,7 @@ class BrowseRecommendsScreen( scrollBehavior = scrollBehavior, // KMK --> toggleSelectionMode = { - bulkFavoriteScreenModel.toggleSelectionMode() + bulkFavoriteScreenModel.toggleSelectionMode(true) }.takeIf { !isExternalSource }, isRunning = bulkFavoriteState.isRunning, // KMK <--