From 4c21f019c06f9b040df198305c5ba6b464b4323a Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Fri, 8 May 2026 12:41:46 +0700 Subject: [PATCH 01/73] chore: Refactor selection state management and improve performance across various screens (#1607) * chore: Refactor selection state management and improve performance across various screens * **MigrateMangaScreen**: Refactor state management to use a `Set` of IDs for selection instead of a list of wrapper items. * **LibraryUpdateErrorScreen**: Simplify `onErrorSelected` signature by removing the unused `userSelected` parameter and clean up related UI components. * **Performance**: Use `remember(state.selection)` in `MigrateMangaScreen` for efficient selection status lookups. * **Code Quality**: Fix minor typos (e.g., `inbetweenItem` to `inBetweenItem`) and optimize imports. * Refactor selection logic to prevent redundant state updates * Move early exit checks for item selection status outside or to the beginning of state update blocks to avoid unnecessary recompositions. * Improve safety and correctness of item selection by returning early from state updates when an item index is not found. * Standardize selection checks across `HistoryScreenModel`, `LibraryUpdateErrorScreenModel`, `MigrateMangaScreenModel`, `UpdatesScreenModel`, and `MangaScreenModel`. * update selection when migration list got updated * Update state management to reset selection and loading status on fetch failure --- .../LibraryUpdateErrorScreen.kt | 2 +- .../components/LibraryUpdateErrorUiItem.kt | 4 +- .../migration/manga/MigrateMangaScreen.kt | 25 +-- .../manga/MigrateMangaScreenModel.kt | 172 +++++++++--------- .../ui/history/HistoryScreenModel.kt | 8 +- .../LibraryUpdateErrorScreenModel.kt | 24 ++- .../tachiyomi/ui/manga/MangaScreenModel.kt | 25 +-- .../ui/updates/UpdatesScreenModel.kt | 21 ++- 8 files changed, 141 insertions(+), 140 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 db940cafd..034d2387c 100644 --- a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt @@ -67,7 +67,7 @@ fun LibraryUpdateErrorScreen( onInvertSelection: () -> Unit, onErrorsDelete: () -> Unit, onErrorDelete: (Long) -> Unit, - onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit, + onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean) -> Unit, navigateUp: () -> Unit, ) { BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) }) diff --git a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt index 1d1f2d13e..c834a61d5 100644 --- a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt @@ -44,7 +44,7 @@ import tachiyomi.presentation.core.util.selectedBackground internal fun LazyListScope.libraryUpdateErrorUiItems( uiModels: List, selectionMode: Boolean, - onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit, + onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean) -> Unit, onClick: (LibraryUpdateErrorItem) -> Unit, onClickCover: (LibraryUpdateErrorItem) -> Unit, onDelete: (Long) -> Unit, @@ -81,7 +81,6 @@ internal fun LazyListScope.libraryUpdateErrorUiItems( selectionMode -> onErrorSelected( libraryUpdateErrorItem, !libraryUpdateErrorItem.selected, - true, false, ) @@ -93,7 +92,6 @@ internal fun LazyListScope.libraryUpdateErrorUiItems( libraryUpdateErrorItem, !libraryUpdateErrorItem.selected, true, - true, ) }, onClickCover = { onClickCover(libraryUpdateErrorItem) }.takeIf { !selectionMode }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt index 4dc5b482f..670b88ec0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt @@ -128,11 +128,9 @@ data class MigrateMangaScreen( // KMK --> bottomBar = { MigrateMangaBottomBar( - selected = state.selection, + selectionMode = state.selectionMode, onMultiMigrateClicked = { - val selection = state.selection - .map { it.manga.id } - navigator.push(MigrationConfigScreen(selection)) + navigator.push(MigrationConfigScreen(state.selection)) }, enableScrollToTop = enableScrollToTop, enableScrollToBottom = enableScrollToBottom, @@ -187,7 +185,7 @@ data class MigrateMangaScreen( contentPadding: PaddingValues, state: MigrateMangaScreenModel.State, // KMK --> - onMangaSelected: (MigrateMangaItem, Boolean, Boolean, Boolean) -> Unit, + onMangaSelected: (Manga, Boolean, Boolean) -> Unit, // KMK <-- onClickItem: (Manga) -> Unit, onClickCover: (Manga) -> Unit, @@ -197,20 +195,23 @@ data class MigrateMangaScreen( contentPadding = contentPadding, ) { items(items = state.titles) { manga -> + // KMK --> + val isSelected = manga.id in state.selection + // KMK <-- MigrateMangaItem( - manga = manga.manga, - isSelected = manga.selected, + manga = manga, + isSelected = isSelected, onClickItem = { // KMK --> when { - state.selectionMode -> onMangaSelected(manga, !manga.selected, true, false) + state.selectionMode -> onMangaSelected(manga, !isSelected, false) // KMK <-- else -> onClickItem(it) } }, onClickCover = onClickCover, // KMK --> - onLongClick = { onMangaSelected(manga, !manga.selected, true, true) }, + onLongClick = { onMangaSelected(manga, !isSelected, true) }, modifier = Modifier.animateItemFastScroll(), // KMK <-- ) @@ -293,7 +294,7 @@ data class MigrateMangaScreen( @Composable private fun MigrateMangaBottomBar( modifier: Modifier = Modifier, - selected: List, + selectionMode: Boolean, onMultiMigrateClicked: () -> Unit, enableScrollToTop: Boolean, enableScrollToBottom: Boolean, @@ -302,7 +303,7 @@ data class MigrateMangaScreen( ) { val scope = rememberCoroutineScope() val animatedElevation by animateDpAsState( - targetValue = if (selected.isNotEmpty()) 3.dp else 0.dp, + targetValue = if (selectionMode) 3.dp else 0.dp, label = "elevation", ) Surface( @@ -350,7 +351,7 @@ data class MigrateMangaScreen( toConfirm = confirm[1], onLongClick = { onLongClickItem(1) }, onClick = onMultiMigrateClicked, - enabled = selected.isNotEmpty(), + enabled = selectionMode, ) Button( title = stringResource(KMR.strings.action_scroll_to_bottom), 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 d9e2a8f3b..b2e4e734a 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 @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga import androidx.compose.runtime.Immutable import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope -import eu.kanade.core.util.addOrRemove import eu.kanade.tachiyomi.source.Source import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -17,6 +16,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority +import mihon.core.common.utils.mutate import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.model.Manga @@ -36,7 +36,6 @@ class MigrateMangaScreenModel( // KMK --> // First and last selected index in list private val selectedPositions: Array = arrayOf(-1, -1) - private val selectedMangaIds: HashSet = HashSet() // KMK <-- init { @@ -50,120 +49,133 @@ class MigrateMangaScreenModel( logcat(LogPriority.ERROR, it) _events.send(MigrationMangaEvent.FailedFetchingFavorites) mutableState.update { state -> - state.copy(titleList = persistentListOf()) + state.copy( + titleList = persistentListOf(), + // KMK --> + selection = emptySet(), + // KMK <-- + ) } } - // KMK --> - .map { manga -> - toMigrationMangaScreenItems(manga) - } - // KMK <-- .map { manga -> manga - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.manga.title }) + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title }) .toImmutableList() } .collectLatest { list -> - mutableState.update { it.copy(titleList = list) } + // KMK --> + mutableState.update { state -> + val titleIds = list.map { it.id }.toSet() + val selection = state.selection.intersect(titleIds).toMutableSet() + updateSelectedPositions(list, selection) + state.copy( + titleList = list, + selection = selection, + ) + } + // KMK <-- } } } - // KMK --> - private fun toMigrationMangaScreenItems(mangas: List): List { - return mangas.map { manga -> - MigrateMangaItem( - manga = manga, - selected = manga.id in selectedMangaIds, - ) - } - } - fun toggleSelection( - item: MigrateMangaItem, + item: Manga, + // KMK --> selected: Boolean, - userSelected: Boolean = false, fromLongPress: Boolean = false, ) { mutableState.update { state -> - val newItems = state.titles.toMutableList().apply { - val selectedIndex = indexOfFirst { it.manga.id == item.manga.id } - if (selectedIndex < 0) return@apply + if (item.id in state.selection == selected) return@update state + val selectedIndex = state.titles.indexOfFirst { it.id == item.id } + if (selectedIndex < 0) return@update state - val selectedItem = get(selectedIndex) - if (selectedItem.selected == selected) return@apply + val selection = state.selection.mutate { list -> + state.titles.run { + val firstSelection = list.isEmpty() + if (selected) list.add(item.id) else list.remove(item.id) - val firstSelection = none { it.selected } - set(selectedIndex, selectedItem.copy(selected = selected)) - selectedMangaIds.addOrRemove(item.manga.id, selected) - - if (selected && userSelected && fromLongPress) { - if (firstSelection) { - selectedPositions[0] = selectedIndex - selectedPositions[1] = selectedIndex - } else { - // Try to select the items in-between when possible - val range: IntRange - if (selectedIndex < selectedPositions[0]) { - range = selectedIndex + 1 until selectedPositions[0] + if (selected && fromLongPress) { + if (firstSelection) { selectedPositions[0] = selectedIndex - } else if (selectedIndex > selectedPositions[1]) { - range = (selectedPositions[1] + 1) until selectedIndex selectedPositions[1] = selectedIndex } else { - // Just select itself - range = IntRange.EMPTY - } + // Try to select the items in-between when possible + val range: IntRange + if (selectedIndex < selectedPositions[0]) { + range = selectedIndex + 1 until selectedPositions[0] + selectedPositions[0] = selectedIndex + } else if (selectedIndex > selectedPositions[1]) { + range = (selectedPositions[1] + 1) until selectedIndex + selectedPositions[1] = selectedIndex + } else { + // Just select itself + range = IntRange.EMPTY + } - range.forEach { - val inBetweenItem = get(it) - if (!inBetweenItem.selected) { - selectedMangaIds.add(inBetweenItem.manga.id) - set(it, inBetweenItem.copy(selected = true)) + range.forEach { + val inBetweenItem = get(it) + if (inBetweenItem.id !in list) { + list.add(inBetweenItem.id) + } } } - } - } else if (userSelected && !fromLongPress) { - if (!selected) { - if (selectedIndex == selectedPositions[0]) { - selectedPositions[0] = indexOfFirst { it.selected } - } else if (selectedIndex == selectedPositions[1]) { - selectedPositions[1] = indexOfLast { it.selected } - } - } else { - if (selectedIndex < selectedPositions[0]) { - selectedPositions[0] = selectedIndex - } else if (selectedIndex > selectedPositions[1]) { - selectedPositions[1] = selectedIndex + } else if (!fromLongPress) { + if (!selected) { + if (selectedIndex == selectedPositions[0]) { + selectedPositions[0] = indexOfFirst { it.id in list } + } else if (selectedIndex == selectedPositions[1]) { + selectedPositions[1] = indexOfLast { it.id in list } + } + } else { + if (selectedIndex < selectedPositions[0]) { + selectedPositions[0] = selectedIndex + } else if (selectedIndex > selectedPositions[1]) { + selectedPositions[1] = selectedIndex + } } } } } - state.copy(titleList = newItems.toImmutableList()) + // KMK <-- + state.copy(selection = selection) } } + // KMK --> + private fun updateSelectedPositions(titles: List, selection: Set) { + if (selection.isEmpty()) { + selectedPositions[0] = -1 + selectedPositions[1] = -1 + return + } + + selectedPositions[0] = titles.indexOfFirst { it.id in selection } + selectedPositions[1] = titles.indexOfLast { it.id in selection } + } + fun toggleAllSelection(selected: Boolean = true) { mutableState.update { state -> - val newItems = state.titles.map { - selectedMangaIds.addOrRemove(it.manga.id, selected) - it.copy(selected = selected) + val selection = if (selected) { + state.titles.mapTo(mutableSetOf()) { it.id } + } else { + emptySet() } selectedPositions[0] = -1 selectedPositions[1] = -1 - state.copy(titleList = newItems.toImmutableList()) + state.copy(selection = selection) } } fun invertSelection() { mutableState.update { state -> - val newItems = state.titles.map { - selectedMangaIds.addOrRemove(it.manga.id, !it.selected) - it.copy(selected = !it.selected) + val selection = state.selection.mutate { list -> + state.titles.forEach { item -> + if (!list.remove(item.id)) list.add(item.id) + } } selectedPositions[0] = -1 selectedPositions[1] = -1 - state.copy(titleList = newItems.toImmutableList()) + state.copy(selection = selection) } } // KMK <-- @@ -177,13 +189,11 @@ class MigrateMangaScreenModel( @Immutable data class State( val source: Source? = null, - private val titleList: ImmutableList? = null, + val selection: Set = emptySet(), + private val titleList: ImmutableList? = null, ) { - // KMK --> - val selection = titles.filter { it.selected } - // KMK <-- - val titles: ImmutableList + val titles: ImmutableList get() = titleList ?: persistentListOf() val isLoading: Boolean @@ -199,11 +209,3 @@ class MigrateMangaScreenModel( sealed interface MigrationMangaEvent { data object FailedFetchingFavorites : MigrationMangaEvent } - -// KMK --> -@Immutable -data class MigrateMangaItem( - val manga: Manga, - val selected: Boolean, -) -// KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt index 4fb192350..34e6d80d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt @@ -298,14 +298,14 @@ class HistoryScreenModel( selectionOptions: HistorySelectionOptions, ) { val (selected, fromLongPress) = selectionOptions - if (item.chapterId in state.value.selection == selected) return mutableState.update { state -> + if (item.chapterId in state.selection == selected) return@update state + val selectedIndex = state.list.indexOfFirst { it.chapterId == item.chapterId } + if (selectedIndex < 0) return@update state + val selection = state.selection.mutate { list -> state.list.run { - val selectedIndex = indexOfFirst { it.chapterId == item.chapterId } - if (selectedIndex < 0) return@run - val firstSelection = list.isEmpty() if (selected) list.add(item.chapterId) else list.remove(item.chapterId) 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 b63cf8ab7..85068c6ea 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 @@ -62,22 +62,20 @@ class LibraryUpdateErrorScreenModel( fun toggleSelection( item: LibraryUpdateErrorItem, selected: Boolean, - userSelected: Boolean = false, fromLongPress: Boolean = false, ) { mutableState.update { state -> + val selectedIndex = state.items.indexOfFirst { it.error.errorId == item.error.errorId } + if (selectedIndex < 0) return@update state + val selectedItem = state.items[selectedIndex] + if (selectedItem.selected == selected) return@update state + val newItems = state.items.toMutableList().apply { - val selectedIndex = indexOfFirst { it.error.errorId == item.error.errorId } - if (selectedIndex < 0) return@apply - - val selectedItem = get(selectedIndex) - if (selectedItem.selected == selected) return@apply - val firstSelection = none { it.selected } set(selectedIndex, selectedItem.copy(selected = selected)) selectedErrorIds.addOrRemove(item.error.errorId, selected) - if (selected && userSelected && fromLongPress) { + if (selected && fromLongPress) { if (firstSelection) { selectedPositions[0] = selectedIndex selectedPositions[1] = selectedIndex @@ -96,14 +94,14 @@ class LibraryUpdateErrorScreenModel( } range.forEach { - val inbetweenItem = get(it) - if (!inbetweenItem.selected) { - selectedErrorIds.add(inbetweenItem.error.errorId) - set(it, inbetweenItem.copy(selected = true)) + val inBetweenItem = get(it) + if (!inBetweenItem.selected) { + selectedErrorIds.add(inBetweenItem.error.errorId) + set(it, inBetweenItem.copy(selected = true)) } } } - } else if (userSelected && !fromLongPress) { + } else if (!fromLongPress) { if (!selected) { if (selectedIndex == selectedPositions[0]) { selectedPositions[0] = indexOfFirst { it.selected } 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 e0ad1faf4..3a886c134 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 @@ -555,8 +555,8 @@ class MangaScreenModel( } val vibrantColor = it.getBestColor() ?: return@launchIO mangaCover.vibrantCoverColor = vibrantColor - updateSuccessState { - it.copy(seedColor = Color(vibrantColor)) + updateSuccessState { state -> + state.copy(seedColor = Color(vibrantColor)) } } } @@ -1729,13 +1729,14 @@ class MangaScreenModel( fromLongPress: Boolean = false, ) { updateSuccessState { successState -> + // KMK --> + val selectedIndex = successState.processedChapters.indexOfFirst { it.id == item.chapter.id } + if (selectedIndex < 0) return@updateSuccessState successState + val selectedItem = successState.processedChapters[selectedIndex] + if (selectedItem.selected == selected) return@updateSuccessState successState + // KMK <-- + val newChapters = successState.processedChapters.toMutableList().apply { - val selectedIndex = successState.processedChapters.indexOfFirst { it.id == item.chapter.id } - if (selectedIndex < 0) return@apply - - val selectedItem = get(selectedIndex) - if ((selectedItem.selected && selected) || (!selectedItem.selected && !selected)) return@apply - val firstSelection = none { it.selected } set(selectedIndex, selectedItem.copy(selected = selected)) selectedChapterIds.addOrRemove(item.id, selected) @@ -1759,10 +1760,10 @@ class MangaScreenModel( } range.forEach { - val inbetweenItem = get(it) - if (!inbetweenItem.selected) { - selectedChapterIds.add(inbetweenItem.id) - set(it, inbetweenItem.copy(selected = true)) + val inBetweenItem = get(it) + if (!inBetweenItem.selected) { + selectedChapterIds.add(inBetweenItem.id) + set(it, inBetweenItem.copy(selected = true)) } } } 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 5fbaba32d..a36426637 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 @@ -379,13 +379,14 @@ class UpdatesScreenModel( val (selected, userSelected, fromLongPress, isGroup, isExpanded) = selectionOptions // KMK <-- mutableState.update { state -> + // KMK --> + val selectedIndex = state.items.indexOfFirst { it.update.chapterId == item.update.chapterId } + if (selectedIndex < 0) return@update state + val selectedItem = state.items[selectedIndex] + if (selectedItem.selected == selected) return@update state + // KMK <-- + val newItems = state.items.toMutableList().apply { - val selectedIndex = indexOfFirst { it.update.chapterId == item.update.chapterId } - if (selectedIndex < 0) return@apply - - val selectedItem = get(selectedIndex) - if (selectedItem.selected == selected) return@apply - val firstSelection = none { it.selected } set(selectedIndex, selectedItem.copy(selected = selected)) selectedChapterIds.addOrRemove(item.update.chapterId, selected) @@ -428,10 +429,10 @@ class UpdatesScreenModel( } range.forEach { - val inbetweenItem = get(it) - if (!inbetweenItem.selected) { - selectedChapterIds.add(inbetweenItem.update.chapterId) - set(it, inbetweenItem.copy(selected = true)) + val inBetweenItem = get(it) + if (!inBetweenItem.selected) { + selectedChapterIds.add(inBetweenItem.update.chapterId) + set(it, inBetweenItem.copy(selected = true)) } } } From a52609b549a0b8f5da857c9a9285f065e1f6b05a Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:22:05 +0700 Subject: [PATCH 02/73] Remove redundant `userSelected` from selection methods (mihonapp-mihon#2976) (cherry picked from commit 9867c160f781b52cd297f7b1202cc6963b375df0) --- .../java/eu/kanade/presentation/manga/MangaScreen.kt | 12 ++++++------ .../eu/kanade/presentation/updates/UpdatesUiItem.kt | 2 -- .../eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt | 5 ++--- .../tachiyomi/ui/updates/UpdatesScreenModel.kt | 7 +++---- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index f4bacf6a2..a0653977f 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -194,7 +194,7 @@ fun MangaScreen( onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, @@ -404,7 +404,7 @@ private fun MangaScreenSmallImpl( onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, @@ -868,7 +868,7 @@ private fun MangaScreenLargeImpl( onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, @@ -1307,7 +1307,7 @@ private fun LazyListScope.sharedChapterItems( // SY <-- onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, - onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) { items( @@ -1376,14 +1376,14 @@ private fun LazyListScope.sharedChapterItems( chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onLongClick = { - onChapterSelected(item, !item.selected, true, true) + onChapterSelected(item, !item.selected, true) haptic.performHapticFeedback(HapticFeedbackType.LongPress) }, onClick = { onChapterItemClick( chapterItem = item, isAnyChapterSelected = isAnyChapterSelected, - onToggleSelection = { onChapterSelected(item, !item.selected, true, false) }, + onToggleSelection = { onChapterSelected(item, !item.selected, false) }, onChapterClicked = onChapterClicked, ) }, diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt index 9e8033f30..8a67b478e 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -171,7 +171,6 @@ internal fun LazyListScope.updatesUiItems( // KMK --> UpdateSelectionOptions( selected = !updatesItem.selected, - userSelected = true, fromLongPress = true, isGroup = isLeader && item.isExpandable, isExpanded = isExpanded, @@ -186,7 +185,6 @@ internal fun LazyListScope.updatesUiItems( // KMK --> UpdateSelectionOptions( selected = !updatesItem.selected, - userSelected = true, fromLongPress = false, isGroup = isLeader && item.isExpandable, isExpanded = isExpanded, 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 3a886c134..55117315f 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 @@ -1725,7 +1725,6 @@ class MangaScreenModel( fun toggleSelection( item: ChapterList.Item, selected: Boolean, - userSelected: Boolean = false, fromLongPress: Boolean = false, ) { updateSuccessState { successState -> @@ -1741,7 +1740,7 @@ class MangaScreenModel( set(selectedIndex, selectedItem.copy(selected = selected)) selectedChapterIds.addOrRemove(item.id, selected) - if (selected && userSelected && fromLongPress) { + if (selected && fromLongPress) { if (firstSelection) { selectedPositions[0] = selectedIndex selectedPositions[1] = selectedIndex @@ -1767,7 +1766,7 @@ class MangaScreenModel( } } } - } else if (userSelected && !fromLongPress) { + } else if (!fromLongPress) { if (!selected) { if (selectedIndex == selectedPositions[0]) { selectedPositions[0] = indexOfFirst { it.selected } 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 a36426637..9aa4d5aa2 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 @@ -362,7 +362,6 @@ class UpdatesScreenModel( /** Bundles all of the boolean flags for update‐selection into one type */ data class UpdateSelectionOptions( val selected: Boolean, - val userSelected: Boolean = false, val fromLongPress: Boolean = false, val isGroup: Boolean = false, val isExpanded: Boolean = false, @@ -376,7 +375,7 @@ class UpdatesScreenModel( // KMK <-- ) { // KMK --> - val (selected, userSelected, fromLongPress, isGroup, isExpanded) = selectionOptions + val (selected, fromLongPress, isGroup, isExpanded) = selectionOptions // KMK <-- mutableState.update { state -> // KMK --> @@ -410,7 +409,7 @@ class UpdatesScreenModel( } // KMK <-- - if (selected && userSelected && fromLongPress) { + if (selected && fromLongPress) { if (firstSelection) { selectedPositions[0] = selectedIndex selectedPositions[1] = selectedIndex @@ -436,7 +435,7 @@ class UpdatesScreenModel( } } } - } else if (userSelected && !fromLongPress) { + } else if (!fromLongPress) { if (!selected) { if (selectedIndex == selectedPositions[0]) { selectedPositions[0] = indexOfFirst { it.selected } From aa7198f082a509b2248a37e0641bcd28822dbe57 Mon Sep 17 00:00:00 2001 From: NarwhalHorns Date: Fri, 20 Feb 2026 16:22:46 +0000 Subject: [PATCH 03/73] Optimize tracked library filter (mihonapp-mihon#2977) Co-authored-by: NarwhalHorns Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> (cherry picked from commit ab214526c6f24466a0432b5c5c7d254a244cd958) --- CHANGELOG.md | 3 +++ .../tachiyomi/ui/library/LibraryScreenModel.kt | 18 +++--------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9139dd71..70e240fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Minimize memory usage by reducing in-memory cover cache size ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2266](https://github.com/mihonapp/mihon/pull/2266)) - Optimize MAL search queries ([@MajorTanya](https://github.com/MajorTanya)) ([#2832](https://github.com/mihonapp/mihon/pull/2832)) - Reword download reindexing message to avoid confusion ([@MajorTanya](https://github.com/MajorTanya)) ([#2874](https://github.com/mihonapp/mihon/pull/2874)) +- Rework internals for better performance ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2955](https://github.com/mihonapp/mihon/pull/2955)) +- Optimize tracked library filter ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2977)) ### Changed - Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773)) @@ -34,6 +36,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Fix reader not saving read duration when changing chapter ([@AntsyLich](https://github.com/AntsyLich), [@KotlinHero](https://github.com/KotlinHero)) ([#2784](https://github.com/mihonapp/mihon/pull/2784)) - Fix pre-1970 upload date display in chapter list ([@MajorTanya](https://github.com/MajorTanya)) ([#2779](https://github.com/mihonapp/mihon/pull/2779)) - Fix crash when trying to install/update extensions while shizuku is not running ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2837](https://github.com/mihonapp/mihon/pull/2837)) +- Fix Add Repo input not taking up the full dialog width ([@cuong-tran](https://github.com/cuong-tran)) ([#2816](https://github.com/mihonapp/mihon/pull/2816)) ### Other - Enable logcat logging on stable and debug builds without enabling verbose logging ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2836](https://github.com/mihonapp/mihon/pull/2836)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index 997d996ab..52df18913 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -450,15 +450,6 @@ class LibraryScreenModel( val filterLewd = preferences.filterLewd // SY <-- - // KMK --> - // Pre-compute track mappings for better performance - val mangaTrackIds = if (!isNotLoggedInAnyTrack && !trackFiltersIsIgnored) { - trackMap.mapValues { entry -> entry.value.map { it.trackerId } } - } else { - emptyMap() - } - // KMK <-- - val filterFnDownloaded: suspend (LibraryItem) -> Boolean = { applyFilter(filterDownloaded) { it.libraryManga.manga.isLocal() || @@ -510,15 +501,12 @@ class LibraryScreenModel( if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true // KMK --> - val mangaTracks = mangaTrackIds[item.id].orEmpty() + val mangaTracks = trackMap[item.id].orEmpty() - // Early return - if (mangaTracks.isEmpty() && includedTracks.isNotEmpty()) return@tracking false + val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it.trackerId in excludedTracks } + val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it.trackerId in includedTracks } // KMK <-- - val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks } - val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks } - !isExcluded && isIncluded } From c290472012eec6364f63d56519cd9e2e975a7f34 Mon Sep 17 00:00:00 2001 From: NarwhalHorns Date: Sat, 21 Feb 2026 10:14:55 +0000 Subject: [PATCH 04/73] Add option for bookmarked chapters to download dropdown (mihonapp/mihon#2891) Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> (cherry picked from commit 3c6f0f1697ccab055ee7af47da84b2161d406f0c) (cherry picked from commit 185cd923c0bf838d55b38ce0ebfb5c010760b880) --- CHANGELOG.md | 1 + .../java/eu/kanade/domain/DomainModule.kt | 2 + .../components/DownloadDropdownMenu.kt | 1 + .../manga/MangaScreenConstants.kt | 1 + .../ui/library/LibraryScreenModel.kt | 68 +++++++++++++++++-- .../tachiyomi/ui/manga/MangaScreenModel.kt | 8 +++ .../GetBookmarkedChaptersByMangaId.kt | 33 +++++++++ .../interactor/GetMergedChaptersByMangaId.kt | 3 + .../moko-resources/base/strings.xml | 1 + 9 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 domain/src/main/java/tachiyomi/domain/chapter/interactor/GetBookmarkedChaptersByMangaId.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 70e240fe8..7cf728ffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Automatically remove downloads on Suwayomi after reading, configurable via extension settings ([@cpiber](https://github.com/cpiber)) ([#2673](https://github.com/mihonapp/mihon/pull/2673)) - Display author & artist name in MAL search results ([@MajorTanya](https://github.com/MajorTanya)) ([#2833](https://github.com/mihonapp/mihon/pull/2833)) - Add filter options to Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#2851](https://github.com/mihonapp/mihon/pull/2851)) +- Add bookmarked chapters to chapter download options ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2891](https://github.com/mihonapp/mihon/pull/2891)) - Add `src:` prefix to search the library by source ID ([@MajorTanya](https://github.com/MajorTanya)) ([#2927](https://github.com/mihonapp/mihon/pull/2927)) - Add `src:local` as a way to search for Local Source entries ([@MajorTanya](https://github.com/MajorTanya)) ([#2928](https://github.com/mihonapp/mihon/pull/2928)) diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 30c3244be..3c1c9e6bf 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -58,6 +58,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetSortModeForCategory import tachiyomi.domain.category.interactor.UpdateCategory import tachiyomi.domain.category.repository.CategoryRepository +import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChapter import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId @@ -161,6 +162,7 @@ class DomainModule : InjektModule { addSingletonFactory { ChapterRepositoryImpl(get()) } addFactory { GetChapter(get()) } addFactory { GetChaptersByMangaId(get()) } + addFactory { GetBookmarkedChaptersByMangaId(get(), get(), get()) } addFactory { GetChapterByUrlAndMangaId(get()) } addFactory { UpdateChapter(get()) } addFactory { SetReadStatus(get(), get(), get(), get(), get()) } diff --git a/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt b/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt index 7bdf97d71..dfe7998b7 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt @@ -44,6 +44,7 @@ private fun DownloadDropdownMenuItems( DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10), DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25), DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread), + DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked), ) options.forEach { (downloadAction, string) -> diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt index 8f337bb2a..c53e254b9 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt @@ -6,6 +6,7 @@ enum class DownloadAction { NEXT_10_CHAPTERS, NEXT_25_CHAPTERS, UNREAD_CHAPTERS, + BOOKMARKED_CHAPTERS, } enum class EditCoverAction { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index 52df18913..668880fb1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -94,6 +94,7 @@ import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category.Companion.UNCATEGORIZED_ID +import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter @@ -134,6 +135,7 @@ class LibraryScreenModel( private val getTracksPerManga: GetTracksPerManga = Injekt.get(), private val getNextChapters: GetNextChapters = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), + private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId = Injekt.get(), private val setReadStatus: SetReadStatus = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), @@ -891,15 +893,19 @@ class LibraryScreenModel( * Queues the amount specified of unread chapters from the list of selected manga */ fun performDownloadAction(action: DownloadAction) { - val mangas = state.value.selectedManga - val amount = when (action) { - DownloadAction.NEXT_1_CHAPTER -> 1 - DownloadAction.NEXT_5_CHAPTERS -> 5 - DownloadAction.NEXT_10_CHAPTERS -> 10 - DownloadAction.NEXT_25_CHAPTERS -> 25 - DownloadAction.UNREAD_CHAPTERS -> null + when (action) { + DownloadAction.NEXT_1_CHAPTER -> downloadNextChapters(1) + DownloadAction.NEXT_5_CHAPTERS -> downloadNextChapters(5) + DownloadAction.NEXT_10_CHAPTERS -> downloadNextChapters(10) + DownloadAction.NEXT_25_CHAPTERS -> downloadNextChapters(25) + DownloadAction.UNREAD_CHAPTERS -> downloadNextChapters(null) + DownloadAction.BOOKMARKED_CHAPTERS -> downloadBookmarkedChapters() } clearSelection() + } + + private fun downloadNextChapters(amount: Int?) { + val mangas = state.value.selectedManga screenModelScope.launchNonCancellable { mangas.forEach { manga -> // SY --> @@ -949,6 +955,54 @@ class LibraryScreenModel( } } + private fun downloadBookmarkedChapters() { + val mangas = state.value.selectedManga + screenModelScope.launchNonCancellable { + mangas.forEach { manga -> + // SY --> + if (manga.source == MERGED_SOURCE_ID) { + val mergedMangas = getMergedMangaById.await(manga.id) + .associateBy { it.id } + getBookmarkedChaptersByMangaId.await(manga.id) + .groupBy { it.mangaId } + .forEach ab@{ (mangaId, chapters) -> + val mergedManga = mergedMangas[mangaId] ?: return@ab + val downloadChapters = chapters.fastFilterNot { chapter -> + downloadManager.queueState.value.fastAny { chapter.id == it.chapter.id } || + downloadManager.isChapterDownloaded( + chapter.name, + chapter.scanlator, + chapter.url, + mergedManga.ogTitle, + mergedManga.source, + ) + } + + downloadManager.downloadChapters(mergedManga, downloadChapters) + } + + return@forEach + } + // SY <-- + + val chapters = getBookmarkedChaptersByMangaId.await(manga.id) + .fastFilterNot { chapter -> + downloadManager.getQueuedDownloadOrNull(chapter.id) != null || + downloadManager.isChapterDownloaded( + chapter.name, + chapter.scanlator, + chapter.url, + // SY --> + manga.ogTitle, + // SY <-- + manga.source, + ) + } + downloadManager.downloadChapters(manga, chapters) + } + } + } + // SY --> fun cleanTitles() { val regex1 = "\\[.*?]".toRegex() 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 55117315f..a17e2f39f 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 @@ -1322,6 +1322,13 @@ class MangaScreenModel( return if (manga.sortDescending()) chaptersSorted.reversed() else chaptersSorted } + private fun getBookmarkedChapters(): List { + val chapterItems = if (skipFiltered) filteredChapters.orEmpty() else allChapters.orEmpty() + return chapterItems + .filter { (chapter, dlStatus) -> chapter.bookmark && dlStatus == Download.State.NOT_DOWNLOADED } + .map { it.chapter } + } + private fun startDownload( chapters: List, startNow: Boolean, @@ -1384,6 +1391,7 @@ class MangaScreenModel( DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10) DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25) DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters() + DownloadAction.BOOKMARKED_CHAPTERS -> getBookmarkedChapters() } if (chaptersToDownload.isNotEmpty()) { startDownload(chaptersToDownload, false) diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetBookmarkedChaptersByMangaId.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetBookmarkedChaptersByMangaId.kt new file mode 100644 index 000000000..a27b3d13f --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetBookmarkedChaptersByMangaId.kt @@ -0,0 +1,33 @@ +package tachiyomi.domain.chapter.interactor + +import exh.source.MERGED_SOURCE_ID +import logcat.LogPriority +import tachiyomi.core.common.util.system.logcat +import tachiyomi.domain.chapter.model.Chapter +import tachiyomi.domain.chapter.repository.ChapterRepository +import tachiyomi.domain.manga.interactor.GetManga + +class GetBookmarkedChaptersByMangaId( + private val chapterRepository: ChapterRepository, + // SY --> + private val getManga: GetManga, + private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId, + // SY <-- +) { + + suspend fun await(mangaId: Long): List { + return try { + // SY --> + val manga = getManga.await(mangaId) ?: return emptyList() + if (manga.source == MERGED_SOURCE_ID) { + return getMergedChaptersByMangaId.await(mangaId, applyFilter = /* KMK --> */false /* KMK <-- */) + .filter { it.bookmark } + } + // SY <-- + chapterRepository.getBookmarkedChaptersByMangaId(mangaId) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + emptyList() + } + } +} diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetMergedChaptersByMangaId.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetMergedChaptersByMangaId.kt index 39a283304..d6658141f 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetMergedChaptersByMangaId.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetMergedChaptersByMangaId.kt @@ -19,6 +19,7 @@ class GetMergedChaptersByMangaId( suspend fun await( mangaId: Long, dedupe: Boolean = true, + /** Filter excluded scanlators & bookmarked/unbookmarked filter */ applyFilter: Boolean = false, ): List { return transformMergedChapters( @@ -31,6 +32,7 @@ class GetMergedChaptersByMangaId( suspend fun subscribe( mangaId: Long, dedupe: Boolean = true, + /** Filter excluded scanlators & bookmarked/unbookmarked filter */ applyFilter: Boolean = false, ): Flow> { return try { @@ -46,6 +48,7 @@ class GetMergedChaptersByMangaId( private suspend fun getFromDatabase( mangaId: Long, + /** Filter excluded scanlators & bookmarked/unbookmarked filter */ applyFilter: Boolean = false, ): List { return try { diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 608a39356..a1466b4f4 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -759,6 +759,7 @@ By upload date Download Unread + Bookmarked Custom cover Cover Cover saved From e50767709a5d79a6e06a386c5a62f1c70a885621 Mon Sep 17 00:00:00 2001 From: NarwhalHorns Date: Sat, 21 Feb 2026 10:26:36 +0000 Subject: [PATCH 05/73] Utilize tracker for library duplicate detection (mihonapp/mihon#2978) * Utilize tracker for library duplicate detection (#2978) Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 89bbdb17fb4ed1cbe99c14f389940e0f91093a10) * Avoid crash due to duplicate manga ID when there are more than 1 trackers * Add index for duplicate library manga detection in manga_sync --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- CHANGELOG.md | 1 + .../sqldelight/tachiyomi/data/manga_sync.sq | 3 +++ .../main/sqldelight/tachiyomi/data/mangas.sq | 17 ++++++++++++++--- .../main/sqldelight/tachiyomi/migrations/45.sqm | 2 ++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cf728ffb..29f65c584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Reword download reindexing message to avoid confusion ([@MajorTanya](https://github.com/MajorTanya)) ([#2874](https://github.com/mihonapp/mihon/pull/2874)) - Rework internals for better performance ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2955](https://github.com/mihonapp/mihon/pull/2955)) - Optimize tracked library filter ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2977)) +- Utilize tracker for library duplicate detection ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2978)) ### Changed - Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773)) diff --git a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq b/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq index 44fa0570b..d5d9799dc 100644 --- a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq +++ b/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq @@ -20,6 +20,9 @@ CREATE TABLE manga_sync( ON DELETE CASCADE ); +-- KMK: index for mangas.sq: getDuplicateLibraryManga's track_dupes +CREATE INDEX idx_manga_sync_sync_id_remote_id ON manga_sync(sync_id, remote_id, 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 bf04d800e..d543b6690 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -119,12 +119,23 @@ AND source = :sourceId; getDuplicateLibraryManga: WITH +track_dupes AS ( + SELECT DISTINCT S2.manga_id + FROM manga_sync S1 + INNER JOIN manga_sync S2 + ON S1.sync_id = S2.sync_id + AND S1.remote_id = S2.remote_id + AND S1.manga_id != S2.manga_id + WHERE S1.manga_id = :id +), duplicates AS ( - SELECT * - FROM mangas + SELECT M.* + FROM mangas M + LEFT JOIN track_dupes D + ON D.manga_id = _id WHERE favorite = 1 AND _id != :id - AND lower(title) LIKE '%' || lower(:title) || '%' + AND (lower(title) LIKE '%' || lower(:title) || '%' OR D.manga_id IS NOT NULL) ), chapter_counts AS ( SELECT diff --git a/data/src/main/sqldelight/tachiyomi/migrations/45.sqm b/data/src/main/sqldelight/tachiyomi/migrations/45.sqm index 2614232b4..56b845447 100644 --- a/data/src/main/sqldelight/tachiyomi/migrations/45.sqm +++ b/data/src/main/sqldelight/tachiyomi/migrations/45.sqm @@ -7,3 +7,5 @@ CREATE INDEX IF NOT EXISTS idx_mangas_categories_category_id ON mangas_categorie 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; + +CREATE INDEX IF NOT EXISTS idx_manga_sync_sync_id_remote_id ON manga_sync(sync_id, remote_id, manga_id); From e04565bc463264559ee60d50953bc4b28c018b1d Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:04:24 +0600 Subject: [PATCH 06/73] Fix migration's selected sources order not preserved (mihonapp/mihon#2993) * Fix migration's selected sources order not preserved (cherry picked from commit 47816d4b218e6a62ef9fcd3097b6b0b8f2f95b17) * Use indexed map for improved sorting performance --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- CHANGELOG.md | 1 + .../feature/migration/config/MigrationConfigScreen.kt | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29f65c584..6773b7c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Fix pre-1970 upload date display in chapter list ([@MajorTanya](https://github.com/MajorTanya)) ([#2779](https://github.com/mihonapp/mihon/pull/2779)) - Fix crash when trying to install/update extensions while shizuku is not running ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2837](https://github.com/mihonapp/mihon/pull/2837)) - Fix Add Repo input not taking up the full dialog width ([@cuong-tran](https://github.com/cuong-tran)) ([#2816](https://github.com/mihonapp/mihon/pull/2816)) +- Fix migration's selected sources order not preserved ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2993)) ### Other - Enable logcat logging on stable and debug builds without enabling verbose logging ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2836](https://github.com/mihonapp/mihon/pull/2836)) diff --git a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt index dcacae77a..c81ca04b6 100644 --- a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt +++ b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt @@ -426,7 +426,7 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { } } - private fun updateSources(save: Boolean = true, action: (List) -> List) { + private fun updateSources(action: (List) -> List) { mutableState.update { state -> val updatedSources = action(state.sources) val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id } @@ -435,7 +435,7 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { // KMK <-- state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources))) } - if (save) saveSources() + saveSources() } private fun initSources() { @@ -451,9 +451,6 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { .asSequence() .filterIsInstance() .filter { it.lang in languages } - // KMK --> - .sortedWith(compareBy { includedSources[it.id] ?: Int.MAX_VALUE }) - // KMK <-- .map { val source = Source( id = it.id, @@ -473,7 +470,9 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { } .toList() - updateSources(save = false) { sources } + mutableState.update { state -> + state.copy(sources = sources.sortedWith(sourcesComparator(includedSources))) + } } fun toggleSelection(id: Long) { From 7c5fe16b529d73b870b8271fa033f9a1604fd307 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:15:13 +0600 Subject: [PATCH 07/73] Fix migration dialog not showing for consecutive prompts from the same screen (mihonapp/mihon#2994) (cherry picked from commit 8b2d35f3068a3f9c5e1bba77ed825ae51531c91c) --- CHANGELOG.md | 1 + .../java/mihon/feature/migration/dialog/MigrateMangaDialog.kt | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6773b7c4e..f1eac5f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Fix crash when trying to install/update extensions while shizuku is not running ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2837](https://github.com/mihonapp/mihon/pull/2837)) - Fix Add Repo input not taking up the full dialog width ([@cuong-tran](https://github.com/cuong-tran)) ([#2816](https://github.com/mihonapp/mihon/pull/2816)) - Fix migration's selected sources order not preserved ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2993)) +- Fix migration dialog not showing for consecutive prompts from the same screen ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2994)) ### Other - Enable logcat logging on stable and debug builds without enabling verbose logging ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2836](https://github.com/mihonapp/mihon/pull/2836)) diff --git a/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt b/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt index 5086d056b..4a65dd0e8 100644 --- a/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt +++ b/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt @@ -154,12 +154,11 @@ private class MigrateDialogScreenModel( } val selectedFlags = sourcePreference.migrationFlags().get() mutableState.update { - it.copy( + State( current = current, target = target, applicableFlags = applicableFlags, selectedFlags = selectedFlags, - isMigrated = false, ) } } From 1c9c35156d0298c87a63e69666aa656ae3a80231 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:28:53 +0600 Subject: [PATCH 08/73] Add all pages of adjacent chapters in the UI instead of only the first or last three (mihonapp/mihon#2995) (cherry picked from commit 0cc724108b4f29a3d1d33ac4666a14873460a657) --- CHANGELOG.md | 1 + .../reader/viewer/pager/PagerViewerAdapter.kt | 20 +++---------------- .../reader/viewer/webtoon/WebtoonAdapter.kt | 18 ++--------------- 3 files changed, 6 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1eac5f3d..c5b35d136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co ### Changed - Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773)) - Add a small increment to chapter number before comparison to fix progress sync issues for Suwayomi ([@cpiber](https://github.com/cpiber)) ([#2657](https://github.com/mihonapp/mihon/pull/2675)) +- Add all pages of adjacent chapters in the UI instead of only the first or last three ([@AntsyLich](https://github.com/AntsyLich)) ([#2995](https://github.com/mihonapp/mihon/pull/2995)) ### Fixed - Fix reader tap zones triggering after scrolling is stopped by tapping ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2680](https://github.com/mihonapp/mihon/pull/2680)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt index 0740598c4..27c09161b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt @@ -74,15 +74,8 @@ class PagerViewerAdapter( val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0 val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0 - // Add previous chapter pages and transition. - if (chapters.prevChapter != null) { - // We only need to add the last few pages of the previous chapter, because it'll be - // selected as the current chapter when one of those pages is selected. - val prevPages = chapters.prevChapter.pages - if (prevPages != null) { - newItems.addAll(prevPages.takeLast(2)) - } - } + // Add previous chapter pages and transition + chapters.prevChapter?.pages?.let(newItems::addAll) // Skip transition page if the chapter is loaded & current page is not a transition page if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) { @@ -124,14 +117,7 @@ class PagerViewerAdapter( } } - if (chapters.nextChapter != null) { - // Add at most two pages, because this chapter will be selected before the user can - // swap more pages. - val nextPages = chapters.nextChapter.pages - if (nextPages != null) { - newItems.addAll(nextPages.take(2)) - } - } + chapters.nextChapter?.pages?.let(newItems::addAll) // Resets double-page splits, else insert pages get misplaced subItems.filterIsInstance().also { subItems.removeAll(it) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt index 5d0ddfc60..799d1a4bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt @@ -49,14 +49,7 @@ class WebtoonAdapter( val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0 // Add previous chapter pages and transition. - if (chapters.prevChapter != null) { - // We only need to add the last few pages of the previous chapter, because it'll be - // selected as the current chapter when one of those pages is selected. - val prevPages = chapters.prevChapter.pages - if (prevPages != null) { - newItems.addAll(prevPages.takeLast(2)) - } - } + chapters.prevChapter?.pages?.let(newItems::addAll) // Skip transition page if the chapter is loaded & current page is not a transition page if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) { @@ -76,14 +69,7 @@ class WebtoonAdapter( newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter)) } - if (chapters.nextChapter != null) { - // Add at most two pages, because this chapter will be selected before the user can - // swap more pages. - val nextPages = chapters.nextChapter.pages - if (nextPages != null) { - newItems.addAll(nextPages.take(2)) - } - } + chapters.nextChapter?.pages?.let(newItems::addAll) updateItems(newItems) } From 0f815ba6624e3589b0300485a47fbc76acde8997 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:05:30 +0600 Subject: [PATCH 09/73] Run automatic library updates even when connected to a VPN (mihonapp/mihon#2996) Co-authored-by: jeremiejig <3978761+jeremiejig@users.noreply.github.com> (cherry picked from commit 039471427448347a1c12c39a488a1127a3ea1497) --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- CHANGELOG.md | 2 ++ .../data/library/LibraryUpdateJob.kt | 21 ++++++++-------- .../main/java/exh/eh/EHentaiUpdateWorker.kt | 25 +++++++++++++------ 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5b35d136..672ba0099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773)) - Add a small increment to chapter number before comparison to fix progress sync issues for Suwayomi ([@cpiber](https://github.com/cpiber)) ([#2657](https://github.com/mihonapp/mihon/pull/2675)) - Add all pages of adjacent chapters in the UI instead of only the first or last three ([@AntsyLich](https://github.com/AntsyLich)) ([#2995](https://github.com/mihonapp/mihon/pull/2995)) +- Going back now first clears search query on browse extension tab ([@cuong-tran](https://github.com/cuong-tran)) ([#2906](https://github.com/mihonapp/mihon/pull/2906)) +- Automatic library updates now run even when connected to a VPN ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773)) ### Fixed - Fix reader tap zones triggering after scrolling is stopped by tapping ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2680](https://github.com/mihonapp/mihon/pull/2680)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 5696a4600..e5c2c5124 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -748,10 +748,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private const val KEY_MANGA_IDS = "manga_ids" // KMK <-- - fun cancelAllWorks(context: Context) { - context.workManager.cancelAllWorkByTag(TAG) - } - fun setupTask( context: Context, prefInterval: Int? = null, @@ -765,16 +761,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } else { NetworkType.CONNECTED } - val networkRequestBuilder = NetworkRequest.Builder() - if (DEVICE_ONLY_ON_WIFI in restrictions) { - networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - } - if (DEVICE_NETWORK_NOT_METERED in restrictions) { - networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + val networkRequest = NetworkRequest.Builder().apply { + removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + if (DEVICE_ONLY_ON_WIFI in restrictions) { + addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + } + if (DEVICE_NETWORK_NOT_METERED in restrictions) { + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + } } + .build() val constraints = Constraints.Builder() // 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used - .setRequiredNetworkRequest(networkRequestBuilder.build(), networkType) + .setRequiredNetworkRequest(networkRequest, networkType) .setRequiresCharging(DEVICE_CHARGING in restrictions) .setRequiresBatteryNotLow(true) .build() diff --git a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt index 2d0b30c53..b0cdba977 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt @@ -40,6 +40,7 @@ import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING +import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_NETWORK_NOT_METERED import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI import tachiyomi.domain.manga.interactor.GetExhFavoriteMangaWithMetadata import tachiyomi.domain.manga.interactor.GetFlatMetadataById @@ -289,17 +290,27 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara val interval = prefInterval ?: exhPreferences.exhAutoUpdateFrequency().get() if (interval > 0) { val restrictions = prefRestrictions ?: exhPreferences.exhAutoUpdateRequirements().get() - val acRestriction = DEVICE_CHARGING in restrictions - - val networkRequestBuilder = NetworkRequest.Builder() - if (DEVICE_ONLY_ON_WIFI in restrictions) { - networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + val networkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { + NetworkType.UNMETERED + } else { + NetworkType.CONNECTED } + val networkRequest = NetworkRequest.Builder().apply { + removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + if (DEVICE_ONLY_ON_WIFI in restrictions) { + addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + } + if (DEVICE_NETWORK_NOT_METERED in restrictions) { + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + } + } + .build() val constraints = Constraints.Builder() // 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used - .setRequiredNetworkRequest(networkRequestBuilder.build(), NetworkType.CONNECTED) - .setRequiresCharging(acRestriction) + .setRequiredNetworkRequest(networkRequest, networkType) + .setRequiresCharging(DEVICE_CHARGING in restrictions) + .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder( From 2b4caa9f97e94fb4d3f6f3d82566309955410901 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 26 Feb 2026 13:24:45 +0100 Subject: [PATCH 10/73] Translations update from Hosted Weblate (mihonapp/mihon#2997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cs/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/ Translation: Mihon/Mihon Co-authored-by: Filip Jaruška Co-authored-by: Nguyễn Trung Đức Co-authored-by: ɴᴇᴋᴏ (cherry picked from commit cc3f640faa7df9f2a64976d2639e6dfd60ebe213) --- .../commonMain/moko-resources/cs/strings.xml | 47 ++++++++++++++++++- .../commonMain/moko-resources/vi/strings.xml | 2 + .../moko-resources/zh-rTW/strings.xml | 2 + 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/i18n/src/commonMain/moko-resources/cs/strings.xml b/i18n/src/commonMain/moko-resources/cs/strings.xml index 5067b588b..2131871b0 100644 --- a/i18n/src/commonMain/moko-resources/cs/strings.xml +++ b/i18n/src/commonMain/moko-resources/cs/strings.xml @@ -710,7 +710,7 @@ Synchronizace knihovny dokončena Přihlášení sledovače Klepněte zde pro pomoc s Cloudflare - Index stažených zneplatněn + Obnovování indexu stažených Nelze vytvořit soubor zálohy Bez připojení k internetu Nelze dosáhnout %s @@ -869,4 +869,49 @@ Zajišťuje kompatibilitu s určitými úložnými médii, která nepodporují Unicode. Pokud je tato funkce povolena, budete muset ručně přejmenovat názvy složkek source a manga tak, že nahradíte non-ASCII znaky jejich malými hexadecimálními znaky v UTF-8. Názvy souborů kapitol není nutné přejmenovávat. Souběžné stahování zdrojů Souběžné stahování stránek + Zavřít okno + Vypočítávání… + Souběžně stahované stránky na zdroj + Vybrat vše + Vybrat žádné + Vybrat povolené zdroje + Vybrat připnuté zdroje + Pokračovat + Data pro migraci + Smazat stažené položky po migraci + Migrace + Migrace (%1$d/%2$d) + Kopírovat + Migrovat + Nebyly nalezeny žádné alternativy + Poslední: %1$s + Neznámý + Vyhledat manuálně + Nemigrovat + Migrovat nyní + Kopírovat nyní + Zastavit migraci? + Zastavit + Zrušit + Kopírovat + Migrovat + Zrušit + Zrušit + Žádné kapitoly nebyly nalezeny, tuto položku nelze použít pro migraci + Založeno + Filtrovat vyloučené scanlatory + Vybrané + Dostupné + Dodatečná klíčová slova (volitelné) + Pomáhá zúžit výsledky vyhledávání přidáním dalších klíčových slov + Skrýt položky bez shody + Skrýt položky bez nových kapitol + Zobrazit položku, pouze pokud má shoda další kapitoly + Tyto možnosti jsou pomalé, nebezpečné a mohou vést k omezení ze strany zdrojů + Pokročilý režim vyhledávání + Rozdělí název na klíčová slova pro rozsáhlejší vyhledávání + Shoda podle čísla kapitoly + Je-li povoleno, vybere shodu, která je nejdále. V opačném případě vybere první shodu podle priority zdroje. + Zobrazovat obrázky v popisu mangy + Přečtené kapitoly a postup u položek mimo knihovnu budou ztraceny diff --git a/i18n/src/commonMain/moko-resources/vi/strings.xml b/i18n/src/commonMain/moko-resources/vi/strings.xml index abe1ee8ff..c37cad67c 100644 --- a/i18n/src/commonMain/moko-resources/vi/strings.xml +++ b/i18n/src/commonMain/moko-resources/vi/strings.xml @@ -912,4 +912,6 @@ Các trang được tải xuống đồng thời theo từng nguồn Đóng thẻ Lọc nhóm dịch bị loại trừ + Đang tính… + Đã đánh dấu diff --git a/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml b/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml index 2217081b1..79d81a9b6 100644 --- a/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml +++ b/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -903,4 +903,6 @@ 每個來源同時下載的頁數 關閉頁籤 篩選排除掃譯者 + 計算中… + 已新增書籤 From 0830cd32b59282fc47dc77f689fdbfa885743c87 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 27 Feb 2026 17:25:48 +0100 Subject: [PATCH 11/73] Translations update from Hosted Weblate (jobobby04/TachiyomiSY#1536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/ar/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/ko/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ar/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/es/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fil/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fr/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/hr/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/it/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ko/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ru/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/tr/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hant/ Translation: Mihon/TachiyomiSY Translation: Mihon/TachiyomiSY Plurals Co-authored-by: Champ0999 Co-authored-by: Dexroneum Co-authored-by: Erick Alejandro Co-authored-by: Frosted Co-authored-by: Infy's Tagalog Translations Co-authored-by: Luis Antonio Co-authored-by: Metin Co-authored-by: Milo Ivir Co-authored-by: NormalRandomPeople Co-authored-by: Swyter Co-authored-by: ZerOriSama Co-authored-by: momoehab Co-authored-by: 안세훈 (cherry picked from commit 724a61f51310b297296aab3c5c5d6d3a47b92491) --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- .../commonMain/moko-resources/es/strings.xml | 2 +- .../commonMain/moko-resources/ar/plurals.xml | 8 +- .../commonMain/moko-resources/ar/strings.xml | 25 ++- .../commonMain/moko-resources/es/strings.xml | 139 ++++++++--------- .../commonMain/moko-resources/fil/strings.xml | 2 + .../commonMain/moko-resources/fr/strings.xml | 10 +- .../commonMain/moko-resources/hr/strings.xml | 8 +- .../commonMain/moko-resources/it/strings.xml | 4 + .../commonMain/moko-resources/ko/plurals.xml | 54 +++++++ .../commonMain/moko-resources/ko/strings.xml | 146 ++++++++++++++++++ .../moko-resources/pt-rBR/strings.xml | 11 +- .../commonMain/moko-resources/ru/strings.xml | 2 + .../commonMain/moko-resources/tr/strings.xml | 4 +- .../moko-resources/zh-rCN/strings.xml | 2 + .../moko-resources/zh-rTW/strings.xml | 2 + 15 files changed, 323 insertions(+), 96 deletions(-) create mode 100644 i18n-sy/src/commonMain/moko-resources/ko/plurals.xml create mode 100644 i18n-sy/src/commonMain/moko-resources/ko/strings.xml diff --git a/i18n-kmk/src/commonMain/moko-resources/es/strings.xml b/i18n-kmk/src/commonMain/moko-resources/es/strings.xml index 267ee0a81..fd83f9a4e 100644 --- a/i18n-kmk/src/commonMain/moko-resources/es/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/es/strings.xml @@ -25,7 +25,7 @@ ¡Próximas actualizaciones (pronto)! Registro de cambios Más así... - Demasiadas fuentes en tu feed, no se pueden agregar más de 20 + Demasiadas fuentes en novedades, no se pueden poner más de 20 Actual: %1$s Tema según la portada Spot Tonal diff --git a/i18n-sy/src/commonMain/moko-resources/ar/plurals.xml b/i18n-sy/src/commonMain/moko-resources/ar/plurals.xml index fa0582be1..c3d3f6dde 100644 --- a/i18n-sy/src/commonMain/moko-resources/ar/plurals.xml +++ b/i18n-sy/src/commonMain/moko-resources/ar/plurals.xml @@ -91,10 +91,10 @@ وقت الإنغلاق ‬وقت الإغلاق - - - - + وقتان للإغلاق + %1$d أوقات للإغلاق + %1$d وقتًا للإغلاق + %1$d وقت للإغلاق ترحيل ‫ المدخل؟ diff --git a/i18n-sy/src/commonMain/moko-resources/ar/strings.xml b/i18n-sy/src/commonMain/moko-resources/ar/strings.xml index ae6f66665..361f89c45 100644 --- a/i18n-sy/src/commonMain/moko-resources/ar/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/ar/strings.xml @@ -215,7 +215,7 @@ استخدم شبكة Hentai@Home يتم حاليًا عرض العناوين الإنجليزية/الحروف اللاتينية في نتائج البحث. امسح ذاكرة التخزين المؤقت للفصل بعد تغيير هذا (في القسم المتقدم) فئاتExHentai التي شاهدتها - يجب ان تكون القيمة بين صفر و ٩٩٩٩‐ + يجب أن تكون القيمة بين -٩٩٩٩ و ٠! سيتم تضمين المعارض التي تم تحميلها مؤخرًا على شاشة المشاهدة إذا كانت تحتوي على علامة مشاهدة واحدة على الأقل ذات وزن إيجابي، ويصل مجموع الأوزان على العلامات التي تمت مشاهدتها إلى هذه القيمة أو أعلى. يمكن تعيين هذه العتبة بين ٠ و٩٩٩٩. حاليًا القيمة :%1$d يجب ان تكون القيمة بين صفر و ٩٩٩٩ ! إذا كنت ترغب في إخفاء المعارض التى بلغات معينة من قائمة المعرض والبحث، اختارها في الحوار الذى سوف يظهر. @@ -245,16 +245,7 @@ عرض إحصائيات التحديث جارٍ جمع الإحصائيات… إحصائيات تحديث المعرض - " -\nالمعارض التي تم فحصها في الماضي -\n- ساعة : %1$d -\n-٦ ساعات : %2$d -\n-١٢ ساعة : %3$d -\n-يوم : %4$d -\n-يومين: %5$d -\n-أسبوع : %6$d -\n-شهر : %7$d -\n-سنة : %8$d" + \nالمعارض التي تم فحصها في الماضي\n\n- ساعة : %1$d\n\n-٦ ساعات : %2$d\n\n-١٢ ساعة : %3$d\n\n-يوم : %4$d\n\n-يومين: %5$d\n\n-أسبوع : %6$d\n\n-شهر : %7$d\n\n-سنة : %8$d تخطي إعادة تصميم الصفحة تحميل الإعدادات إلى الخادم حدث خطأ أثناء عملية التكوين:%1$s @@ -538,7 +529,7 @@ ملاحظات مزامنة المفضلة الهامة إنهاء \'%1$s\'%2$s - رائع + مدهش قصة بديلة عناوين متشابهة مصدر MangaDex المفضل @@ -575,7 +566,7 @@ ادخل إلى مؤلم لا تقييم - ‬الفنان + فنان رسومات حاسوبية النوع خطأ في تنزيل الفصول اندماج المراجع غير صالح @@ -636,10 +627,16 @@ مجموعات المسح للعرض تدور خارج دوجينشي - Non-H + غير إباحي إباحيات آسياوية المُعرّف is Exhentai gallery %1$s مترجم دوجينشي + اسم الملف + امتداد الملف + عنوان URL الأساسي + الفصل الأخير + تضمين تجزئة عنوان URL للفصل + أضف الأحرف الستة الأولى من تجزئة MD5 الخاصة بعنوان URL الخاص بالفصل إلى اسم ملف أو مجلد الفصل. diff --git a/i18n-sy/src/commonMain/moko-resources/es/strings.xml b/i18n-sy/src/commonMain/moko-resources/es/strings.xml index 3de10f48c..2c2a0c77c 100644 --- a/i18n-sy/src/commonMain/moko-resources/es/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/es/strings.xml @@ -1,6 +1,6 @@ - Buscar manualmente + Búsqueda manual Migrar ahora Limpiar títulos Editar información @@ -19,7 +19,7 @@ Solo clientes de puerto predeterminado Mostrar títulos japoneses en los resultados de búsqueda ¡Debe estar entre -9999 y 0! - ¡Debe estar entre 0 y 9999! + Escribe un número entre 0 y 9999 1280x Exploración E/ExHentai mejorada No migrar @@ -28,7 +28,7 @@ Webtoon Manga Todas las fuentes - Ajustes de Bifurcación + Ajustes de esta versión de la aplicación Use la red Hentai@Home Desea cargar imágenes a través de la red de Hentai@Home, si está disponible? Deshabilitar esta opción reducirá la cantidad de páginas que puede ver \nOpciones: @@ -39,27 +39,25 @@ Utiliza imágenes originales Actualmente usando imágenes originales Etiquetas vistas - Filtrado de idioma + Filtrar idiomas 1600x Categorías de la página principal 2400x La calidad de las imágenes descargadas Calidad de imagen - Auto + Automática 980x Desactivar la subida de favoritos 780x Actualmente se muestran los títulos en inglés/romanizado en los resultados de búsqueda. Borra la caché de capítulos después de cambiar esto (en la sección Avanzado) Abre una vista web a tu página de etiquetas vistas de E/ExHentai - ¿Qué categorías te gustaría mostrar por defecto en la página principal y en las búsquedas? Aún pueden habilitarse activando sus filtros - Si deseas ocultar galerías en ciertos idiomas de la lista de galerías y búsquedas, selecciónalas en el cuadro de diálogo que aparecerá. -\nTen en cuenta que las galerías coincidentes nunca aparecerán, sin importar tu consulta de búsqueda. -\nTldr marcado = excluir + ¿Qué categorías te gustaría mostrar por defecto en la página principal y en las búsquedas? El resto se puede seguir viendo activando filtros + Si deseas ocultar galerías en ciertos idiomas de la lista de galerías y búsquedas, márcalas a continuación. \nTen en cuenta que las galerías que marcadas nunca aparecerán, aunque las busques. \nEn pocas palabras: si está marcado = se excluye Etiquetas Observadas en ExHentai Umbral de Filtrado de Etiquetas - Umbral de Monitoreo de Etiquetas + Umbral de monitoreo de etiquetas Las galerías recientemente subidas se incluirán en la pantalla de observación si tienen al menos una etiqueta observada con peso positivo, y la suma de los pesos de sus etiquetas observadas alcanza este valor o es mayor. Este umbral se puede establecer entre 0 y 9999. Actualmente: %1$d - Estado Predeterminado del Filtro de la Lista Observada + Estado predeterminado del filtro de la lista de favoritos Al navegar en ExHentai/E-Hentai, ¿debería estar habilitado el filtro de la lista de seguimiento de forma predeterminada? Habilitar/Deshabilitar el menú de navegación mejorado hecho para E/ExHentai Sincronización de Favoritos de E-Hentai @@ -67,15 +65,15 @@ Los favoritos solo se descargan desde ExHentai. Cualquier cambio en los favoritos en la aplicación no se cargará. Previene la pérdida accidental de favoritos en ExHentai. Ten en cuenta que las eliminaciones aún se descargarán (si eliminas un favorito en ExHentai, también se eliminará en la aplicación). Mostrar notas sincronizadas de favoritos Mostrar información sobre la función de sincronización de favoritos - ¡Por favor inicia sesión! + ¡Tienes que iniciar sesión! Ignorar errores de sincronización cuando sea posible - Forzar el restablecimiento del estado de sincronización - Restablecimiento del estado de sincronización + Restablece el estado de sincronización + Restablecer estado de sincronización No canceles el proceso de sincronización inmediatamente si encuentras errores. Los errores se seguirán mostrando cuando se complete la sincronización. En algunos casos, puede provocar la pérdida de favoritos. Resulta útil cuando se sincronizan bibliotecas grandes. Realiza una resincronización completa en la próxima sincronización. Las eliminaciones no se sincronizarán. Todos los favoritos de la aplicación se volverán a cargar en ExHentai y todos los favoritos de ExHentai se volverán a descargar en la aplicación. Útil para reparar la sincronización después de que se haya interrumpido. - Comprobador de actualizaciones de la galería - Restricciones para actualizaciones automáticas - Tiempo entre actualizaciones + Comprobar actualizaciones de la galería + Restringir actualizaciones automáticas + Intervalo de tiempo entre actualizaciones Nunca actualizar las galerías 1 hora 2 horas @@ -91,21 +89,19 @@ El actualizador ejecutó %1$s por última vez, y comprobó %2$d de las %3$d galerías que estaban listas para ser comprobadas. El actualizador aún no se ha ejecutado. Nota de perfil de configuración - ¡Ajustes cargados correctamente! - ¡Error en la configuración! + ¡Los ajustes se han subido! + ¡Hubo un error en la configuración! Se ha producido un error durante el proceso de configuración: %1$s - Cargar la configuración en el servidor + Subiendo ajustes al servidor %1$s comprueba/actualiza las galerías por lotes. Esto significa que esperará %2$d hora(s), comprobará %3$d galerías, esperará %2$d hora(s), comprobará %3$d y así sucesivamente… - \nGalerías que se comprobaron en:\n- hora: %1$d\n- 6 horas: %2$d\n- 12 horas: %3$d\n- día: %4$d\n- 2 días: %5$d\n- semana: %6$d\n- mes: %7$d\n- año: %8$d - La aplicación añadirá ahora un nuevo perfil de configuración en E-Hentai y ExHentai para optimizar el rendimiento de la aplicación. Asegúrate de tener menos de tres perfiles en ambos sitios. -\n -\nSi no tienes ni idea de lo que son los perfiles de configuración, probablemente no importe, simplemente pulsa \"Aceptar\". - Por favor espere, esto puede tardar algún tiempo… - ¡No tienes espacios en tu perfil %1$s, por favor elimine un perfil! - Vuelva a verificar el estado de inicio de sesión + \nGalerías que se comprobaron hace:\n- una hora: %1$d\n- 6 horas: %2$d\n- 12 horas: %3$d\n- un día: %4$d\n- 2 días: %5$d\n- una semana: %6$d\n- un mes: %7$d\n- un año: %8$d + La aplicación añadirá ahora un nuevo perfil de configuración en E-Hentai y ExHentai para optimizar el rendimiento de la aplicación. Asegúrate de tener menos de tres perfiles en ambos sitios. \n \nSi no tienes ni idea de lo que son los perfiles de configuración no te preocupes por esto y pulsa «Aceptar». + Espera un momento, puede tardar un poco… + ¡No tienes espacio en tu perfil %1$s, primero elimina un perfil ya existente! + Recomprobar estado de inicio de sesión Página de inicio de sesión alternativa Saltar el cambio de estilo de la página - Cookie ígneo personalizado + Cookie «igneous» personalizada Algunos usuarios no pueden acceder a ExHentai de la manera normal y deben ingresar un valor específico de cookie ígneo. Esta opción es para esos usuarios. Herramientas de desarrollador Activar funciones integradas de hentai @@ -113,51 +109,49 @@ Activar fuentes delegadas Aplicar %1$s mejoras a las siguientes fuentes si están instaladas: %2$s Nivel de registro - Cambiar esto puede afectar el rendimiento de la aplicación. Reinicie la aplicación forzosamente después de realizar el cambio. Valor actual: %s + Cambiar esto puede afectar el rendimiento de la aplicación. Reinicia la aplicación manualmente tras el cambio. Valor actual: %s Activar lista negra de fuentes - Ocultar extensiones/fuentes que son incompatibles con %1$s. Reinicie la aplicación forzosamente después de realizar el cambio. + Ocultar extensiones/fuentes que son incompatibles con %1$s. Reinicia la aplicación manualmente tras el cambio. Abrir menú de depuración - ¡PUEDE CORROMPER SU BIBLIOTECA!]]> - Comenzando la limpieza + ¡PUEDES CORROMPER TU BIBLIOTECA!]]> + Comenzando el borrado Limpiar capítulos descargados - Eliminar carpetas de capítulos inexistentes, parcialmente descargadas y leídas - No hay carpetas para limpiar - Limpiar huérfanos - Limpiar leídos - Limpiar entradas no en la biblioteca + Elimina carpetas de capítulos inexistentes, parcialmente descargados, o ya leídos + No hay ninguna carpeta que limpiar + Borrar descargas huérfanas + Borrar ya leídos + Borrar lo que no esté en la biblioteca Ahorro de datos - Comprimir imágenes antes de descargarlas o cargarlas en el lector - Usar ahorrador de datos en el descargador + Comprimir imágenes antes de descargarlas o verlas en el visor + Descargar con el ahorrador de datos Ignorar imágenes JPEG Ignorar animaciones GIF Calidad de imagen - Comprimir a Jpeg - El tamaño del archivo Jpeg es considerablemente más pequeño que el de Webp (lo que significa que se guarda más datos), pero también hace que las imágenes pierdan más calidad. -\nActualmente, se comprime a Jpeg - Valores más altos significan que se guarda un mayor porcentaje de la calidad de la imagen, pero también implica que el tamaño del archivo es mayor; un 80 por ciento es una buena media entre el tamaño del archivo y la calidad de la imagen - El tamaño de; archivo JPEG es considerablemente mas pequeño que el WEBP (lo que significa que ser guardan mas datos), pero tambien hace que las imagenes pierdan mucha calidad. -\nActualmente se comprimen en WEBP + Comprimir como JPEG + El tamaño de un archivo JPEG es considerablemente más pequeño que el de WebP (lo que significa que te ahorras datos de descarga), pero también hace que las imágenes pierdan más calidad.\nAhora mismo se comprime utilizando JPEG. + Al poner un porcentaje mayor aumenta la calidad de la imagen, pero también lo hace el tamaño en disco. Dejarlo en 80% es un término medio entre tamaño aceptable y con calidad. + El tamaño de un archivo JPEG es considerablemente más pequeño que el de WebP (lo que significa que te ahorras datos de descarga), pero también hace que las imágenes pierdan más calidad.\nAhora mismo se comprime utilizando WEBP. Convertir en Blanco & Negro Bandwidth Hero ( Requiere un servidor proxy ) wsrv.nl - Pom aqui la URl del Servidor Proxy de Bandwidth Hero + Pega aquí la dirección URL del servidor proxy de Bandwidth Hero Conservar obras con capítulos leídos Servidor Proxy de Bandwidth Hero - Minimo + Mínimo Extra Extremo - Solo errores criticos - Todos los registros - Modo de inspeccion de red + Solo errores críticos + Registrar todos los mensajes + Modo de inspección de red Ampliar todos los filtros de búsqueda de forma predeterminada - Vista previa del recuento de filas + Filas en vista previa Mostrar actualizaciones en la barra de navegación Mostrar historial en la barra de navegación Mostrar siempre las etiquetas de navegación Barra de navegación - Mostrar recomendaciones en el menú - Coloque el botón de recomendaciones en el menú adicional en lugar de en la página de entrada - Fusionar en el menú adicional + Recomendaciones en menú lateral + Poner el botón de recomendaciones en el menú lateral en vez de en la página de la obra + Fusionar en el menú lateral Coloque el botón de fusión en el menú adicional en lugar de en la página de entrada Ajustes de ordenación Utilizar las últimas preferencias y fuentes guardadas antes de la migración para migrar @@ -168,7 +162,7 @@ Marcar nuevos capítulos duplicados como leídos Marcar automáticamente nuevos capítulos como leídos si se han leído antes Cada hora - Ocultar pestaña Feed + Ocultar pestaña de novedades Filtrar las fuentes que están en categorías, haciendo que las fuentes no se pongan debajo del idioma si están en una categoría Reemplace el botón más reciente con una vista de exploración personalizada que incluya tanto lo más reciente como la exploración Todas las obras leídas @@ -177,8 +171,8 @@ Cada 30 minutos Filtrar las fuentes en categorías Cada 3 horas - Posición de la pestaña Feed - ¿Quieres que la pestaña feed sea la primera pestaña en navegar? Esto hará que sea la pestaña predeterminada al abrir la navegación, no se recomienda si está con datos móviles o una red medida + Posición de la pestaña de novedades + ¿Quieres que la pestaña de novedades sea la inicial? No te recomendamos que lo actives si tienes datos móviles o datos limitados Reemplazar el botón más reciente Carpetas ocultas de fuente local Permite a la fuente local leer carpetas ocultas @@ -217,10 +211,10 @@ Editar horarios de bloqueo No tienes horarios de bloqueo biométrico. Pulsa el botón «+» para añadir uno. ¡Una hora de bloqueo entra en conflicto con otra existente! - Introducir hora de inicio - Introducir hora de finalización + Introduce una hora de inicio + Introduce una hora de finalización Eliminar intervalo de tiempo - ¿Deseas eliminar el intervalo de tiempo %s? + ¿Quieres borrar el intervalo de tiempo %s? Días de bloqueo biométrico Días para tener la aplicación bloqueada Domingo @@ -242,7 +236,7 @@ Jueves Cifrar base de datos Viernes - Requiere reiniciar la aplicación para que surta efecto + Es necesario reiniciar la aplicación para que surta efecto ACTIVAR ESTO CREARÁ UNA NUEVA BASE DE DATOS. USA ESTOS PASOS PARA MANTENER TUS DATOS
1. AJUSTES -> COPIA DE SEGURIDAD -> CREAR
2. AJUSTES DEL SISTEMA -> BORRAR LOS DATOS DE LA APLICACIÓN
3. ABRIR LA APLICACIÓN Y ACTIVAR ESTO
4. AJUSTES DEL SISTEMA -> FORZAR REINICIO
5. AJUSTES -> COPIA DE SEGURIDAD -> RESTAURAR]]>
Descargas protegidas por contraseña Encripta las descargas de archivos CBZ con la contraseña dada.\nADVERTENCIA: LOS DATOS DE LOS ARCHIVOS SE PERDERÁN PARA SIEMPRE SI SE OLVIDA LA CONTRASEÑA @@ -435,7 +429,7 @@ Eliminando %1$d galerías del servidor remoto ¡No se pudo eliminar las galerías del servidor remoto! Estado de seguimiento - Sin seguir + Sin seguimiento Sincronizar favoritos de EH ¿Estás seguro? Restablecer el estado de sincronización puede hacer que la próxima sincronización sea extremadamente lenta. @@ -490,7 +484,7 @@ Derivado de Historia alternativa Versión alternativa - ¿Añadir %1$s al feed? + ¿Añadir %1$s a novedades? Error: %1$s No se pudo abrir esta entrada:\n\n%1$s Iniciando aplicación… @@ -549,11 +543,11 @@ Recortar el borde vertical hace unos instantes Recortar bordes Webtoon - Feed - ¿Borrar artículo de feed? + Novedades + ¿Borrar el elemento de novedades? ¡Lee esto! ¡Etiquetas deben ser exactas, no hay coincidencias parciales, no puedes hacer netorare para excluir mujer:netorare o similar!\nEl estilo para etiquetas de nombre es\n\"mujer: solo mujer\"\n¡sin citas!\n¡Se pueden añadir multiples variantes de la misma etiqueta, así que puedes hacer \"etiqueta:netorare\" para NHentai y \"mujer:netorare\" para E-Hentai! Selecciona ninguno - No tiene fuentes en su feed, navegar a la parte superior derecha para añadir una + No tienes ninguna fuente en novedades, añade alguna desde la parte superior derecha Saltar pre-migración Buscar parámetro (p. ej. idioma:inglés) Lascivo @@ -584,7 +578,7 @@ Número de páginas Padre Cargador - URL + Dirección URL Cargador principal Seguir estado %1$s TR @@ -610,7 +604,7 @@ Monocromo El título no tiene seguimiento. Elige un servicio de seguimiento - Rellenar desde el serv. de seguimiento + Rellenar desde seguim. No se puede añadir la galería al servidor remoto: \'%1$s\' (GID: %2$s)! La búsqueda no se ha podido completar Recomendaciones en común @@ -626,10 +620,13 @@ Recomendaciones de la fuente Servicios de recomendación en los que buscar Ocultar elementos que ya estén en la biblioteca - Encuentra el título automáticamente si la fuente ya enlaza con el servicio de seguimiento. De momento solo funciona en MangaDex - Elegir títulos a través de los metadatos de la fuente + Encuentra la obra automáticamente si la fuente ya enlaza con el servicio de seguimiento. De momento solo funciona en MangaDex + Elegir obras a través de los metadatos de la fuente Escanear un código QR Capítulo final Extensión de archivo - URL base + Dirección URL base + Nombre de archivo + Añade las primeras seis letras del hash MD5 de la dirección URL del capítulo al nombre de archivo o carpeta local. + Incluir el hash de la URL del capítulo
diff --git a/i18n-sy/src/commonMain/moko-resources/fil/strings.xml b/i18n-sy/src/commonMain/moko-resources/fil/strings.xml index 0af9d883f..990096a14 100644 --- a/i18n-sy/src/commonMain/moko-resources/fil/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/fil/strings.xml @@ -651,4 +651,6 @@ Extension ng file Huling Kabanata Base url + Isama ang hash ng URL ng kabanata + Idagdag ang unang anim na karakter ng MD5 hash ng URL ng kabanata sa pangalan ng file o folder ng kabanata. diff --git a/i18n-sy/src/commonMain/moko-resources/fr/strings.xml b/i18n-sy/src/commonMain/moko-resources/fr/strings.xml index 4f56545de..3106ba86b 100644 --- a/i18n-sy/src/commonMain/moko-resources/fr/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/fr/strings.xml @@ -143,8 +143,8 @@ Paramètres de tri Utilisez les dernières préférences et sources de pré-migration enregistrées pour migrer en masse Mises à jour des catégories dynamiques de la bibliothèque - Lancez toujours les mises à jour mondiales - Lancer des mises à jour globales uniquement pour les mises à jour de catégories non groupées pour les autres + Toujours lancer des mises à jour globales + Lancer des mises à jour globales uniquement pour les entrées non regroupées, lancer des mises à jour par catégorie pour les autres Lancer des mises à jour de catégorie tout le temps Remplacer le dernier bouton @@ -669,4 +669,10 @@ Synchronise toutes les séries non suivies par MdList vers MangaDex en tant que lecture en cours. Titres similaires Groupes de scantrad à afficher + Inclure le hash de l\'URL du chapitre + Ajoutez les six premiers caractères du hachage MD5 de l\'URL du chapitre au nom du fichier ou du dossier du chapitre. + Nom du fichier + Extension du fichier + URL de base + Chapitre final diff --git a/i18n-sy/src/commonMain/moko-resources/hr/strings.xml b/i18n-sy/src/commonMain/moko-resources/hr/strings.xml index bd3a9ba1a..8880c9e63 100644 --- a/i18n-sy/src/commonMain/moko-resources/hr/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/hr/strings.xml @@ -587,7 +587,7 @@ Informacije unosa: Stanje praćenja %1$s prevedeno - ID madija + ID medija Zadnja provjera aktualiziranja Praćene oznake Otvara web prikaz za tvoju E/ExHentai stranicu praćenih oznaka @@ -620,4 +620,10 @@ Nemaš nijedan izvor u svom feedu, idi gore desno za dodavanje izvora Dodati %1$s u feed? Preuzmi teme + Ime datoteke + Sufiks datoteke + Osnovni URL + Završno poglavlje + Uključi hash URL-a poglavlja + Dodaj prvih šest znakova MD5 hash-a URL-a poglavlja imenu datoteke ili mapi poglavlja. diff --git a/i18n-sy/src/commonMain/moko-resources/it/strings.xml b/i18n-sy/src/commonMain/moko-resources/it/strings.xml index 0f2b7ff7f..86590b4dd 100644 --- a/i18n-sy/src/commonMain/moko-resources/it/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/it/strings.xml @@ -336,4 +336,8 @@ Impossibile accedere a Google Drive: %s Non hai effettuato l\'accesso a Google Drive Errore durante l\'eliminazione del file di blocco di Google Drive + Inserisci l\'orario di inizio + Elimina intervallo di tempo + È necessario riavviare l\'applicazione affinché abbia effetto + Proteggi i download con password diff --git a/i18n-sy/src/commonMain/moko-resources/ko/plurals.xml b/i18n-sy/src/commonMain/moko-resources/ko/plurals.xml new file mode 100644 index 000000000..a6d022ff6 --- /dev/null +++ b/i18n-sy/src/commonMain/moko-resources/ko/plurals.xml @@ -0,0 +1,54 @@ + + + + 클린업 완료. %d 폴더를 삭제했습니다 + + + %d 락 타임 + + + 실패한 페이지 %1$d 재시도 중… + + + 정렬 리스트에 %1$d개 태그 추가. 라이브러리에 우선순위 기반 태그 목록을 기준으로 정렬하는 옵션을 추가합니다. 즉, 항목이 원하는 태그를 가진 항목을 우선하는 방식으로 정렬됩니다 + + + %1$d%2$s 항목을 이동하시겠습니까? + + + %1$d%2$s 항목을 복사하시겠습니까? + + + %d개 항목 이동됨 + + + %1$d 페이지 + + + %2$s, %1$d 페이지 + + + %1$d년 전 + + + %1$d개월 전 + + + %1$d주 전 + + + %1$d일 전 + + + %1$d시간 전 + + + %1$d분 전 + + + %1$d초 전 + + + %d 행 + + diff --git a/i18n-sy/src/commonMain/moko-resources/ko/strings.xml b/i18n-sy/src/commonMain/moko-resources/ko/strings.xml new file mode 100644 index 000000000..e9c2fcc93 --- /dev/null +++ b/i18n-sy/src/commonMain/moko-resources/ko/strings.xml @@ -0,0 +1,146 @@ + + + 이동하지 않음 + 수동 검색 + 지금 이동 + 지금 복사 + 타이틀 삭제 + 읽기 시작 + 정보 편집 + 만화(일본) + 만화(한국) + 만화(중국) + 코믹 + 웹툰 + 모든 소스 + E-Hentai + 포크 설정 + MangaDex + E/ExHentai 로그인, 갤러리 동기화 + MangaDex 로그인, 팔로우 동기화 + 버전 %1$s + E-Hentai 웹사이트 계정 설정 + ExHentai 활성화 + 로그인 필요 + Hentai@Home 네트워크 사용 + 가능한 경우 Hentai@Home 네트워크를 통해 이미지를 불러오시겠습니까? 비활성화할 경우 볼 수 있는 페이지 수가 감소합니다\n옵션: \n- 모든 클라이언트 (추천)\n- 기본 포트 클라이언트만 (느려질 수 있음. 비표준 발송 포트를 차단하는 방화벽/프록시 뒤에 있는 경우 활성화) + 모든 클라이언트 (추천) + 기본 포트 클라이언트만 + 검색 결과를 일본어 제목으로 표시 + 현재 검색 결과에 일본어 제목이 표시됩니다. 설정 변경 후 챕터 캐시를 지우십시오 (고급 설정) + 현재 검색 결과에 영어/로마자 제목이 표시됩니다. 설정 변경 후 챕터 캐시를 지우십시오 (고급 설정) + 원본 이미지 사용 + 현재 원본 이미지를 사용하고 있습니다 + 현재 리샘플된 이미지를 사용하고 있습니다 + 관심 태그 + 웹뷰를 열어 E/ExHentai 관심 태그 페이지로 이동합니다 + ExHentai 관심 태그 + 태그 필터 임계값 + -9999 ~ 0 사이의 값이어야 합니다! + 읽은 챕터가 있는 항목을 유지 + 아티스트 + 저자 + 태그에 음수 가중치를 부여해 '내 태그'의 E/ExHentai 페이지에 추가하면 태그를 소프트 필터할 수 있습니다. 갤러리의 태그 가중치가 이 값보다 낮을 경우 표시되지 않습니다. 임계값은 -9999에서 0까지 설정할 수 있습니다. 현재: %1$d + 태그 열람 임계값 + 0 ~ 9999 사이의 값이어야 합니다! + 최근 업로드된 갤러리는 양수 가중치의 최소 한 개의 관심 태그가 있으며, 갤러리의 관심 태그 가중치 합계가 이 값보다 높을 경우 관심 화면에 포함됩니다. 이 임계값은 0에서 9999 사이로 설정할 수 있습니다. 현재: %1$d + 언어 필터 + 갤러리 및 검색 목록에서 특정 언어를 숨기고 싶은 경우, 대화상자에서 해당 갤러리를 선택하십시오.\n해당 갤러리는 검색 결과와 무관하게 표시되지 않습니다.\n요약: 체크 표시 = 제외됨 + 첫 페이지 카테고리 + 첫 페이지와 검색 결과에서 기본적으로 어느 카테고리를 표시하시겠습니까? 해당 필터를 활성화하면 사용할 수 있습니다 + 관심 목록 필터 기본 상태 + ExHentai/E-Hentai를 탐색할 때 관심 목록 필터를 기본적으로 활성화합니다 + 다운로드 이미지 품질 + 이미지 품질 + 자동 + 2400x + 1600x + 1280x + 980x + 780x + 고급 E/ExHentai 탐색 + E/ExHentai 용도로 제작된 고급 탐색 메뉴 활성화/비활성화 + E-Hentai 즐겨찾기 동기화 + 즐겨찾기 업로드 비활성화 + 즐겨찾기는 ExHentai에서만 다운로드됩니다. 앱 내 즐겨찾기 변경사항은 업로드되지 않습니다. 이는 ExHentai에서 즐겨찾기가 실수로 제거되는 것을 방지합니다. 다만 제거된 항목은 여전히 다운로드됩니다(ExHentai에서 즐겨찾기를 제거하면 앱에서도 제거됩니다). + 즐겨찾기 동기화 노트 표시 + 즐겨찾기 동기화 기능에 대한 정보를 표시합니다 + 로그인해 주십시오! + 가능한 경우 동기화 오류를 무시 + 동기화 중 오류가 발생해도 즉시 중단하지 않습니다. 오류 내용은 동기화가 완료되었을 때 표시됩니다. 경우에 따라 즐겨찾기 정보가 손상될 수 있습니다. 대용량 라이브러리 동기화에 유용합니다. + 동기화 상태 강제 리셋 + 다음 동기화 때 전체 재동기화를 수행합니다. 삭제는 동기화되지 않습니다. 앱의 모든 즐겨찾기를 Exhentai에 다시 업로드하며 Exhentai의 모든 즐겨찾기를 앱에 다시 다운로드합니다. 동기화가 중단된 후 복구할 때에 유용합니다. + 동기화 상태 초기화 + 갤러리 업데이트 확인 + 자동 업데이트 제한 + 업데이트 배치 시간 간격 + 갤러리를 업데이트하지 않음 + 1시간 + 2시간 + 3시간 + 6시간 + 12시간 + 24시간 + 48시간 + %1$s(은)는 현재 라이브러리에서 갤러리 업데이트를 확인하지 않습니다. + 업데이트 통계 표시 + 통계 집계 중… + 갤러리 업데이트 통계 + 업데이트가 아직 실행되지 않음. + \n마지막으로 확인된 갤러리:\n- 시간: %1$d\n- 6시간: %2$d\n- 12시간: %3$d\n- 일: %4$d\n- 2일: %5$d\n- 주: %6$d\n- 월: %7$d\n- 연: %8$d + 프로필 노트 설정 + 앱 성능 최적화를 위해 E-Hentai 및 ExHentai에 새 설정 프로필이 추가됩니다. 두 사이트의 프로필이 3개 이하인지 확인해 주십시오.\n\n설정 프로필이 무엇인지 모르는 경우, 중요하지 않을 것이므로 「OK」를 눌러 주십시오. + 설정이 업로드되었습니다! + 설정에 실패했습니다! + 설정 처리 중 오류 발생: %1$s + 설정을 서버로 업로드 중 + 잠시 기다려 주십시오, 시간이 걸립니다… + %1$s 프로필 슬롯이 부족합니다. 프로필을 삭제해 주십시오! + 로그인 상태 재확인 + 대체 로그인 페이지 + 페이지 스타일 변경 넘어가기 + 커스텀 igneous 쿠키 + 일부 사용자는 ExHentai에 정상적인 방법으로 액세스할 수 없으며, 특정 igneous 쿠키 값을 전달해야 합니다. 이 옵션은 해당 사용자를 위한 것입니다. + 개발자 도구 + 통합 hentai 기능 활성화 + 비활성화하면 모든 hentai 기능을 비활성화하는 실험적 기능 + 위임된 소스 활성화 + 다음 소스가 설치된 경우 %1$s 향상 적용: %2$s + 로그 레벨 + 앱 성능에 영향을 미치는 설정입니다. 변경 후 앱을 강제로 재시작합니다. 현재 값: %s + 소스 블랙리스트 활성화 + %1$s 호환되지 않는 확장자/소스를 숨깁니다. 변경 후 앱을 강제로 재시작합니다. + 디버그 메뉴 열기 + 라이브러리가 망가질 수 있습니다!]]> + 정리 시작 중 + 다운로드한 챕터 정리 + 존재하지 않는 폴더, 부분적으로 다운로드된 폴더, 읽은 챕터 폴더를 삭제합니다 + 정리할 폴더 없음 + 고립된 폴더 삭제 + 읽은 폴더 삭제 + 라이브러리에 없는 항목 삭제 + 데이터 세이버 + 리더에서 다운로드 또는 불러오기 전에 이미지를 압축 + 다운로더에 데이터 세이버 사용 + Jpeg 이미지 제외 + Gif 애니메이션 제외 + 이미지 화질 + 값이 높을수록 이미지 화질이 더 많이 보존되지만 파일 크기가 커집니다. 80 퍼센트는 파일 크기와 이미지 화질 사이의 좋은 타협점입니다 + Jpeg로 압축 + Jpeg 파일 크기는 Webp(데이터 보존 중심)보다 작지만, 화질이 더 낮아집니다.\n현재는 Jpeg로 압축 + Jpeg 파일 크기는 Webp(데이터 보존 중심)보다 작지만, 화질이 더 낮아집니다.\n현재는 Webp로 압축 + 흑백으로 변환 + Bandwidth Hero (Bandwidth Hero 프록시 서버 필요) + wsrv.nl + Bandwidth Hero 프록시 서버 + 여기에 Bandwidth Hero 프록시 서버 url 입력 + 챕터 URL 해시 포함 + 챕터 URL의 MD5 해시의 처음 여섯 글자를 챕터 파일 또는 폴더 이름에 추가합니다. + 최소 + 확장 + 최대 + 치명적 오류만 + 전체 로그 + 네트워크 검사 모드 + 모든 검색 필터를 기본으로 확장 + diff --git a/i18n-sy/src/commonMain/moko-resources/pt-rBR/strings.xml b/i18n-sy/src/commonMain/moko-resources/pt-rBR/strings.xml index 548d58114..5bb31aa2f 100644 --- a/i18n-sy/src/commonMain/moko-resources/pt-rBR/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/pt-rBR/strings.xml @@ -531,7 +531,7 @@ Usar economia de dados no baixador Bandwidth Hero (Requer um servidor proxy Bandwidth Hero) wsrv.nl - Mesclar em transborde + Mesclar em caso de excesso Colocar o botão de mesclar no menu flutuante ao invés da pagina inicial Você quer que a aba Feed seja a primeira aba em Navegar? Isso irá fazer dela a aba principal ao abrir Navegar, não é recomendado se você estiver usando uma rede medida ou dados móveis Gatilhos @@ -542,7 +542,7 @@ Frequência de sincronização Error ao Deletar Ficheiro de Bloqueio no Google Drive Pode ser usado para criar gatilhos de sincronização - Sincronizar após Leitura de Capitulo + Sincronizar com a leitura do capítulo Confirmação de Limpeza Criar gatilhos de sincronização Sincronizar após abrir capítulo @@ -655,4 +655,11 @@ Prólogo Eliminar duplicatas com base na prioridade Sem remoção de duplicatas + Incluir o hash da URL do capítulo + Adicione os seis primeiros caracteres do hash MD5 da URL do capítulo ao nome do arquivo ou pasta do capítulo. + Selecione entradas usando metadados de origem + Seleciona automaticamente o título correspondente se a fonte fornecer links para rastreadores. Atualmente compatível com MangaDex + HABILITAR ESTA OPÇÃO CRIARÁ UM NOVO BANCO DE DADOS. SIGA ESTAS ETAPAS PARA MANTER SEUS DADOS SEGUROS
1. CONFIGURAÇÕES -> DADOS E ARMAZENAMENTO -> CRIAR BACKUP
2. CONFIGURAÇÕES DO SISTEMA -> LIMPAR DADOS DO APP
3. ABRA O APP E ATIVE ISSO
4. CONFIGURAÇÕES DO SISTEMA -> FORÇAR REINICIALIZAÇÃO
5. CONFIGURAÇÕES -> DADOS E ARMAZENAMENTO -> RESTAURAR BACKUP]]>
+ AES 256 + AES 128 diff --git a/i18n-sy/src/commonMain/moko-resources/ru/strings.xml b/i18n-sy/src/commonMain/moko-resources/ru/strings.xml index 4b0858c1f..cd728dd08 100644 --- a/i18n-sy/src/commonMain/moko-resources/ru/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/ru/strings.xml @@ -694,4 +694,6 @@ Расширение файла Главный URL Последняя глава + Включать URL хэш главы + Добавить первые шесть символов хеша MD5 URL-адреса главы к имени файла или папки главы. diff --git a/i18n-sy/src/commonMain/moko-resources/tr/strings.xml b/i18n-sy/src/commonMain/moko-resources/tr/strings.xml index 805b77432..14eabb326 100644 --- a/i18n-sy/src/commonMain/moko-resources/tr/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/tr/strings.xml @@ -567,7 +567,7 @@ Takip etme durumu Mal id Görülebilir: %1$s - %1$s ÇVR + %1$s Çeviri Emin misiniz? Girdi birleştirmeden kaldırılacaktır, ayrıca birleştirilmiş girdiye uygulanmış kaydedilmeyen değişiklikler kaybolacaktır Bölüm güncellemelerini ayarla @@ -624,4 +624,6 @@ Dosya uzantısı Ana url Son bölüm + Bölüm URL hash\'ini ekle + Bölüm URL\'sinin MD5 hash değerinin ilk altı karakterini bölüm dosyasının veya klasörünün adına ekleyin. diff --git a/i18n-sy/src/commonMain/moko-resources/zh-rCN/strings.xml b/i18n-sy/src/commonMain/moko-resources/zh-rCN/strings.xml index 4e29c759e..6ccea123d 100644 --- a/i18n-sy/src/commonMain/moko-resources/zh-rCN/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/zh-rCN/strings.xml @@ -682,4 +682,6 @@ 文件拓展名 终章 基础url + 包含章节URL哈希值 + 将章节URL的MD5哈希值的前六个字符添加到章节文件或文件夹名中。 diff --git a/i18n-sy/src/commonMain/moko-resources/zh-rTW/strings.xml b/i18n-sy/src/commonMain/moko-resources/zh-rTW/strings.xml index 4e2f5d0e2..ccdc2dc34 100644 --- a/i18n-sy/src/commonMain/moko-resources/zh-rTW/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -682,4 +682,6 @@ 副檔名 基礎url 終章 + 包含章節網址雜湊值 + 將章節網址的 MD5 雜湊值前六個字元附加至章節檔案或資料夾名。 From 6124efa1649a861ff8f41ee71ab17d13b69e59ca Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 25 Feb 2026 14:13:17 +0100 Subject: [PATCH 12/73] Translations update from Hosted Weblate (mihonapp/mihon#2980) Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/el/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sk/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/ Translation: Mihon/Mihon Co-authored-by: Dexroneum Co-authored-by: Frosted Co-authored-by: Jakub Szafranek13 Fabijan Co-authored-by: KraXen Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com> Co-authored-by: Milo Ivir Co-authored-by: Pitpe11 Co-authored-by: Swyter Co-authored-by: ZerOriSama (cherry picked from commit 565379779f82ee7dfdad45e251d82ff73dc3b7b2) --- .../commonMain/moko-resources/de/strings.xml | 5 +- .../commonMain/moko-resources/el/strings.xml | 5 +- .../commonMain/moko-resources/eo/strings.xml | 4 +- .../commonMain/moko-resources/es/strings.xml | 1 + .../commonMain/moko-resources/hr/strings.xml | 1 + .../commonMain/moko-resources/ru/strings.xml | 1 + .../commonMain/moko-resources/sk/strings.xml | 113 ++++++++++++++++-- .../commonMain/moko-resources/tr/strings.xml | 1 + .../moko-resources/zh-rCN/strings.xml | 1 + 9 files changed, 118 insertions(+), 14 deletions(-) diff --git a/i18n/src/commonMain/moko-resources/de/strings.xml b/i18n/src/commonMain/moko-resources/de/strings.xml index 3e889c1ff..36b77d18b 100644 --- a/i18n/src/commonMain/moko-resources/de/strings.xml +++ b/i18n/src/commonMain/moko-resources/de/strings.xml @@ -170,8 +170,8 @@ Pausiert Quellentitel Kapitelnummer - Quelle - Kapitelnummer + Nach Quelle + Nach Kapitelnummer Herunterladen Ungelesen Bist du sicher, dass du die ausgewählten Kapitel löschen willst? @@ -911,4 +911,5 @@ Tab schließen Ausgeschlossene Scanlationgruppen herausfiltern Wird berechnet… + Mit Lesezeichen versehen diff --git a/i18n/src/commonMain/moko-resources/el/strings.xml b/i18n/src/commonMain/moko-resources/el/strings.xml index 28392e9dc..9ad30cf58 100644 --- a/i18n/src/commonMain/moko-resources/el/strings.xml +++ b/i18n/src/commonMain/moko-resources/el/strings.xml @@ -709,7 +709,7 @@ Συγχρονισμός βιβλιοθήκης Ο συγχρονισμός βιβλιοθήκης ολοκληρώθηκε Πατήστε εδώ για βοήθεια με το Cloudflare - Το ευρετήριο λήψεων ακυρώθηκε + Αναδημιουργία ευρετηρίου λήψεων Σύνδεση παρακολούθησης Δεν ήταν δυνατή η δημιουργία αντιγράφου ασφαλείας Δεν ήταν δυνατή η πρόσβαση στο %s @@ -911,4 +911,7 @@ Ταυτόχρονες λήψεις πηγών Ταυτόχρονες λήψεις σελίδων Σελίδες που λήφθηκαν ταυτόχρονα ανά πηγή + Υπολογισμός… + Προστέθηκε στους σελιδοδείκτες + Φιλτράρισμα εξαιρούμενων scanlator diff --git a/i18n/src/commonMain/moko-resources/eo/strings.xml b/i18n/src/commonMain/moko-resources/eo/strings.xml index d47265896..0fe38264c 100644 --- a/i18n/src/commonMain/moko-resources/eo/strings.xml +++ b/i18n/src/commonMain/moko-resources/eo/strings.xml @@ -697,7 +697,7 @@ Devigas restartigi apon por efektiviĝi Restarigas legadan reĝimon kaj orientiĝon de ĉiuj serioj Reindeksi elŝutojn - Indeksoj de elŝutoj nevalidigitaj + Rekreado de indekso de elŝutoj Forigi historion de elementoj ne konservitaj en biblioteko %1$d nebibliotekaj elementoj en datumbazo Nenio por viŝi @@ -901,4 +901,6 @@ Kunrulaj elŝutoj de paĝoj Paĝoj elŝutitaj sammomente por fonto Filtri ekskluzivitajn skantradukistojn + Kalkulado… + Legosignita diff --git a/i18n/src/commonMain/moko-resources/es/strings.xml b/i18n/src/commonMain/moko-resources/es/strings.xml index 3978bdc83..414fdfac6 100644 --- a/i18n/src/commonMain/moko-resources/es/strings.xml +++ b/i18n/src/commonMain/moko-resources/es/strings.xml @@ -906,4 +906,5 @@ Cerrar pestaña Ocultar equipos de traducción excluidos Calculando… + Favoritos diff --git a/i18n/src/commonMain/moko-resources/hr/strings.xml b/i18n/src/commonMain/moko-resources/hr/strings.xml index 192077ea1..af8a499fe 100644 --- a/i18n/src/commonMain/moko-resources/hr/strings.xml +++ b/i18n/src/commonMain/moko-resources/hr/strings.xml @@ -913,4 +913,5 @@ Zatvori karticu Izračunavanje … Filtriraj isključene amaterske prevoditelje + Zabilježeno diff --git a/i18n/src/commonMain/moko-resources/ru/strings.xml b/i18n/src/commonMain/moko-resources/ru/strings.xml index e5ea6f666..3e5e7427d 100644 --- a/i18n/src/commonMain/moko-resources/ru/strings.xml +++ b/i18n/src/commonMain/moko-resources/ru/strings.xml @@ -911,4 +911,5 @@ Число одновременных загрузок страниц с источника Фильтровать исключенных переводчиков Вычисление… + В закладках diff --git a/i18n/src/commonMain/moko-resources/sk/strings.xml b/i18n/src/commonMain/moko-resources/sk/strings.xml index 36c866d7e..15b3239b5 100644 --- a/i18n/src/commonMain/moko-resources/sk/strings.xml +++ b/i18n/src/commonMain/moko-resources/sk/strings.xml @@ -166,7 +166,7 @@ Štvrtá predposledná prečítaná kapitola Piata posledná prečítaná kapitola Stiahnuť nové kapitoly - Služby + Sledovače Vytvoriť zálohu Možno použiť na obnovenie aktuálnej knižnice Obnoviť zálohu @@ -406,8 +406,8 @@ Odstrániť kapitoly Uložiť ako archív CBZ Zlepšuje výkon čítačky - Návod k sledovaniu - Rozšírené služby + Príručka k sledovačom + Vylepšené sledovače Sieť Vymazanie vyrovnávacej pamäte kapitoly pri zatvorení aplikácie Čo je nové @@ -450,7 +450,7 @@ Zálohovanie/obnovenie nemusí fungovať správne, ak je vypnutá optimalizácia MIUI. Zakázať optimalizáciu batérie Teraz ste odhlásení - Návod pre lokálne zdroje + Príručka pre lokálne zdroje Neznámy autor Nepodarilo sa skopírovať do schránky Nepodarilo sa nájsť cestu k súboru stránky %d @@ -491,7 +491,7 @@ Vymazať históriu Chyba pri ukladaní obalu Nastaviť ako predvolené - Jednosmerná synchronizácia na aktualizáciu priebehu kapitol v službách sledovania. Nastavte sledovanie pre jednotlivé položky mangy z ich tlačidla sledovania. + Jednosmerná synchronizácia postupu v mange na externé sledovacie služby (sledovače). Nastavte sledovanie pre jednotlivé položky mangy z ich tlačidla sledovania. Režim čítania Aktualizácie kapitol V databáze je manga %1$d, ktorá sa nenachádza v knižnici @@ -551,7 +551,7 @@ Čakajúce aktualizácie Ďalšia strana Otvoriť na GitHub-e - Príručka Začíname + Príručka pre začiatočníkov Zakázať režim inkognito Menej Služby, ktoré poskytujú rozšírené funkcie pre konkrétne zdroje. Mangy sú automaticky sledované po pridaní do vašej knižnice. @@ -577,7 +577,7 @@ Nie je možné otvoriť poslednú prečítanú kapitolu Klepnutím sa dozviete viac Stiahnuté kapitoly - Návod k migrácií zdrojov + Príručka k migrácií zdrojov Všetky nastavenia čítačky sa obnovia Aktivita na pozadí Pomáha s aktualizáciami a zálohovaním knižnice na pozadí @@ -640,10 +640,10 @@ Aktualizovať kategóriu Kopírovať do schránky OK - Presunúť sériu na koniec - Ďalšie + Presunúť sériu na spodok + Ďalej Preskočiť - Povolenie pre sťahovanie aplikácie + Povolenie na inštalovanie ľubovoľných aplikácii Bezpečnosť Začíname Odstrániť rozšírenie? @@ -665,4 +665,97 @@ Nadchádzajúce Nahradiť Navigovať hore + Prispieť finančne + Auto + Vlastná frekvencia aktualizácie + Skóre na sledovači + Neprečítané kapitoly + Pokračovať v čítaní tlačidlo + Poznámky + Upraviť poznámky + Vrátiť na predvolené + Zatvoriť kartu + Nie teraz + Ajtak pridať + Vypočítavam… + Príručka na úvod + Poďme najprv nastaviť nejaké veci. Vždy ich môžete neskôr zmeniť. + Vyberte priečinok, kde %1$s bude ukladať stiahnuté kapitoly, zálohy a ďalšie.\n\nOdporúčame použiť samostatný priečinok špecificky pre túto aplikáciu.\n\nVybraný priečinok: %2$s + Aktualizujete zo staršej verzie, a nie ste si istí, čo vybrať? Viac informácií nájdete v príručke úložiska. + Príručka úložiska + Na inštaláciu rozšírení zdrojov. + Povolenie posielania notifikácii + Notifikácie o aktualizáciach v knižnici a podobne. + Využitie batérie v pozadí + Vyhnite sa prerušeniam aktualizácie knižnice, sťahovania a obnovy záloh. + Poslať záznamy v prípade zlyhania aplikácie + Poslať anonymizované záznamy v prípade zlyhania aplikácie vývojárom. + Povoliť analytické dáta + Posielať anonymizované dáta o používaní aplikácie vývojárom pre zlepšenie zážitku. + Povoliť + Prvý krát používate %s? Odporúčame prečítať si príručku pre začiatočníkov. + Inštalujete %s znovu? + Téma, dátum a formát času + Príručka pre nadchádzajúce + Prihlásenie do sledovača + Zahrnúť citlivé nastavenia (napr. prihlasovacie údaje do sledovačov) + Aktualizovať postup na sledovačoch po kapitolu %d? + Sledovače aktualizované na kapitolu %d + Sledovače + Kategórie, globálna aktualizácia, prechádzanie kapitolami + Režim čítania, displej, navigácia + Automatické sťahovanie, sťahovanie dopredu + Jednosmerná synchronizácia postupu, vylepšená synchronizácia + Zdroje, rozšírenia, globálne vyhľadávanie + Manuálne & automatické zálohy, miesto v úložisku + Zamknutie aplikácie, bezpečná obrazovka + Uložiť záznamy o zlyhaní aplikácie, optimalizácie batérie + Monochromatická + Relatívne časové značky + \"%1$s\" namiesto \"%2$s\" + Zobraziť obrázky v popisoch mangy + Analytické dáta a záznamy v prípade zlyhania + Posielaním záznamov v prípade zlyhania aplikácie a analytických údajov nám umožníte identifikovať a opraviť chyby, zlepšiť rýchlost aplikácie a urobiť aplikáciu viac relevantnejšiu pre vaše potreby + Inteligentné aktualizácie + Zobraziť počet neprečítaných na ikone Aktualizácii + Správanie + Kapitola po potiahnutí prstom doprava + Kapitola po potiahnutí prstom doľava + Označiť duplikátne kapitoly ako prečítané + Po prečítaní kapitoly + Po získaní novej kapitoly + Schovať chýbajúce indikátory kapitol + Zakázať názvy súborov, ktoré nie sú v kódovaní ASCII + Zaručí kompatibilitu s niektorými úložiskami, ktoré nepodporujú kódovanie Unicode. Keď je táto možnosť zvolená, budete musieť manuálne premenovať priečinky zdrojov a mangy tak, že nahradíte znaky, ktoré nie sú podporované v kódovaní ASCII, ich malými hexadecimálnymi reprezentáciami. Súbory kapitol netreba premenovávať. + Odstrániť + Na inštaláciu rozšírení sú potrebné povolenia. Ťuknite sem, aby ste ich udelili. + Odstrániť status \"dôveryhodné\" z neznámych rozšírení + Repozitáre rozšírení + Nemáte nastavené žiadne repozitáre. + Pridať repozitár + URL Repozitára + Pridať ďalšie repozitáre do %s. Hodnota by mala byť URL, ktorá končí \"index.min.json\". + Tento repozitár už existuje! + Odstrániť repozitár + Neplatná URL repozitára + Naozaj chcete odstrániť repozitár \"%s\"? + Chcete pridať repozitár \"%s\"? + Open-source repozitár + Otlačok podpisového kľúča už existuje + Repozitár %1$s má rovnaký otlačok podpisového kľúča ako %2$s.\nAk je toto očakávané, %2$s bude nahradený, inak kontaktuje manažéra repozitára. + Otočiť široké strany, aby sa zmestili + Preklopiť orientáciu otočených širokých strán + Dvojťuk pre priblíženie + Prebliknúť pri zmene strany + Znižuje ghosting na e-ink displejoch + Trvanie prebliknutia + Prebliknúť každých + Prebliknúť s + Čiernou + Bielou + Čiernou aj Bielou + Vlastný prah pre hardvérové bitmapy + Predvolený (%d) + Ak čítacie rozhranie načítava prázdne obrázky, postupne znížte prah.\nVybrané: %s + Použiť starší dekodér pre čítacie rozhranie \"dlhé pásy\" diff --git a/i18n/src/commonMain/moko-resources/tr/strings.xml b/i18n/src/commonMain/moko-resources/tr/strings.xml index 4ee172b46..4126b5da1 100644 --- a/i18n/src/commonMain/moko-resources/tr/strings.xml +++ b/i18n/src/commonMain/moko-resources/tr/strings.xml @@ -913,4 +913,5 @@ Sekmeyi kapat Hariç tutulan Çevirenleri filtrele Hesaplanıyor… + Yer imli diff --git a/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml b/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml index a662d6c33..8fd311a76 100644 --- a/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml +++ b/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml @@ -911,4 +911,5 @@ 关闭标签 筛选排除翻译组 计算中… + 已添加书签 From bad1dd264df600bb221911cbf3e843bbc77ddf42 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Thu, 26 Feb 2026 07:58:38 +0600 Subject: [PATCH 13/73] Fix extension install/update stuck at pending (#1630) * Fix extension install/update stuck at pending (mihonapp/mihon#3000) Co-authored-by: p (cherry picked from commit 84265febf3ce24d71994ced2b81215f858430d4e) * Fix lint * Throw exception when Shizuku shell interface is unavailable during installation instead of silently doing nothing * Properly close OkHttp response and streams * Delete temporary extension file if failed to install * Use ConcurrentHashMap for active jobs and steps to ensure thread safety when managing extension installation states across multiple coroutines. * Use SupervisorJob to ensure that a failure in one installation job doesn't cancel the entire scope * Don't use `:` in package name for safer filename * Throw exception if asset file descriptor fails to open during extension installation --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- CHANGELOG.md | 1 + .../tachiyomi/extension/ExtensionManager.kt | 6 +- .../installer/PackageInstallerInstaller.kt | 3 + .../extension/installer/ShizukuInstaller.kt | 17 +- .../util/ExtensionInstallActivity.kt | 5 + .../extension/util/ExtensionInstaller.kt | 286 +++++------------- .../browse/extension/ExtensionsScreenModel.kt | 6 +- 7 files changed, 101 insertions(+), 223 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 672ba0099..305497994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Fix Add Repo input not taking up the full dialog width ([@cuong-tran](https://github.com/cuong-tran)) ([#2816](https://github.com/mihonapp/mihon/pull/2816)) - Fix migration's selected sources order not preserved ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2993)) - Fix migration dialog not showing for consecutive prompts from the same screen ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2994)) +- Fix extension install/update stuck at pending ([@AntsyLich](https://github.com/AntsyLich)) ([#3000](https://github.com/mihonapp/mihon/pull/3000)) ### Other - Enable logcat logging on stable and debug builds without enabling verbose logging ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2836](https://github.com/mihonapp/mihon/pull/2836)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index f2fd81758..477b61be1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -200,7 +200,7 @@ class ExtensionManager( availableExtensionMapFlow.value = extensions.associateBy { it.pkgName + // KMK --> - ":${it.signatureHash}" + "_${it.signatureHash}" // KMK <-- } updatedInstalledExtensionsStatuses(extensions) @@ -312,7 +312,7 @@ class ExtensionManager( val availableExt = availableExtensionMapFlow.value[ extension.pkgName + // KMK --> - ":${extension.signatureHash}", + "_${extension.signatureHash}", // KMK <-- ] ?: return emptyFlow() return installExtension(availableExt) @@ -322,7 +322,7 @@ class ExtensionManager( installer.cancelInstall( extension.pkgName + // KMK --> - ":${extension.signatureHash}", + "_${extension.signatureHash}", // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt index d3aef3b18..931bf48af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.extension.installer +import android.annotation.SuppressLint import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver @@ -82,6 +83,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic inputStream.copyTo(outputStream) session.fsync(outputStream) } + service.contentResolver.delete(entry.uri, null, null) val intentSender = PendingIntent.getBroadcast( service, @@ -89,6 +91,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic Intent(INSTALL_ACTION).setPackage(service.packageName), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0, ).intentSender + @SuppressLint("RequestInstallPackagesPolicy") session.commit(intentSender) } } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt index b9e175577..8eae5277f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt @@ -111,20 +111,17 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { override fun processEntry(entry: Entry) { super.processEntry(entry) - - // KMK --> - val installer = shellInterface - if (installer == null) { - logcat(LogPriority.ERROR) { "Shizuku shell interface not available for ${entry.downloadId}" } - continueQueue(InstallStep.Error) - return - } - try { service.contentResolver.openAssetFileDescriptor(entry.uri, "r")?.use { - installer.install(it) + shellInterface?.install(it) + // KMK --> + ?: throw Exception("Shell interface is not available") + // KMK <-- } + // KMK --> + ?: throw Exception("Failed to open asset file descriptor") // KMK <-- + service.contentResolver.delete(entry.uri, null, null) } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } continueQueue(InstallStep.Error) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt index a300ba2ea..ef73e9de7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt @@ -64,6 +64,11 @@ class ExtensionInstallActivity : Activity() { } } + override fun onDestroy() { + super.onDestroy() + intent.data?.let { contentResolver.delete(it, null, null) } + } + private fun checkInstallationResult(resultCode: Int) { val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID) val extensionManager = Injekt.get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 6b471ba3b..15b1af052 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -1,66 +1,52 @@ package eu.kanade.tachiyomi.extension.util -import android.app.DownloadManager -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.net.Uri -import android.os.Environment import androidx.core.content.ContextCompat -import androidx.core.content.getSystemService import androidx.core.net.toUri import eu.kanade.domain.base.BasePreferences -import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.installer.Installer import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep +import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.isPackageInstalled -import kotlinx.coroutines.delay +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.launch import logcat.LogPriority -import tachiyomi.core.common.util.lang.withUIContext +import okhttp3.OkHttpClient +import okhttp3.Request import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File -import kotlin.time.Duration.Companion.seconds +import java.util.concurrent.ConcurrentHashMap /** * The installer which installs, updates and uninstalls the extensions. * * @param context The application context. */ -internal class ExtensionInstaller(private val context: Context) { - - /** - * The system's download manager - */ - private val downloadManager = context.getSystemService()!! - - /** - * The broadcast receiver which listens to download completion events. - */ - private val downloadReceiver = DownloadCompletionReceiver() - - /** - * The currently requested downloads, with the package name (unique id) as key, and the id - * returned by the download manager. - */ - private val activeDownloads = hashMapOf() - - private val downloadsStateFlows = hashMapOf>() +internal class ExtensionInstaller( + private val context: Context, +) { + // KMK --> + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val activeJobs = ConcurrentHashMap() + private val activeSteps = ConcurrentHashMap>() + // KMK <-- private val extensionInstaller = Injekt.get().extensionInstaller() + private val httpClient: OkHttpClient = Injekt.get().client + /** * Adds the given extension to the downloads queue and returns an observable containing its * step in the installation process. @@ -71,130 +57,97 @@ internal class ExtensionInstaller(private val context: Context) { fun downloadAndInstall(url: String, extension: Extension): Flow { val pkgName = extension.pkgName + // KMK --> - ":${extension.signatureHash}" + "_${extension.signatureHash}" // KMK <-- + val downloadId = pkgName.hashCode().toLong() + cancelInstall(pkgName) - val oldDownload = activeDownloads[pkgName] - if (oldDownload != null) { - deleteDownload(pkgName) - } + val step = MutableStateFlow(InstallStep.Pending) + activeSteps[downloadId] = step - // Register the receiver after removing (and unregistering) the previous download - downloadReceiver.register() + val job = scope.launch { + val tmpFile = File(context.cacheDir, "extension_$pkgName.apk") + try { + step.value = InstallStep.Downloading + val request = Request.Builder().url(url).build() + httpClient.newCall(request).execute() + // KMK --> + .use { response -> + // KMK <-- - val downloadUri = url.toUri() - val request = DownloadManager.Request(downloadUri) - .setTitle(extension.name) - .setMimeType(APK_MIME) - .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + if (!response.isSuccessful) { + throw Exception("Failed to download extension") + } + // KMK --> + tmpFile.outputStream().use { output -> + response.body.byteStream().use { input -> + // KMK <-- + input.copyTo(output) + } + } + } - val id = downloadManager.enqueue(request) - activeDownloads[pkgName] = id - - val downloadStateFlow = MutableStateFlow(InstallStep.Pending) - downloadsStateFlows[id] = downloadStateFlow - - // Poll download status - val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus -> - // Map to our model - when (downloadStatus) { - DownloadManager.STATUS_PENDING -> InstallStep.Pending - DownloadManager.STATUS_RUNNING -> InstallStep.Downloading - else -> null + step.value = InstallStep.Installing + installApk(downloadId, tmpFile) + } catch (e: Exception) { + if (e is InterruptedException) { + // Canceled + } else { + logcat(LogPriority.ERROR, e) + step.value = InstallStep.Error + } + // KMK --> + tmpFile.delete() + // KMK <-- } } - return merge(downloadStateFlow, pollStatusFlow).transformWhile { - emit(it) - // Stop when the application is installed or errors - !it.isCompleted() - }.onCompletion { - // Always notify on main thread - withUIContext { - // Always remove the download when unsubscribed - deleteDownload(pkgName) + activeJobs[pkgName] = job + + return step.asStateFlow() + .onCompletion { + activeJobs.remove(pkgName) + activeSteps.remove(downloadId) + job.cancel() } - } } - /** - * Returns a flow that polls the given download id for its status every second, as the - * manager doesn't have any notification system. It'll stop once the download finishes. - * - * @param id The id of the download to poll. - */ - private fun downloadStatusFlow(id: Long): Flow = flow { - val query = DownloadManager.Query().setFilterById(id) - while (true) { - // Get the current download status - val downloadStatus = downloadManager.query(query).use { cursor -> - if (!cursor.moveToFirst()) return@flow - cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) - } - - emit(downloadStatus) - - // Stop polling when the download fails or finishes - if ( - downloadStatus == DownloadManager.STATUS_SUCCESSFUL || - downloadStatus == DownloadManager.STATUS_FAILED - ) { - return@flow - } - - delay(1.seconds) - } - } - // Ignore duplicate results - .distinctUntilChanged() - /** * Starts an intent to install the extension at the given uri. * - * @param uri The uri of the extension to install. + * @param tempFile The file of the extension to install. Delete after use. */ - fun installApk(downloadId: Long, uri: Uri) { + private fun installApk(downloadId: Long, tempFile: File) { when (val installer = extensionInstaller.get()) { BasePreferences.ExtensionInstaller.LEGACY -> { val intent = Intent(context, ExtensionInstallActivity::class.java) - .setDataAndType(uri, APK_MIME) + .setDataAndType(tempFile.getUriCompat(context), APK_MIME) .putExtra(EXTRA_DOWNLOAD_ID, downloadId) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) context.startActivity(intent) } BasePreferences.ExtensionInstaller.PRIVATE -> { - val extensionManager = Injekt.get() - val tempFile = File(context.cacheDir, "temp_$downloadId") - - if (tempFile.exists() && !tempFile.delete()) { - // Unlikely but just in case - extensionManager.updateInstallStep(downloadId, InstallStep.Error) - return - } - try { - context.contentResolver.openInputStream(uri)?.use { input -> - tempFile.outputStream().use { output -> - input.copyTo(output) - } - } - if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) { - extensionManager.updateInstallStep(downloadId, InstallStep.Installed) + updateInstallStep(downloadId, InstallStep.Installed) } else { - extensionManager.updateInstallStep(downloadId, InstallStep.Error) + updateInstallStep(downloadId, InstallStep.Error) } } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." } - extensionManager.updateInstallStep(downloadId, InstallStep.Error) + updateInstallStep(downloadId, InstallStep.Error) } tempFile.delete() } else -> { - val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer) + val intent = ExtensionInstallService.getIntent( + context, + downloadId, + tempFile.getUriCompat(context), + installer, + ) ContextCompat.startForegroundService(context, intent) } } @@ -204,9 +157,8 @@ internal class ExtensionInstaller(private val context: Context) { * Cancels extension install and remove from download manager and installer. */ fun cancelInstall(pkgName: String) { - val downloadId = activeDownloads.remove(pkgName) ?: return - downloadManager.remove(downloadId) - Installer.cancelInstallQueue(context, downloadId) + activeJobs.remove(pkgName)?.cancel() + Installer.cancelInstallQueue(context, pkgName.hashCode().toLong()) } /** @@ -233,91 +185,11 @@ internal class ExtensionInstaller(private val context: Context) { * @param step New install step. */ fun updateInstallStep(downloadId: Long, step: InstallStep) { - downloadsStateFlows[downloadId]?.let { it.value = step } - } - - /** - * Deletes the download for the given package name. - * - * @param pkgName The package name of the download to delete together with its signature. - */ - private fun deleteDownload(pkgName: String) { - val downloadId = activeDownloads.remove(pkgName) - if (downloadId != null) { - downloadManager.remove(downloadId) - downloadsStateFlows.remove(downloadId) - } - if (activeDownloads.isEmpty()) { - downloadReceiver.unregister() - } - } - - /** - * Receiver that listens to download status events. - */ - private inner class DownloadCompletionReceiver : BroadcastReceiver() { - - /** - * Whether this receiver is currently registered. - */ - private var isRegistered = false - - /** - * Registers this receiver if it's not already. - */ - fun register() { - if (isRegistered) return - isRegistered = true - - val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) - ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED) - } - - /** - * Unregisters this receiver if it's not already. - */ - fun unregister() { - if (!isRegistered) return - isRegistered = false - - context.unregisterReceiver(this) - } - - /** - * Called when a download event is received. It looks for the download in the current active - * downloads and notifies its installation step. - */ - override fun onReceive(context: Context, intent: Intent?) { - val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return - - // Avoid events for downloads we didn't request - if (id !in activeDownloads.values) return - - val uri = downloadManager.getUriForDownloadedFile(id) - - // Set next installation step - if (uri == null) { - logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" } - updateInstallStep(id, InstallStep.Error) - return - } - - val query = DownloadManager.Query().setFilterById(id) - downloadManager.query(query).use { cursor -> - if (cursor.moveToFirst()) { - val localUri = cursor.getString( - cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI), - ).removePrefix(FILE_SCHEME) - - installApk(id, File(localUri).getUriCompat(context)) - } - } - } + activeSteps[downloadId]?.let { it.value = step } } companion object { const val APK_MIME = "application/vnd.android.package-archive" const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID" - const val FILE_SCHEME = "file://" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt index d3768217c..5065fcf31 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt @@ -53,7 +53,7 @@ class ExtensionsScreenModel( map[ it.pkgName + // KMK --> - ":${it.signatureHash}", + "_${it.signatureHash}", // KMK <-- ] ?: InstallStep.Idle, ) @@ -207,7 +207,7 @@ class ExtensionsScreenModel( it + Pair( extension.pkgName + // KMK --> - ":${extension.signatureHash}", + "_${extension.signatureHash}", // KMK <-- installStep, ) @@ -219,7 +219,7 @@ class ExtensionsScreenModel( it - ( extension.pkgName + // KMK --> - ":${extension.signatureHash}" + "_${extension.signatureHash}" // KMK <-- ) } From 38acd6f110e0d07eaae87b7ec9a6a3062ed5f83a Mon Sep 17 00:00:00 2001 From: KaiserBh <41852205+kaiserbh@users.noreply.github.com> Date: Sat, 28 Feb 2026 03:20:59 +1100 Subject: [PATCH 14/73] feat: Add sync events to SyncYomi (jobobby04/TachiyomiSY#1558) * feat: Add sync events to SyncYomi Now it will send the events back to `SyncYomi` server and then forward those to the notifications services that are enabled, such as discord, telegram, and etc. * chore: fix build error. (cherry picked from commit 2776e411272499eba2f91acfea3cb40dc0d4c622) * Improve client management and response handling --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- .../data/sync/service/SyncYomiSyncService.kt | 126 ++++++++++++++---- 1 file changed, 100 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt index ea04f72ac..3db50c859 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt @@ -5,8 +5,14 @@ import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.sync.SyncNotifier import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.await +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf @@ -21,7 +27,6 @@ import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.concurrent.TimeUnit class SyncYomiSyncService( context: Context, @@ -32,9 +37,34 @@ class SyncYomiSyncService( private val protoBuf: ProtoBuf = Injekt.get(), ) : SyncService(context, json, syncPreferences) { + // KMK --> + companion object { + private val client by lazy { OkHttpClient() } + } + // KMK <-- + private class SyncYomiException(message: String?) : Exception(message) + @Serializable + private data class SyncEvent( + val event: SyncEventStatus, + @SerialName("device_name") + val deviceName: String? = null, + val message: String? = null, + ) + + @Serializable + private enum class SyncEventStatus { + SYNC_STARTED, + SYNC_SUCCESS, + SYNC_FAILED, + SYNC_ERROR, + SYNC_CANCELLED, + } + override suspend fun doSync(syncData: SyncData): Backup? { + reportSyncEvent(SyncEventStatus.SYNC_STARTED) + try { val (remoteData, etag) = pullSyncData() @@ -52,11 +82,26 @@ class SyncYomiSyncService( syncData } - pushSyncData(finalSyncData, etag) + val success = pushSyncData(finalSyncData, etag) + + if (success) { + reportSyncEvent(SyncEventStatus.SYNC_SUCCESS) + } else { + reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data") + // KMK --> + return null + // KMK <-- + } + return finalSyncData.backup } catch (e: Exception) { + if (e is CancellationException) { + reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message) + throw e + } logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" } notifier.showSyncError(e.message) + reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message) return null } } @@ -78,7 +123,6 @@ class SyncYomiSyncService( headers = headers, ) - val client = OkHttpClient() val response = client.newCall(downloadRequest).await() if (response.code == HttpStatus.SC_NOT_MODIFIED) { @@ -123,13 +167,12 @@ class SyncYomiSyncService( /** * Return true if update success */ - private suspend fun pushSyncData(syncData: SyncData, eTag: String) { - val backup = syncData.backup ?: return + private suspend fun pushSyncData(syncData: SyncData, eTag: String): Boolean { + val backup = syncData.backup ?: return true val host = syncPreferences.clientHost().get() val apiKey = syncPreferences.clientAPIKey().get() val uploadUrl = "$host/api/sync/content" - val timeout = 30L val headersBuilder = Headers.Builder().add("X-API-Token", apiKey) if (eTag.isNotEmpty()) { @@ -137,13 +180,6 @@ class SyncYomiSyncService( } val headers = headersBuilder.build() - // Set timeout to 30 seconds - val client = OkHttpClient.Builder() - .connectTimeout(timeout, TimeUnit.SECONDS) - .readTimeout(timeout, TimeUnit.SECONDS) - .writeTimeout(timeout, TimeUnit.SECONDS) - .build() - val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup) if (byteArray.isEmpty()) { throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error)) @@ -156,20 +192,58 @@ class SyncYomiSyncService( body = body, ) - val response = client.newCall(uploadRequest).await() + client.newCall(uploadRequest).await() + // KMK --> + .use { response -> + // KMK <-- + if (response.isSuccessful) { + val newETag = response.headers["ETag"] + .takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag") + syncPreferences.lastSyncEtag().set(newETag) + logcat(LogPriority.DEBUG) { "SyncYomi sync completed" } + return true + } else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) { + // other clients updated remote data, will try next time + logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" } + return false + } else { + val responseBody = response.body.string() + notifier.showSyncError("Failed to upload sync data: $responseBody") + logcat(LogPriority.ERROR) { "SyncError: $responseBody" } + return false + } + } + } - if (response.isSuccessful) { - val newETag = response.headers["ETag"] - .takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag") - syncPreferences.lastSyncEtag().set(newETag) - logcat(LogPriority.DEBUG) { "SyncYomi sync completed" } - } else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) { - // other clients updated remote data, will try next time - logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" } - } else { - val responseBody = response.body.string() - notifier.showSyncError("Failed to upload sync data: $responseBody") - logcat(LogPriority.ERROR) { "SyncError: $responseBody" } + private suspend fun reportSyncEvent(event: SyncEventStatus, message: String? = null) { + withContext(NonCancellable) { + try { + val host = syncPreferences.clientHost().get() + val apiKey = syncPreferences.clientAPIKey().get() + val url = "$host/api/sync/event" + + val headersBuilder = Headers.Builder().add("X-API-Token", apiKey) + val headers = headersBuilder.build() + + val bodyObj = SyncEvent( + event = event, + deviceName = android.os.Build.MODEL, + message = message, + ) + + val jsonBody = json.encodeToString(SyncEvent.serializer(), bodyObj) + val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType()) + + val request = POST( + url = url, + headers = headers, + body = requestBody, + ) + + client.newCall(request).await().close() + } catch (e: Exception) { + logcat(LogPriority.ERROR) { "Failed to report sync event: ${e.message}" } + } } } } From 1f904815faf4ea5ed4786656a1a3dc34ab84d81d Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:03:12 +0700 Subject: [PATCH 15/73] Clean up some build warnings --- .../java/eu/kanade/presentation/webview/EhLoginWebViewScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/presentation/webview/EhLoginWebViewScreen.kt b/app/src/main/java/eu/kanade/presentation/webview/EhLoginWebViewScreen.kt index 344a2fe6a..1eebd48bc 100644 --- a/app/src/main/java/eu/kanade/presentation/webview/EhLoginWebViewScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/webview/EhLoginWebViewScreen.kt @@ -80,7 +80,7 @@ fun EhLoginWebViewScreen( ) is LoadingState.Loading -> { val animatedProgress by animateFloatAsState( - (loadingState as? LoadingState.Loading)?.progress ?: 1f, + loadingState.progress, animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, label = "webview_loading", ) From 5c0ca305a91bfc12b1807e8276970ee0dc789355 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Fri, 27 Feb 2026 22:07:31 -0500 Subject: [PATCH 16/73] Add recommended proguard rules (cherry picked from commit 5c26bb3a52c1f209facfda0b4e99d0a1feba9d76) --- app/proguard-rules.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 6b8216079..f8dfadd5b 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -307,3 +307,5 @@ -dontwarn org.ietf.jgss.GSSManager -dontwarn org.ietf.jgss.GSSName -dontwarn org.ietf.jgss.Oid +-dontwarn com.google.re2j.Matcher +-dontwarn com.google.re2j.Pattern From f5e1c1e5ce964057965466574964f18e1d900f59 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Fri, 27 Feb 2026 22:07:59 -0500 Subject: [PATCH 17/73] Copy last page read in migrate (cherry picked from commit 3a3b719b8bdc3c9ec95120ddca3a87183b40cd4d) --- .../java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt index 4de4a5fc8..a17709104 100644 --- a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt +++ b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt @@ -99,6 +99,7 @@ class MigrateMangaUseCase( // SY <-- dateFetch = prevChapter.dateFetch, bookmark = prevChapter.bookmark, + lastPageRead = prevChapter.lastPageRead, ) // SY --> // KMK --> From 3344fd0755130faa48d310524aff89d6453cc6ed Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Fri, 27 Feb 2026 13:49:26 -0500 Subject: [PATCH 18/73] Fix blank page on cloudflare guard (cherry picked from commit 07599ade3aeb127f643b45e84a6b9ccdb1d0ba6d) Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- .../main/java/exh/ui/login/EhLoginActivity.kt | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/exh/ui/login/EhLoginActivity.kt b/app/src/main/java/exh/ui/login/EhLoginActivity.kt index d1568e8d5..10c5191fc 100644 --- a/app/src/main/java/exh/ui/login/EhLoginActivity.kt +++ b/app/src/main/java/exh/ui/login/EhLoginActivity.kt @@ -93,14 +93,30 @@ class EhLoginActivity : BaseActivity() { xLogD(url) val parsedUrl = url.toUri() if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) { - // Hide distracting content - if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) { - view.evaluateJavascript(HIDE_JS, null) - } - // Check login result + view.evaluateJavascript( + """ + (function() { + let html = document.documentElement.innerHTML; + return html.includes("/cdn-cgi/"); + })(); + """.trimIndent(), + ) { result -> + val isCloudflareBlock = result == "true" - if (parsedUrl.getQueryParameter("code")?.toInt() != 0) { - if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/") + if (isCloudflareBlock) { + xLogD("Cloudflare block detected — skipping logic") + return@evaluateJavascript + } + + // Hide distracting content + if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) { + view.evaluateJavascript(HIDE_JS, null) + } + // Check login result + + if (parsedUrl.getQueryParameter("code")?.toIntOrNull() != 0) { + if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/") + } } } else if (parsedUrl.host.equals("exhentai.org", ignoreCase = true)) { // At ExHentai, check that everything worked out... From 0293507d1a842250223a21810711d25b7ce64451 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 13 May 2026 05:55:28 +0200 Subject: [PATCH 19/73] Translations update from Hosted Weblate (#1567) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Turkish) Currently translated at 70.4% (148 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/tr/ * Translated using Weblate (Turkish) Currently translated at 70.4% (148 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/tr/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 84.2% (177 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 84.2% (177 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/zh_Hans/ * Translated using Weblate (Bengali) Currently translated at 22.3% (47 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/bn/ * Translated using Weblate (French) Currently translated at 92.8% (195 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/fr/ * Translated using Weblate (Thai) Currently translated at 27.1% (57 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/th/ * Translated using Weblate (Thai) Currently translated at 27.1% (57 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/th/ * Translated using Weblate (Danish) Currently translated at 79.0% (166 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/da/ * Translated using Weblate (Danish) Currently translated at 79.0% (166 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/da/ * Translated using Weblate (Czech) Currently translated at 80.9% (170 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/cs/ * Translated using Weblate (Czech) Currently translated at 80.9% (170 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/cs/ * Translated using Weblate (Malay) Currently translated at 20.4% (43 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ms/ * Translated using Weblate (Ukrainian) Currently translated at 86.1% (181 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/uk/ * Translated using Weblate (Ukrainian) Currently translated at 86.1% (181 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/uk/ * Translated using Weblate (Spanish) Currently translated at 85.7% (180 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/es/ * Translated using Weblate (Spanish) Currently translated at 85.7% (180 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/es/ * Translated using Weblate (Swedish) Currently translated at 79.5% (167 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/sv/ * Translated using Weblate (Swedish) Currently translated at 79.5% (167 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/sv/ * Translated using Weblate (Korean) Currently translated at 76.1% (160 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ko/ * Translated using Weblate (Korean) Currently translated at 76.1% (160 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ko/ * Translated using Weblate (Italian) Currently translated at 97.1% (204 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/it/ * Translated using Weblate (Italian) Currently translated at 97.1% (204 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/it/ * Translated using Weblate (Assamese) Currently translated at 82.8% (174 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/as/ * Translated using Weblate (Assamese) Currently translated at 82.8% (174 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/as/ * Translated using Weblate (Vietnamese) Currently translated at 75.2% (158 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/vi/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 89.0% (187 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/pt_BR/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 89.0% (187 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/pt_BR/ * Translated using Weblate (Romanian) Currently translated at 78.5% (165 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ro/ * Translated using Weblate (Romanian) Currently translated at 78.5% (165 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ro/ * Translated using Weblate (Filipino) Currently translated at 95.7% (201 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/fil/ * Translated using Weblate (Filipino) Currently translated at 95.7% (201 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/fil/ * Translated using Weblate (German) Currently translated at 92.8% (195 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/de/ * Translated using Weblate (German) Currently translated at 92.8% (195 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/de/ * Translated using Weblate (Greek) Currently translated at 79.0% (166 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/el/ * Translated using Weblate (Greek) Currently translated at 79.0% (166 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/el/ * Translated using Weblate (Croatian) Currently translated at 30.4% (64 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/hr/ * Translated using Weblate (Croatian) Currently translated at 30.4% (64 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/hr/ * Translated using Weblate (Arabic) Currently translated at 92.3% (194 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ar/ * Translated using Weblate (Arabic) Currently translated at 92.3% (194 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ar/ * Translated using Weblate (Dutch) Currently translated at 79.5% (167 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/nl/ * Translated using Weblate (Dutch) Currently translated at 79.5% (167 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/nl/ * Translated using Weblate (Japanese) Currently translated at 84.7% (178 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ja/ * Translated using Weblate (Japanese) Currently translated at 84.7% (178 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ja/ * Translated using Weblate (Finnish) Currently translated at 79.0% (166 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/fi/ * Translated using Weblate (Finnish) Currently translated at 79.0% (166 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/fi/ * Translated using Weblate (Hindi) Currently translated at 27.1% (57 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/hi/ * Translated using Weblate (Portuguese) Currently translated at 87.1% (183 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/pt/ * Translated using Weblate (Indonesian) Currently translated at 92.8% (195 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/id/ * Translated using Weblate (Tamil) Currently translated at 35.2% (74 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ta/ * Translated using Weblate (Polish) Currently translated at 80.9% (170 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/pl/ * Translated using Weblate (Polish) Currently translated at 80.9% (170 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/pl/ * Translated using Weblate (Catalan) Currently translated at 74.7% (157 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ca/ * Translated using Weblate (Catalan) Currently translated at 74.7% (157 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ca/ * Translated using Weblate (Korean) Currently translated at 76.1% (160 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/ko/ * Translated using Weblate (Filipino) Currently translated at 96.6% (203 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/fil/ * Translated using Weblate (Filipino) Currently translated at 97.1% (204 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/fil/ * Translated using Weblate (Assamese) Currently translated at 100.0% (210 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/as/ * Translated using Weblate (Assamese) Currently translated at 100.0% (210 of 210 strings) Translation: Komikku-App/Komikku Translate-URL: https://hosted.weblate.org/projects/komikku-app/komikku/as/ * [ar] `connections_discord_info`: placeholder for app name Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [ar] `pref_discord_idle`: don't use emoji tag Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [ro] `pref_discord_idle`: don't use emoji tag Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [zh-rCN] `connections_discord_info`: placeholder for app name Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [ro] `connections_discord_info`: placeholder for app name Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [pt-rBR] `discord_status_description`: placeholder Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [bn] `action_scroll_to_top`: remove quote sign Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [ca] `pref_discord_show_download_button`: translate Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * [da] `connections_discord`: correct Discord name Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [es] `connections_discord`: correct Discord name Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [fi] `connections_discord_info`: placeholder for app name Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [hi] `pref_discord_idle`: correct use of word Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [hi] `pref_theme_cover_based_style_fruitsalad`: type Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [hr] `action_open_folder`: use folder instead of file Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [it] `pref_fetch_manga_chapters_on_add_description`: correct translation Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [ja] `pref_discord_idle`: translate Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [ko] `pref_discord_show_timestamp`: correct use of words Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * [ms] `download_cache_renew_interval_12hour`: remove AM/PM Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * [nl] `connections_discord`: correct Discord Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * [ta] `scale_type_16_9`: remove space Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [tr] `updating`: grammar Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [tr] `pref_auto_sync_progress_from_trackers`: translate * [uk] `pref_discord_custom_message`: correct translation Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [zh-rCN] `discord_status_description`: add space Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: Weblate Translation Memory Co-authored-by: Weblate Co-authored-by: 안세훈 Co-authored-by: Infy's Tagalog Translations Co-authored-by: Itsmechinmoy <167056923+itsmechinmoy@users.noreply.github.com> Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../commonMain/moko-resources/ar/strings.xml | 37 ++++++ .../commonMain/moko-resources/as/strings.xml | 73 +++++++++++ .../commonMain/moko-resources/bn/strings.xml | 49 ++++++- .../commonMain/moko-resources/ca/strings.xml | 122 ++++++++++++++++++ .../commonMain/moko-resources/cs/strings.xml | 57 ++++++++ .../commonMain/moko-resources/da/strings.xml | 53 ++++++++ .../commonMain/moko-resources/de/strings.xml | 3 + .../commonMain/moko-resources/el/strings.xml | 52 ++++++++ .../commonMain/moko-resources/es/strings.xml | 47 +++++++ .../commonMain/moko-resources/fi/strings.xml | 54 ++++++++ .../commonMain/moko-resources/fil/strings.xml | 22 ++++ .../commonMain/moko-resources/fr/strings.xml | 3 + .../commonMain/moko-resources/hi/strings.xml | 60 ++++++++- .../commonMain/moko-resources/hr/strings.xml | 62 +++++++++ .../commonMain/moko-resources/in/strings.xml | 3 + .../commonMain/moko-resources/it/strings.xml | 7 + .../commonMain/moko-resources/ja/strings.xml | 38 ++++++ .../commonMain/moko-resources/ko/strings.xml | 96 ++++++++++++++ .../commonMain/moko-resources/ms/strings.xml | 42 ++++++ .../commonMain/moko-resources/nl/strings.xml | 54 ++++++++ .../commonMain/moko-resources/pl/strings.xml | 52 ++++++++ .../moko-resources/pt-rBR/strings.xml | 41 ++++++ .../commonMain/moko-resources/pt/strings.xml | 2 + .../commonMain/moko-resources/ro/strings.xml | 52 ++++++++ .../commonMain/moko-resources/sv/strings.xml | 52 ++++++++ .../commonMain/moko-resources/ta/strings.xml | 73 +++++++++++ .../commonMain/moko-resources/th/strings.xml | 59 ++++++++- .../commonMain/moko-resources/tr/strings.xml | 46 +++++++ .../commonMain/moko-resources/uk/strings.xml | 44 +++++++ .../commonMain/moko-resources/vi/strings.xml | 21 +++ .../moko-resources/zh-rCN/strings.xml | 48 +++++++ 31 files changed, 1421 insertions(+), 3 deletions(-) diff --git a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml index df7a724f5..140ddc34d 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml @@ -162,4 +162,41 @@ البيانات التي تم تنزيلها الفصول والسجل من قاعدة البيانات تثبيت يدوي + خدمة ديسكورد هذه تعمل من خلال تسجيل الرموز. استخدمها على مسؤوليتك الخاصة. يتم تخزين الرمز المميز الخاص بك محليا ولا يقوم %s بمشاركته في أي مكان آخر + وجود ديسكورد الغني + إظهار عنوان الحلقة التي تشاهدها + حسابات ديسكورد + الحساب النشط + التمرير عبر + تم تجاوزها عند تمكين وضع التخفي + اتصالات + 🌙 الخمول + إظهار التقدم + إظهار الوقت + لم يتم العثور على حسابات + فشل في تسجيل الدخول + الخدمات الخاصة + ديسكورد، أكثر من ذلك.. + ديسكورد + تخصيص ديسكورد + رسالة مخصصة + رسالة مخصصة سيتم عرضها على ديسكورد + إظهار الأزرار + عرض الأزرار لتنزيل التطبيق أو الانضمام إلى خادم ديسكورد + يعرض زر لتنزيل التطبيق + إظهار زر ديسكورد + تمكين وجود ديسكورد الغني + حالة ديسكورد + ⛔ الرجاء عدم الإزعاج + 🟢 متصل + وضع التخفي RPC + المدخلات في الفئات المدرجة لن يتم عرضها في ديسكورد + تصفح + المراسلة مع + إظهار زر التحميل + يعرض زرًا للانضمام إلى خادم ديسكورد + وضع التخفي ديسكورد + باستخدام: + إدعمني + %1$s %2$s diff --git a/i18n-kmk/src/commonMain/moko-resources/as/strings.xml b/i18n-kmk/src/commonMain/moko-resources/as/strings.xml index c428c24a1..5e73506cb 100644 --- a/i18n-kmk/src/commonMain/moko-resources/as/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/as/strings.xml @@ -137,4 +137,77 @@ আপডেট কৰিবলগীয়া মোক স্পনছৰ কৰক সমৰ্থন: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + ডিস্ক’ৰ্ড ৰিচ প্ৰেজেন্স + সক্ৰিয় একাউণ্ট + সংযোগ + ডিস্ক’ৰ্ড, আৰু আহিব... + 🌙 নিষ্ক্ৰিয় + 🟢 অনলাইন + ডিস্ক’ৰ্ড একাউণ্ট + ডিস্ক’ৰ্ড + এই ডিস্ক\'ৰ্ড সেৱা ট\'কেন লগিঙৰ জৰিয়তে কাম কৰে। নিজৰ ঝুঁকিত ব্যৱহাৰ কৰক। আপোনাৰ ট\'কেন স্থানীয়ভাৱে সাঁচৱা হয় আৰু %s এ ইয়াক অন্য ক\'ত শ্বেয়াৰ নকৰে + ইনকগনিটো মোড সক্ৰিয় হ’লে ওভাৰৰাইড কৰা হয় + ব্ৰাউজ কৰি আছে + মাজেৰে স্ক্ৰ’ল কৰি আছে + ডিস্ক’ৰ্ড কাষ্টমাইজেশ্যন + কাষ্টম বাৰ্তা + বিশেষ সেৱা + অন্তৰ্ভুক্ত শ্ৰেণীৰ এণ্ট্ৰি ডিস্ক’ৰ্ডত দেখুৱা নহ’ব + কোনো একাউণ্ট পোৱা নগ’ল + লগইন বিফল + ডিস্ক’ৰ্ডত দেখুৱা হ’বলগীয়া কাষ্টম বাৰ্তা + প্ৰগ্ৰেছ দেখুৱাওক + সময় দেখুৱাওক + বাটন দেখুৱাওক + এপ ডাউনলোড বা ডিস্ক’ৰ্ড চাৰ্ভাৰত যোগদানৰ বাটন দেখুৱাওক + ডিস্ক’ৰ্ড বাটন দেখুৱাওক + আৰ পি চি ইনকগনিটো + ডাউনলোড বাটন দেখুৱাওক + এপ ডাউনলোডৰ বাবে এটা বাটন দেখুৱায় + ডিস্ক’ৰ্ড ইনকগনিটো মোড + খেলা-ধূলা কৰি আছে + ডিস্ক’ৰ্ড চাৰ্ভাৰত যোগদানৰ বাবে এটা বাটন দেখুৱায় + আপুনি পঢ়ি থকা অধ্যায়ৰ শিৰোনাম দেখুৱাওক + ডিস্ক’ৰ্ড ৰিচ প্ৰেজেন্স সক্ৰিয় কৰক + ডিস্ক’ৰ্ডৰ স্থিতি + ⛔ বিৰক্ত নকৰিব + ইউজাৰনেম + পাছৱাৰ্ড + %1$s %2$s + অসম্পূৰ্ণ মাংগা + অসম্পূৰ্ণ অধ্যায় + পুথিভঁৰালৰ বাহিৰৰ এণ্ট্ৰি + পিঞ্চ টু জুম + জুম ইন নিষ্ক্ষিয় কৰক + বহল ছবিৰ জুম মোড + ডাবল জুম + বহল স্ক্ৰীনত স্মাৰ্ট স্কেল + দীঘল ষ্ট্ৰিপত ফাঁকসহ স্মাৰ্ট স্কেল প্ৰয়োগ + ১৬:৯ + ২০:৯ + ৪:৩ + ৩:২ + সন্ধান/ফিল্টাৰৰ সময়ত খালী শ্ৰেণী দেখুৱাওক + পুথিভঁৰালত যোগ কৰোঁতে মেটাডেটা আনক + পুথিভঁৰালত নতুন মাংগা যোগ কৰোঁতে স্বয়ংক্ৰিয়ভাৱে মেটাডেটা আনে + পুথিভঁৰালত যোগ কৰোঁতে অধ্যায় আনক + পুথিভঁৰালত নতুন মাংগা যোগ কৰোঁতে স্বয়ংক্ৰিয়ভাৱে অধ্যায়ৰ তালিকা আনে + ৱেবডেভি + ৱেবডেভি চাৰ্ভাৰ ইউআৰএল + ৱেবডেভি চাৰ্ভাৰৰ ঠিকনা দিয়ক + আপোনাৰ ৱেবডেভি একাউণ্টৰ ইউজাৰনেম দিয়ক + আপোনাৰ ৱেবডেভি একাউণ্টৰ পাছৱাৰ্ড দিয়ক + ৱেবডেভি ডেটা ফ\'ল্ডাৰ + এপ্লিকেশ্যনে এই ফ\'ল্ডাৰত তথ্য সাঁচিব + স্মাৰ্ট সন্ধান ব্যৱহাৰ কৰক + সক্ৰিয় কৰিলে স্বয়ংক্ৰিয়ভাৱে সৰ্বোত্তম ফলাফল বাছি লয়। অন্যথা, মেনুৱেল বাছনিৰ বাবে সকলো ফলাফল দেখুৱায়। + স্বয়ংক্ৰিয় পুথিভঁৰাল আপডেট নিৰ্ধাৰণ বিফল: %s + স্বয়ংক্ৰিয় বেকআপ নিৰ্ধাৰণ বিফল: %s + ডিস্ক\'ৰ্ড আৰ পি চি কনফিগাৰ কৰিবলৈ খোলক + অধ্যায়ৰ শিৰোনাম দেখুৱাওক + ব্যৱহাৰ কৰি আছে + %s-ত পঢ়ক + কমিককু - এটা মাংগা ৰিডাৰ এপ + বৰ্তমান পঢ়ি থকা অধ্যায় দেখুৱাওক + কাৰ্যকলাপৰ সময় দেখুৱাওক diff --git a/i18n-kmk/src/commonMain/moko-resources/bn/strings.xml b/i18n-kmk/src/commonMain/moko-resources/bn/strings.xml index 55344e519..4e1c4a4c2 100644 --- a/i18n-kmk/src/commonMain/moko-resources/bn/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/bn/strings.xml @@ -1,3 +1,50 @@ - \ No newline at end of file + ১২ ঘন্টা + ১ দিন + খোল + সংযোগ + %1$s %2$s + আপডেট সম্পন্ন হয়েছে + ডিসকর্ড + সূর্যাস্ত + মনোক্রোম + নিজ হাতে নিজের চেষ্টায় ইন্সটল + নিরপেক্ষ + প্রাণবন্ত + উপরে যান + লুকান + ফোল্ডার খুলুন + অভিব্যক্তিপূর্ণ + রংধনু + কাস্টম থিম + রাস্পবেরি + আপডেট ইনস্টল করা যায়নি + পরামর্শ + ডাউনলোড বোতাম দেখও + ওয়েব-ড্যাভ + নাম + পাসওয়ার্ড + রিলিজ পাতা + উপরে  স্ক্রল করুন + বিঘ্নমুক্ত পরিবেশ + নিষ্ক্রিয় + সক্রিয় + দেখছি + ডিসকর্ড রিচ প্রেজেন্স + টোনাল স্পট + ফলের সালাদ + বিশ্বস্ততা + বিষয়বস্তু + ম্যানুয়াল + ১ ঘন্টা + FAQ এবং নির্দেশনা + নীচে যান + আপডেট + ৩ঃ২ + আপডেট করা + ৬ ঘন্টা + স্বয়ংক্রিয় আপডেট অ্যাপ + সব এড়িয়ে যান + ২ ঘণ্টা + diff --git a/i18n-kmk/src/commonMain/moko-resources/ca/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ca/strings.xml index 3d5e2dc09..4cdade365 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ca/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ca/strings.xml @@ -35,4 +35,126 @@ Pessiga per ampliar Desactivar el zoom Doble zoom + Manual + Auto-update app + Permission granted. Try action again. + If set to \"manual\", either refresh each manga or using the \"Invalidate downloads index\" action in \"Advanced\" + Allow using custom covers in entry\'s Edit Info + More like these... + Allow all + Manga Info + Monochrome + Scroll to next + Skip it + Tonal Spot + Home from Suggestions + Suggestions + Panorama Cover + LCD + Library search + Toggle NSFW only + Custom theme style + Theme based on cover + Cover based theme style + Neutral + Vibrant + Show restoring progress banner + Show syncing progress banner + Clean invalid downloads + Invalid downloads cleaned + Avoid using Private Installer because it\'s not stable yet, will lead to really slow start/resume App. + Allow it + Skip all + Update completed + Release page + Could not install update + Coming (soon) updates! + Manual download + Show source-website\'s Suggestions while viewing entry + Scroll to top + Scroll to bottom + Source icon + Find and remove all downloads, files, folders which are not saved in your library + Preload library\'s cover color + Don\'t auto download update + Access media files permission + Website Suggestions + Scroll to previous + To be updated + Expressive + Rainbow + Fidelity + Show cover in landscape mode if it\'s a wide image + Panorama Cover Flow + Show panorama cover in Global Search & Suggestions + Panorama comfortable grid + Expand Suggestions + If turned off, Suggestions won\'t automatically load & show expanded + Suggestions in overflow + Put the Suggestions button in the overflow menu instead of on the entry page + Show an icon while browsing Suggestions to navigate directly back to Home + Cloudflare + Doom + Palette Personalizer + Pick a seed color for the custom Theme + Custom Theme + Casual + Outrun + Raspberry + Cathode + Bubblegum + Night at the beach + Springfield + PICO-8 + Show updating progress banner + Sync progress from Trackers up to chapter %d + Saved Searches & Feeds + Allow saving covers & using custom covers in entry\'s Edit Info + What\'s coming (soon) + Library update errors + You have no library update errors. + Too many sources in your feed, cannot add more than 20 + Sort feeds + Would you like to sort the feeds alphabetically? + More extensions... + Download cache renew interval + Change Logs + Saved Searches (Hold to add to Feed) + Saved Searches (Hold to delete) + Library update errors (%d) + Current: %1$s + Try now + Auto sync progress from Trackers + Mocha + Spectral + Patagonia + Fruit Salad + Content + Pop + Use language icon + Refresh extension page to update list. + Inactiu + Cap compte trobat + Navegació + Instal·lació manual + Actualització + No molesteu + En línia + 3:2 + WebDAV + Nom d\'usuari + Contrasenya + Discord + Estat d\'activitat de Discord + Fallida l\'accedir + Mitjançant: + Mostra l\'hora + Mostra els botons + 16:9 + Compte actiu + %1$s %2$s + Mostra el botó de descàrrega + Obrir el menú de rastreig al afegir a la biblioteca + Activa la Discord Rich Presence + Connexions diff --git a/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml b/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml index 0b020bcd7..769f155a4 100644 --- a/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml @@ -116,4 +116,61 @@ Automaticky nestahovat aktualizaci Odstranit původní položky z knihovny? Podpora: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Bubblegum + Discord účty + Vlastní zpráva, která bude zobrazena na Discordu + Zobrazit Discord tlačítko + Zobrazí tlačítko pro připojení k Discord serveru + Outrun + Pop + Discord, více k nám. + Discord + Tato Discord služba funguje pomocí logování tokenů. Používejte na vlastní riziko. Váš token je uložen lokálně a Anikku jej nesdílejí nikde jinde + Discord Rich Presence + ⛔ Režim Nerušit + 🌙 Nečinný + Zobrazit tlačítko Stahování + Zobrazí tlačítko ke stažení aplikace + Speciální služby + Kontakty + Zobrazit název epizody, kterou sledujete + Povolit Discord Rich Presence + Stav Discordu + RPC Incognito + Discord anonymní režim + Přepsáno při povoleném anonymním režimu + Aktivní účet + Nebyly nalezeny žádné účty + Přihlášení se nezdařilo + Procházení + Posunutí přes + Vlastní zpráva + Zobrazit čas + Zobrazit tlačítka ke stažení aplikace nebo se připojit k Discord serveru + Zobrazit obal zarovnaný nahoře vedle informací anime + Doom + Springfield + Poslání s + Zobrazit průběh + Mocha + 🟢 Online + Položky v zahrnutých kategoriích nebudou zobrazeny v Discordu + Přizpůsobení Discordu + Sapphire + Zobrazit tlačítka + Uživatelské jméno + Heslo + Ruční instalace + Podpoř mě + 16:9 + Otevřít nabídku stopování při přidávání do knihovny + Otevřít složku + Aktualizace + 3:2 + WebDAV + %1$s %2$s + Používání + Číst na %s + Aktualizace + Nelze otevřít složku. diff --git a/i18n-kmk/src/commonMain/moko-resources/da/strings.xml b/i18n-kmk/src/commonMain/moko-resources/da/strings.xml index 0df4bf402..68a9b49e7 100644 --- a/i18n-kmk/src/commonMain/moko-resources/da/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/da/strings.xml @@ -116,4 +116,57 @@ Download ikke automatisk opdatering Fjern originale indgange fra biblioteket? Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Sapphire + Særlige Tjenester + Indgange i inkluderede kategorier vil ikke blive vist i Discord + Rul gennem + Mocha + Springfield + Discord Rig Tilstedeværelse + Forbindelser + Discord, mere at komme. + Denne Discord Service virker gennem token logning. Bruges på egen risiko. Dit token gemmes lokalt, og Anikku deler det ikke andre steder + Aktiver Discord Rig Tilstedeværelse + 🟢 Online + Viser en knap til download af appen + Discord Status + ⛔ Forstyr Ikke + 🌙 Tomgang + Tilsidesæt når Inkognitotilstand er aktiveret + Ingen konti fundet + Gennemser + Beskeder rundt med + Brugerdefineret Besked + Brugerdefineret besked, der vil blive vist på Discord + Vis Tid + Vis Knapper + Vis knapper til at downloade appen, eller deltag i Discord serveren + Vis Download-Knap + Viser en knap til at deltage i Discord serveren + Tonal Spot + Vis coveret justeret til toppen sammen med anime info + Outrun + Pop + Bubblegum + Kunne ikke logge ind + Discord Konti + Vis titlen på den episode du ser + Aktiv Konto + Discord Tilpasning + Vis Discord Knap + RPC Incognito + Discord Inkognito tilstand + Vis Fremskridt + Discord + WebDAV + Brugernavn + Adgangskode + Manuel installation + %1$s %2$s + Bruger: + Opdaterer + Åbn mappe + Opdater + 16:9 + 3: 2 diff --git a/i18n-kmk/src/commonMain/moko-resources/de/strings.xml b/i18n-kmk/src/commonMain/moko-resources/de/strings.xml index ea143a52c..455b90577 100644 --- a/i18n-kmk/src/commonMain/moko-resources/de/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/de/strings.xml @@ -192,4 +192,7 @@ 🟢 Online %1$s %2$s Discord Rich Presence + WebDAV + Benutzername + Passwort diff --git a/i18n-kmk/src/commonMain/moko-resources/el/strings.xml b/i18n-kmk/src/commonMain/moko-resources/el/strings.xml index 306c70fa9..3344b16ed 100644 --- a/i18n-kmk/src/commonMain/moko-resources/el/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/el/strings.xml @@ -117,4 +117,56 @@ Να μην γίνεται αυτόματη λήψη ενημέρωσης Αφαιρέστε τις αρχικές καταχωρήσεις από τη βιβλιοθήκη? Υποστήριξη: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Sapphire + Ειδικές Υπηρεσίες + Discord + Discord Πλούσια Παρουσία + 🟢 Online + Δεν βρέθηκαν λογαριασμοί + Outrun + Κύλιση μέσα + Εμφάνιση Κουμπιών + Προσαρμοσμένο Μήνυμα + Εμφάνιση Προόδου + Εμφάνιση κουμπιών για λήψη της εφαρμογής ή σύνδεση με τον διακομιστή Discord + Εμφανίζει ένα κουμπί για να συνδεθεί με τον διακομιστή του Discord + Discord, περισσότερα για να έρθει.. + Αυτή η υπηρεσία του Discord λειτουργεί μέσω καταγραφής συμβόλων. Χρησιμοποιήστε το με δική σας ευθύνη. Το διακριτικό αποθηκεύεται τοπικά και το Anikku δεν το μοιράζεται πουθενά αλλού + Εμφάνιση του τίτλου του επεισοδίου που παρακολουθείτε + Ενεργοποίηση Discord Rich Presence + Κατάσταση Discord + ⛔ Μην Ενοχλείτε + 🌙 Αδρανής + RPC Incognito + Ανώνυμη περιήγηση σε Discord + Αντικαταστάθηκε όταν είναι ενεργοποιημένη η λειτουργία ανώνυμης περιήγησης + Οι καταχωρήσεις στις κατηγορίες που περιλαμβάνονται δεν θα εμφανίζονται στο Discord + Λογαριασμοί Discord + Ενεργός Λογαριασμός + Ανταλλαγή μηνυμάτων με + Προσαρμογή Discord + Προσαρμοσμένο μήνυμα που θα εμφανίζεται στο Discord + Εμφάνιση Κουμπιού Λήψης + Εμφανίζει ένα κουμπί για λήψη της εφαρμογής + Tonal Spot + Show panorama cover in Global Search & Suggestions + Springfield + Εμφάνιση του καλύμματος που είναι ευθυγραμμισμένο στην κορυφή παράλληλα με τις πληροφορίες anime + Cathode + Εμφάνιση Ώρας + Συνδέσεις + Εμφάνιση Κουμπιού Discord + Αποτυχία σύνδεσης + Περιήγηση + Όνομα χρήστη + Χειροκίνητη εγκατάσταση + Ενημέρωση + 16:9 + WebDAV + Κωδικός + %1$s %2$s + 3:2 + Γίνεται ενημέρωση + Άνοιγμα φακέλου + Χρησιμοποιεί: diff --git a/i18n-kmk/src/commonMain/moko-resources/es/strings.xml b/i18n-kmk/src/commonMain/moko-resources/es/strings.xml index fd83f9a4e..96c6b3dd2 100644 --- a/i18n-kmk/src/commonMain/moko-resources/es/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/es/strings.xml @@ -135,4 +135,51 @@ Abrir menú de seguimiento al añadir a la biblioteca Datos descargados Error al iniciar sesión + Nombre de usuario + Contraseña + Instalación manual + WebDAV + Patrociname + Sigue leyendo en %s + Utilización + %1$s %2$s + Reemplazado cuando el modo Incognito está activado + 🌙 Inactivo + 🟢 Online + Mensaje personalizado + RPC Incognito + Pop + Outrun + Discord, más por venir.. + Este Servicio de Discord funciona a través del registro de token. Úsalo bajo tu propio riesgo. Tu token se almacena localmente y Anikku no lo comparte en ningún otro lugar + Episodios e historial de la base de datos + Conexiones + Discord + Presencia de Discord Rich + Habilitar la presencia de Discord Rich + ⛔ No molestar + Modo Incognito de Discord + Las entradas en las categorías incluidas no se mostrarán en Discord + Desplazándose a través + Personalización de Discord + Mensaje personalizado que se mostrará en Discord + Mostrar progreso + Mostrar hora + Patagonia + Springfield + Muestra el título del episodio que estás viendo + Estado de Discord + Servicios Especiales + Navegando + Cuenta activa + No hay cuentas encontradas + Muestra un botón para unirse al servidor de Discord + Doom + Cuentas de Discord + Mensajes alrededor con + Mostrar botones + Mostrar botones para descargar la aplicación o unirse al servidor de Discord + Mostrar botón de descarga + Muestra un botón para descargar la aplicación + Mostrar botón de Discord diff --git a/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml index a57321d05..34ce68ff0 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml @@ -115,4 +115,58 @@ Älä lataa automaattisesti päivitystä Poistetaanko alkuperäiset merkinnät kirjastosta? Tuki: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Discord Rich Presence + Kirjautuminen epäonnistui + Sapphire + Outrun + Bubblegum + Patagonia + Ohita kun Incognito-tila on käytössä + Tilejä ei löytynyt + Mukautettu viesti, joka näytetään Discordissa + Näytä Edistyminen + Näytä painikkeet, joilla voit ladata sovelluksen tai liittyä Discord-palvelimeen + Discord + Tämä Discord-palvelu toimii tunnuksen lokilla. Käytä omalla vastuullasi. Tunnuksesi tallennetaan paikallisesti ja %s ei jaa sitä mistään muualta. + Discordin Tila + ⛔ Älä Häiritse + Discord Incognito -tila + Discordin Mukauttaminen + Mukautettu Viesti + Näytä Latauspainike + Näyttää painikkeen, jolla sovellus ladataan + Näyttää painikkeen, jolla voit liittyä Discord-palvelimeen + Näytä kansi, joka on linjattu yläreunaan anime info + Spectral + Yhteydet + Discord, lisää tullaan.. + Ota Discordin Rich Presence Käyttöön + 🌙 Käyttämätön + 🟢 Online + RPC Incognito + Sisällytettyjä kategorioita ei näytetä Discordissa + Vieritä läpi + Näytä Aika + Näytä Discord-painike + Mocha + Springfield + Discordin Tilit + Aktiivinen Tili + Selataan + Näytä sen jakson otsikko, jonka katselet + Doom + Erityiset Palvelut + Messing noin kanssa + Näytä Painikkeet + Päivitä + WebDAV + Käyttäjätunnus + Salasana + Manuaalinen asennus + Käyttäen: + 16:9 + 3:2 + Päivitetään + %1$s %2$s + Avaa kansio diff --git a/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml index 35461dd78..4a1623ca0 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml @@ -182,4 +182,26 @@ 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 + Monochrome + Fidelity + Panorama Cover + Panorama Cover Flow + Panorama comfortable grid + Doom + Mocha + Outrun + Raspberry + Pop + Cathode + Bubblegum + Spectral + Patagonia + Springfield + Discord + %1$s %2$s + Username (pangalan) + Password + Gamitin ang matalinong paghahanap + Kapag napagana, awtomatikong pipiliin ang pinaka-akmang resulta. Kung hindi, ipakikita ang lahat ng resulta para manual na makapili. + Folder ng WebDAV data diff --git a/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml index b7baa9ace..ed23d640b 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml @@ -192,4 +192,7 @@ Afficher un bouton pour télécharger l\'application Afficher le bouton Discord Afficher un bouton pour rejoindre le serveur Discord + WebDAV + Nom d\'utilisateur + Mot de passe diff --git a/i18n-kmk/src/commonMain/moko-resources/hi/strings.xml b/i18n-kmk/src/commonMain/moko-resources/hi/strings.xml index a6b3daec9..77b71f88b 100644 --- a/i18n-kmk/src/commonMain/moko-resources/hi/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/hi/strings.xml @@ -1,2 +1,60 @@ - \ No newline at end of file + + 12 घंटे + हस्तचालित + 1 घंटा + सभी छोड़ें + अयांत्रिक संस्थापन + सुझाव + ऊपर स्क्रॉल करें + नीचे तक जाएं + ब्राउज़िंग + प्रगति दिखाएं + मेघधनुष + एक रंग का + निष्क्रिय + ऊपर ले जाएँ + चमकीला + कयामत + सामान्य प्रश्नोत्तर और मार्गदर्शन + तटस्थ + अद्यतन पूर्ण हुआ + फ़ोल्डर खोलें + अद्यतन कर रहे हैं + बटन दिखाएं + Show download button + 3:2 + उपयोगकर्ता नाम + पासवर्ड + वर्तमान: %1$s + कनेक्शन + डिस्कॉर्ड + डिस्कॉर्ड रिच प्रेजेंस + %1$s %2$s + उपयोग में: + माँगा जानकारी + टोनल स्पॉट + अभिव्यंजक + फल सलाद + फिडेलिटी + सामग्री + आव्यूह + नीलम + कस्टम थीम + सूर्यास्त + रसभरी + १ दिन + डिस्टर्ब न करें + ऑनलाइन + 2 घंटे + 6 घंटे + मैनुअल डाउनलोड + अपडेट इंस्टॉल नहीं किया जा सका + समय दिखाएं + खोलें + रिलीज़ पेज + नीचे ले जाएँ + छुपाएं + सभी की अनुमति दें + अद्यतन + diff --git a/i18n-kmk/src/commonMain/moko-resources/hr/strings.xml b/i18n-kmk/src/commonMain/moko-resources/hr/strings.xml index 0a030ddb5..4b654cfc1 100644 --- a/i18n-kmk/src/commonMain/moko-resources/hr/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/hr/strings.xml @@ -2,4 +2,66 @@ Cloudflare Previše izvora u tvom feedu, ne možeš dodati više od 20 + Ručno preuzimanje + Još više proširenja + 1 sat + 2 sata + 12 sati + 1 dan + WebDAV + Preskoči sve + Stranica izdanja + Nije moguće instalirati ažuriranje + 16:9 + Jednobojno + Online + Neutralni + ČPP i vodiči + Pomakni gore + Sakrij + Dopusti sve + Otvori mapu + Nadopunjivanje + Otvori izbornik zapisa prilikom dodavanja u biblioteku + Veze + Prikaži vrijeme + Korisničko ime + Lozinka + Ručno instaliranje + Prijedlozi + Discord + Bogata prisutnost Discorda + Enable Discord Rich Presence + Prikaži gumbove + Prikaži gumb za preuzimanje + Duga + Zalazak sunca + OutRun + Spektralno + LCD + %1$s %2$s + Koristi: + Čitaj na %s + Pop + Pomakni se skroz dolje + Informacije o mangi + 6 sati + Aktualiziranje je završeno + Mirovanje + Ne smetaj + Prikaz Napretka + Traži izvore + Pomakni dolje + Sadržaj + Vlastita tema + Opušteno + Ručno podešavanje + Automatsko aktualiziranje programa + Open + Ažuriraj + Matrix + Cotton Candy + Doom + Mocha + Sapphire diff --git a/i18n-kmk/src/commonMain/moko-resources/in/strings.xml b/i18n-kmk/src/commonMain/moko-resources/in/strings.xml index 1c362be11..a0e84b533 100644 --- a/i18n-kmk/src/commonMain/moko-resources/in/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/in/strings.xml @@ -192,4 +192,7 @@ Menampilkan tombol untuk mengunduh aplikasi Tampilkan Tombol Discord Menampilkan tombol untuk bergabung dengan server Discord + WebDAV + Nama Pengguna + Kata sandi diff --git a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml index b1f921db7..57afd2300 100644 --- a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml @@ -198,4 +198,11 @@ Regolazione automatica per la visualizzazione su schermo panoramico Apri il menu tracciamento quando aggiungi una voce alla libreria Naviga + Pizzica per ingrandire + Utilizzando: + RPC Incognito + Scorrimento attraverso + Messaggiare con + Recupera automaticamente i metadati di un nuovo manga quando lo aggiungi alla libreria + Recupera automaticamente l'elenco dei capitoli di un nuovo manga quando lo aggiungi alla libreria diff --git a/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml index 3d525c496..c96bb7181 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml @@ -148,4 +148,42 @@ Discord 今後も続々登場予定 Discord 本Discordサービスは、トークンを記録することで動作します。利用は自己責任でお願いします。トークンは端末内に保存され、%s が外部と共有することはありません。 + 含まれるカテゴリのエントリはDiscordに表示されません + Discordアカウント + アカウントが見つかりません + 閲覧 + ふざけてる人 + 進行状況を表示 + スクロール中 + Outrun + あなたが見ているエピソードのタイトルを表示する + Discord ステータス + ログインに失敗しました + Discordのカスタマイズ + カスタムメッセージ + Discordに表示されるカスタムメッセージ + 時間を表示 + ボタンを表示 + ダウンロードボタンを表示 + アプリをダウンロードするボタンを表示します + Rainbow + Discord Rich Presenceを有効にする + 🌙 離席中 + 🟢 オンライン + RPC Incognito + シークレットモードが有効になっているときにオーバーライドする + アプリをダウンロードしたりDiscordサーバーに参加するボタンを表示します + Discordシークレットモード + Spectral + Springfield + Discord Rich Presence + アクティブなアカウント + ⛔ マナーモード + スポンサーになりませんか + WebDAV + ユーザー名 + パスワード + %1$s %2$s + 使用: + %s で読む diff --git a/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml index 9f12eb296..92d48563a 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml @@ -64,4 +64,100 @@ 업데이트 중 배너 표시 소스 아이콘 언어 아이콘 사용 + If set to \"manual\", either refresh each manga or using the \"Invalidate downloads index\" action in \"Advanced\" + Auto sync progress from Trackers + Sync progress from Trackers up to chapter %d + Show syncing progress banner + Preload library\'s cover color + Access media files permission + Permission granted. Try action again. + Coming (soon) updates! + Show source-website\'s Suggestions while viewing entry + Scroll to bottom + To be updated + Pop + Monochrome + Skip it + Neutral + Tonal Spot + Expressive + Refresh extension page to update list. + More extensions... + Download cache renew interval + Manual + Open + Could not install update + Release page + Clean invalid downloads + Invalid downloads cleaned + Avoid using Private Installer because it\'s not stable yet, will lead to really slow start/resume App. + Saved Searches & Feeds + Auto-update app + Allow it + Allow all + Change Logs + Suggestions + Saved Searches (Hold to delete) + Library update errors + Library update errors (%d) + You have no library update errors. + Scroll to top + Rainbow + Fruit Salad + Fidelity + Casual + Sunset + Outrun + Raspberry + Cathode + LCD + Allow saving covers & using custom covers in entry\'s Edit Info + What\'s coming (soon) + Content + Bubblegum + Patagonia + Springfield + PICO-8 + Show restoring progress banner + Current: %1$s + Scroll to previous + Sort feeds + Would you like to sort the feeds alphabetically? + Don\'t auto download update + Night at the beach + Spectral + Find and remove all downloads, files, folders which are not saved in your library + Allow using custom covers in entry\'s Edit Info + Website Suggestions + More like these... + Saved Searches (Hold to add to Feed) + Scroll to next + Too many sources in your feed, cannot add more than 20 + Skip all + Try now + Update completed + Manual download + Vibrant + 서재에 추가할 때 동기화 메뉴 열기 + 디스코드 + 디스코드 리치 프레즌스 + 유휴 + 온라인 + %1$s %2$s + 맞춤 메시지 + 시간 표시 + Discord 리치 프레즌스 기능 켜기 + WebDAV + 사용자 이름 + 비밀번호 + 후원하기 + 버튼들 보이기 + 다운로드 단추 표시 + 업데이트 중 + 사용 중: + 검색 + 방해 금지 + 연결 + 명령어로 설치하기 + 진행 상황 표시 diff --git a/i18n-kmk/src/commonMain/moko-resources/ms/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ms/strings.xml index 278f2dc1e..09241b1b3 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ms/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ms/strings.xml @@ -1,4 +1,46 @@ Cari sumber + 1 jam + 2 Jam + 12 jam + 1 Hari + Kata Laluan + Auto-kemas kini apl + Langkau semua + Pengesyoran + Dayakan Discord Rich Presence + Content + Bergerak ke bawah + Tema Kustom + Sembunyi + Neutral + Bergerak ke atas + Soalan Lazim dan Panduan + azab + Buka folder + Kemas kini + Monokrom + nilam + Mengemaskini + Manual + Nama pengguna + kemas kini siap + Open + Hubungan + %1$s %2$s + Baca terus %s + Cubit untuk zum + pelangi + Cloudflare + Matriks + Rasberi + Halaman keluaran + Tidak dapat memasang kemas kini + Discord + Tunjukkan butang muat turun + Discord Rich Presence + Jangan ganggu + Terbiar + Di talian diff --git a/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml b/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml index ce9d4e2c2..09dbd6810 100644 --- a/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml @@ -116,4 +116,58 @@ Update niet automatisch downloaden Originele invoergegevens uit bibliotheek verwijderen? Ondersteuning: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Gebruikersnaam + Map openen + Updaten + Bezig met update + WebDAV + Wachtwoord + %1$s %2$s + met: + Knijp om te zoomen + 16:9 + 3:2 + Hand­matige installatie + Discord + Deze Discord Service werkt met token loggen. Gebruik op eigen risico. Je token wordt lokaal opgeslagen en Anikku deelt het nergens anders + 🟢 Online + RPC Incognito + Toont een knop om de app te downloaden + Toon Discord knop + Messing rond met + Discord aanpassingen + 🌙 Idle + Auto-update app + Toon de titel van de aflevering die je bekijkt + Discord Rich Presence inschakelen + Discord status + ⛔ Niet storen + Invoer in opgenomen categorieën zal niet worden weergegeven in Discord + Discord accounts + Browsen + Aangepast bericht dat zal worden getoond op Discord + Toon voortgang + Toon tijd + Knoppen weergeven + Toon knoppen om de app te downloaden of toe te treden tot de Discord server + Downloadknop weergeven + Toont een knop om toe te treden tot de Discord server + Toon de omslag uitgelijnd aan de bovenkant naast anime info + Outrun + Pop + Bubblegum + Springfield + Scrollen door + Aangepast bericht + Speciale diensten + Discord, meer te komen.. + Discord Incognito modus + Overschreven wanneer Incognito modus is ingeschakeld + Geen accounts gevonden + Inloggen mislukt + Mocha + Sapphire + Actieve account + Verbindingen + Discord Rich Presence diff --git a/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml b/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml index c1397db18..651dc7570 100644 --- a/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml @@ -121,4 +121,56 @@ Szafir Patagonia Springfield + Nazwa użytkownika + Hasło + Otwórz folder + 3:2 + WebDAV + Nie udało się otworzyć folderu. + 16:9 + 4:3 + Aktualizowanie + %1$s %2$s + Wykorzystując: + Wesprzyj mnie + Otwórz menu po dodaniu do biblioteki + Aktualizacja + Ta usługa Discorda działa poprzez rejestrowanie tokenów. Używaj na własne ryzyko. Twój token jest przechowywany lokalnie, a Anikku nigdzie indziej nie udostępnia. + Wpisy w dołączonych kategoriach nie będą wyświetlane w Discordzie + Konta Discorda + Aktywne konto + Logowanie nie powiodło się + Niestandardowa wiadomość, która będzie wyświetlana na Discordzie + Pokaż postęp + Pokaż przyciski + Wyświetla przycisk do pobrania aplikacji + Pokaż przycisk Discorda + 🟢 Online + Fidelity + Usługi specjalne + Połączenia + Status Discorda + ⛔ Nie przeszkadzać + Wiadomość niestandardowa + Pokaż tytuł oglądanego odcinka + Włącz Discord Rich Presence + 🌙 Bezczynny + RPC Incognito + Tryb Discord Incognito + Zastąpione, gdy włączony jest tryb incognito + Wyświetlaj przyciski, aby pobrać aplikację lub dołączyć do serwera Discorda + Pokaż przycisk Pobierz + Wyświetla przycisk dołączenia do serwera Discorda + Pokaż okładkę wyrównaną do góry obok informacji anime + Outrun + Nie znaleziono kont + Przeglądanie + Przewijanie przez + Komunikacja z + Discord, jeszcze więcej. + Discord + Wzbogacona obecność Discorda + Pokaż czas + Pop + Dostosowanie Discorda diff --git a/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml b/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml index 73ed54717..6e495609e 100644 --- a/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml @@ -147,4 +147,45 @@ Casual Outrun Abrir menu \'Acompanhar\' ao adicionar à biblioteca + Este serviço do Discord funciona através do registro de tokens. Use por sua conta e risco. Seu token é armazenado localmente e o Anikku não o compartilha em nenhum outro lugar + Exibir o título do episódio que você está assistindo + Habilitar o Rich Presence do Discord + Status do Discord + Substituído quando o Modo Anônimo está ativado + As entradas nas categorias incluídas não serão exibidas no Discord + Contas do Discord + Nenhuma conta encontrada + Falha ao logar + Exibe um botão para entrar no servidor do Discord + RPC Incognito + Navegação + Rolagem através + Mostrar progresso + Mostrar hora + Mostrar botões + Exibir botões para baixar o aplicativo ou entrar no servidor do Discord + Mostrar botão do Discord + Serviços Especiais + Conexões + Discord, mais para vir... + Personalização do Discord + Mensagem personalizada que será exibida no Discord + Mostrar botão de download + Exibe um botão para baixar o aplicativo + Rich Presence do Discord + Modo incógnito do Discord + Conta ativa + Envia mensagens com + Mensagem personalizada + Discord + ⛔ Não perturbe + 🌙 Idle + 🟢 Online + %1$s %2$s + Leia em %s + WebDAV + Nome de usuário + Senha + Uso + Tornar-se patrocinador diff --git a/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml b/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml index 9435f1e1e..21a6c4914 100644 --- a/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml @@ -192,4 +192,6 @@ Exibe um botão para baixar o aplicativo Mostrar botão do Discord Exibe um botão para entrar no servidor do Discord + Nome de Utilizador + Palavra passe diff --git a/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml index 4187dc493..3ff59cdfa 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml @@ -116,4 +116,56 @@ Nu descărca automat Eliminaţi intrările originale din bibliotecă? Suport: MangaDex, E-H, ExH, nH, 8Muse, Tsumino + Servicii speciale + Conexiuni + Cont activ + Mesaje în jur cu + Arată butonul de Descărcare + Afișează un buton pentru descărcarea aplicației + Arată coperta aliniată în partea de sus alături de informații anime + Afișează butoane pentru a descărca aplicația sau pentru a se alătura serverului Discord + Mocha + Patagonia + Discord, mai multe de făcut. + Arată butoanele + Prezență Discord Rich + Arată titlul episodului pe care îl urmăriți + ⛔ Nu deranja + 🟢 Online + Modul Incognito Discord + Suprascrie când modul Incognito este activat + Nici un cont găsit + Autentificare eșuată + Defilare prin + Personalizare Discord + Mesaj personalizat + Mesaj personalizat care va fi afișat pe Discord + Arată timpul + Vibrant + Sapphire + Outrun + Pop + Springfield + Navigare + Activează Discord Rich Presence + 🌙 Idle + Intrările în categoriile incluse nu vor fi afișate în Discord + Conturi Discord + RPC Incognito + Discord + Acest serviciu Discord funcționează prin conectarea simbolurilor. Folosiți pe propriul risc. Token-ul dvs. este stocat local și %s nu îl partajează nicăieri altundeva + Arată progresul + Stare Discord + Arată butonul Discord + Afișează un buton pentru a se alătura serverului Discord + Se actualizează + 3:2 + WebDAV + Nume de utilizator + Parolă + Instalare manuală + %1$s %2$s + Folosind: + Deschide Folder + Actualizați diff --git a/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml b/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml index 7596a3375..debbcf93c 100644 --- a/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml @@ -118,4 +118,56 @@ Ta bort ursprungliga poster från biblioteket? Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino Manuell installation + Rullar igenom + Ringer runt med + Visa knappar för att ladda ner appen eller gå med i Discord-servern + Springfield + Discord, mer att komma.. + Denna Discord-tjänst fungerar genom tokenloggning. Använd på egen risk. Din token lagras lokalt och Anikku delar den inte någon annanstans + Särskilda tjänster + Poster i inkluderade kategorier kommer inte att visas i Discord + Anpassat meddelande + Anpassat meddelande som kommer att visas på Discord + Visa framsteg + Aktivera Discord Rich Presence + ⛔ Stör ej + Visa Discord-knapp + Visar en knapp för att ansluta till Discord-servern + Discord Rich Presence + Visa titeln på avsnittet du tittar på + Fidelity + Visa omslaget justerat till toppen tillsammans med anime info + Outrun + Anslutningar + Discord + Discord inkognitoläge + Åsidosätt när inkognitoläge är aktiverat + Surfning + 🌙 Idle + 🟢 Online + Discords anpassning + Visa knappar + Visa nedladdningsknapp + Visar en knapp för att ladda ner appen + Mocha + Pop + Aktivt konto + Sapphire + Discords status + RPC Incognito + Discord-konton + Inga konton hittades + Kunde inte logga in + Visa tid + Öppna mapp + 3: 2 + WebDAV + Användarnamn + Lösenord + Nyp för att zooma + 16:9 + %1$s %2$s + Använder: + Uppdaterar + Uppdatera diff --git a/i18n-kmk/src/commonMain/moko-resources/ta/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ta/strings.xml index 310f93f9a..3a9549b8c 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ta/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ta/strings.xml @@ -1,4 +1,77 @@ உங்கள் ஊட்டத்தில் பல ஆதாரங்கள், 20 க்கு மேல் சேர்க்க முடியாது + மேலும் நீட்டிப்புகள் + கையேடு + 1 மணி நேரம் + 2 மணி நேரம் + 6 மணி நேரம் + 12 மணி நேரம் + 1 நாள் + கடவுச்சொல் + இப்போது முயற்சிக்கவும் + புதுப்பிப்பு முடிந்தது + மேலே உருட்டவும் + செயலில் கணக்கு + பயன்படுத்துகிறது: + %s படிக்கவும் + தனிப்பயன் செய்தி + ஆதாரங்களைத் தேடுங்கள் + மேலே செல்லுங்கள் + கீழே செல்லுங்கள் + மறை + திறந்த கோப்புறை + புதுப்பிப்பு + நேரத்தைக் காட்டு + Webdav + பயனாளர் பெயர் + அனைத்தையும் அனுமதிக்கவும் + அனைத்தையும் தவிர்க்கவும் + கைமுறை நிறுவல் + கைமுறையாக பதிவிறக்கம் + பரிந்துரைகள் + கீழே உருட்டவும் + நடப்பு: %1$s + முரண்பாடு பணக்கார இருப்பு + நிலையிக்கம் + கணக்குகள் எதுவும் இல்லை + %1$s %2$s + உலாவுதல் + பொத்தான்களைக் காட்டு + கோப்புறையைத் திறக்கத் தவறிவிட்டது. + பெரிதாக்க கிள்ளுதல் + 16:9 + 3:2 + மங்கா செய்தி + டோனல் ச்பாட் + நடுநிலை + துடிப்பான + வெளிப்படுத்தும் + ரெயின்போ + பழ சாலட் + ஒரே வண்ணமுடைய + மெய்நிலை + உள்ளடக்கம் + கிளவுட்ஃப்ளேர் + பருத்தி மிட்டாய் + டூம் + அணி + மோச்சா + சபையர் + Custom Theme + கருக்கல் + விஞ்சி + புற்றுப்பழம் + பாப் + நிறமாலை + எல்.சி.டி. + புதுப்பித்தல் + நூலகத்தில் சேர்ப்பதில் டிராக் மெனுவைத் திறக்கவும் + முரண்பாடு பணக்கார இருப்பை இயக்கவும் + முன்னேற்றத்தைக் காட்டு + இணைப்புகள் + முரண்பாடு + தொந்தரவு செய்ய வேண்டாம் + ஆன்லைனில் + திற diff --git a/i18n-kmk/src/commonMain/moko-resources/th/strings.xml b/i18n-kmk/src/commonMain/moko-resources/th/strings.xml index 55344e519..37638f8ff 100644 --- a/i18n-kmk/src/commonMain/moko-resources/th/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/th/strings.xml @@ -1,3 +1,60 @@ - \ No newline at end of file + ด้วยตนเอง + 1 ชั่วโมง + 2 ชั่วโมง + 6 ชั่วโมง + WebDAV + อนุญาตทั้งหมด + อัปเดตเสร็จแล้ว + เลื่อนกลับสู่ด้านบน + พร้อมใช้งาน + %1$s %2$s + แสดงปุ่มดาวน์โหลด + ราสเบอร์รี่ + ขาวดำ + 12 ชั่วโมง + แสดงออก + เป็นกลาง + เลื่อนขึ้น + อัปเดต + คำแนะนำ + อายุขัย + คำถามที่พบบ่อยและคู่มือ + ซ่อน + แสดงหมวดหมู่ที่ซ่อนอยู่ + เปิดแฟ้ม + 4:3 + ที่กําหนดเอง ธีม + แคชชวล + อัปเดตแอปอัตโนมัติ + ข้ามทั้งหมด + Discord + ชื่อผู้ใช้ + รหัสผ่าน + เปิด + หน้ารีลีส + ไม่สามารถติดตั้งอัปเดต + การเชื่อมต่อ + กำลังเปิด + แสดงความคืบหน้า + แสดงเวลา + หดเพื่อซูม + 16:9 + 3:2 + ข้อมูลมังงะ + จุดวรรณยุกต์ + ความสดใส + สีรุ้ง + สลัดผลไม้ + ความจงรักภักดี + เนื้อหา + ปาตาโกเนีย + สปริงฟิลด์ + 1 วัน + ห้ามรบกวน + ออนไลน์ + ดิสครอด Rich Presence + เลื่อนลง + กำลังอัปเดต + diff --git a/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml b/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml index 47e52c143..044489c5e 100644 --- a/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml @@ -105,4 +105,50 @@ Kütüphane güncelleme hataları (%d) Kütüphane güncelleme hataları Manuel kurulum + İlerlemeyi takip servisleriyle otomatik eşitle + Sync progress from Trackers up to chapter %d + Update completed + You have no library update errors. + Panorama cover + Copy first page to clipboard + Copy combined page to clipboard + Panorama Cover + Scroll to previous + Could not install update + Scroll to bottom + Copy second page to clipboard + Show cover in landscape mode if it\'s a wide image + Panorama Cover Flow + Show panorama cover in Global Search & Suggestions + Mocha + Güncelleniyor + To be updated + Panorama comfortable grid + Pop + Kitaplığa eklerken izleyici menüsünü aç + Discord Detaylı Etkinlik + Saati Göster + Güncelleme + Yakınlaştırmak için sıkıştır + WebDAV + Kullanıcı adı + Parola + Discord + Discord etkinliğini etkinleştir + Eylemsiz + Çevrimiçi + Hesap yok + %1$s %2$s + Şunu kullanarak: + Gözatma + %s üzerinde oku + İlerlemeyi Göster + Düğmeleri Göster + 16:9 + 4:3 + 3:2 + Bağlantılar + Klasörü aç + Rahatsız Etme + İndirme butonunu göster diff --git a/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml b/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml index fe28b796d..18458cd20 100644 --- a/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml @@ -137,4 +137,48 @@ Підтримати мене Аутран Springfield + Цей сервіс Discord працює через систему з використанням жетонів. Використовуйте на ваш власний ризик. Ваш токен зберігається на локальному і Anikku не поширює його ніде інше + Rich Presence Discord + Увімкнути Rich Presence в Discord + Статус Discord + RPC Incognito + Категорії у базі даних не будуть відображатися в Discord + Облікові записи Discord + Активний обліковий запис + Рахунків не знайдено + Прокрутка + Обмін інформацією з + Показувати кнопку завантаження + Відображає кнопку для приєднання до серверу Discord + Перезаписувати, коли ввімкнено режим інкогніто + 🌙 Очікування + Не вдалося увійти + Користувацьке повідомлення + Discord, ще більше... + Спеціальні послуги + Показати назву епізоду, що ви дивитеся + ⛔ Не турбувати + Перегляд + З\'єднання + Налаштування Discord + Користувацькі повідомлення, які будуть відображатися в Discord + Відображати час + Показати кнопки + Показувати кнопки для завантаження програми або приєднання до сервера Discord + Відображає кнопку для завантаження програми + Показувати кнопку Discord + 🟢 Online + Режим інкогніто Discord + Показувати поступ + Discord + 16:9 + Використовує: + 4:3 + %1$s %2$s + WebDAV + Ім\'я користувача + Пароль + Прищемити, щоб масштабувати + 3:2 + Читати на %s diff --git a/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml b/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml index cc02a045a..83b4403ec 100644 --- a/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml @@ -137,4 +137,25 @@ Dữ liệu đã tải xuống Chương & Lịch sử từ cơ sở dữ liệu Tài trợ cho tôi + Kéo để phóng to + Kết nối + Discord + Tích hợp trạng thái Discord + Kích hoạt tính năng Discord Rich Presence + Chế độ không làm phiền + Đang chờ + Trực tuyến + Không có tài khoản nào + %1$s %2$s + Dùng: + Đang duyệt + Đọc trên %s + Hiển thị thời gian + Hiện nút tải xuống + 16:9 + 4:3 + 3:2 + WebDAV + Tên người dùng + Mật khẩu diff --git a/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml b/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml index 5758f29a2..aeec5a62e 100644 --- a/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml @@ -129,4 +129,52 @@ 赞助 缩减点按区域 更新 + 用户名 + 密码 + 添加到仓库时打开进度记录菜单 + 捏合放大 + 16:9 + 4:3 + 3:2 + WebDAV + %1$s %2$s + 在 %s 查看 + 正在使用: + 数据库中的剧集与观看记录 + Discord 状态 + 显示加入Discord服务器的按钮 + 连接 + 🌙 空闲的 + 显示按钮下载应用程序或加入Discord服务器 + 显示时间 + 更新 + RPC Incognito + 特别服务 + Discord + 这个Discord服务通过令牌记录来运行。请自行承担风险。您的令牌是本地存储的,%s 不在任何其他地方共享。 + Discord 丰富存在 + 显示您观看的剧集的标题 + 启用 Discord Rich Presence + Discord 隐身模式 + 包含类别中的条目将不会在Discord中显示 + 登录失败 + 四周消息: + 在 Discord 上显示的自定义消息 + 显示进度 + 显示 Discord 按钮 + 与动画信息一起显示顶部对齐的封面 + Discord,更多即将到来。 + 当隐身模式启用时覆盖 + 未找到账户 + Discord 自定义 + 显示按钮 + 显示下载按钮 + ⛔ 勿扰模式 + 🟢 Online + 显示下载应用程序的按钮 + 自定义消息 + 浏览中 + 滚动通过 + Discord 账户 + 活动账户 From dc3d034109883baf7ab40b2b098147c7cda9462f Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Wed, 13 May 2026 12:55:15 +0700 Subject: [PATCH 20/73] Remove HBrowse and Tsumino sources from delegated sources (#1622) --- README.md | 1 - app/src/main/AndroidManifest.xml | 31 ---- app/src/main/baseline-prof.txt | 2 - .../kanade/presentation/manga/MangaScreen.kt | 10 -- .../tachiyomi/source/AndroidSourceManager.kt | 16 -- .../source/online/english/HBrowse.kt | 95 ----------- .../source/online/english/Tsumino.kt | 151 ------------------ app/src/main/java/exh/EXHMigrations.kt | 2 +- app/src/main/java/exh/source/SourceHelper.kt | 4 - .../adapters/HBrowseDescriptionAdapter.kt | 66 -------- .../adapters/TsuminoDescriptionAdapter.kt | 119 -------------- .../migrations/DelegateHBrowseMigration.kt | 29 ---- .../core/migration/migrations/Migrations.kt | 1 - .../commonMain/moko-resources/ar/strings.xml | 2 +- .../commonMain/moko-resources/as/strings.xml | 2 +- .../moko-resources/base/strings.xml | 2 +- .../commonMain/moko-resources/cs/strings.xml | 2 +- .../commonMain/moko-resources/da/strings.xml | 2 +- .../commonMain/moko-resources/de/strings.xml | 2 +- .../commonMain/moko-resources/el/strings.xml | 2 +- .../commonMain/moko-resources/es/strings.xml | 2 +- .../commonMain/moko-resources/fi/strings.xml | 2 +- .../commonMain/moko-resources/fil/strings.xml | 2 +- .../commonMain/moko-resources/fr/strings.xml | 2 +- .../commonMain/moko-resources/in/strings.xml | 2 +- .../commonMain/moko-resources/it/strings.xml | 2 +- .../commonMain/moko-resources/ja/strings.xml | 2 +- .../commonMain/moko-resources/nl/strings.xml | 2 +- .../commonMain/moko-resources/pl/strings.xml | 2 +- .../moko-resources/pt-rBR/strings.xml | 2 +- .../commonMain/moko-resources/pt/strings.xml | 2 +- .../commonMain/moko-resources/ro/strings.xml | 2 +- .../commonMain/moko-resources/ru/strings.xml | 2 +- .../commonMain/moko-resources/sv/strings.xml | 2 +- .../commonMain/moko-resources/tr/strings.xml | 2 +- .../commonMain/moko-resources/uk/strings.xml | 2 +- .../commonMain/moko-resources/vi/strings.xml | 2 +- .../moko-resources/zh-rCN/strings.xml | 2 +- .../moko-resources/zh-rTW/strings.xml | 2 +- .../metadata/HBrowseSearchMetadata.kt | 76 --------- .../metadata/TsuminoSearchMetadata.kt | 114 ------------- 41 files changed, 27 insertions(+), 742 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/english/HBrowse.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt delete mode 100644 app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt delete mode 100644 app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt delete mode 100644 app/src/main/java/mihon/core/migration/migrations/DelegateHBrowseMigration.kt delete mode 100644 source-api/src/commonMain/kotlin/exh/metadata/metadata/HBrowseSearchMetadata.kt delete mode 100644 source-api/src/commonMain/kotlin/exh/metadata/metadata/TsuminoSearchMetadata.kt diff --git a/README.md b/README.md index a22a2841f..2a72d0a27 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,6 @@ Custom sources: Additional features for some extensions, features include custom description, opening in app, batch add to library, and a bunch of other things based on the source: * 8Muses (EroMuse) -* HBrowse * Mangadex * NHentai * Puruin diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9adfec8ee..bfe5a748a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -333,22 +333,6 @@ - - - - - - - - - - - - - - - - @@ -363,21 +347,6 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/baseline-prof.txt b/app/src/main/baseline-prof.txt index d6333726b..af3c28df3 100644 --- a/app/src/main/baseline-prof.txt +++ b/app/src/main/baseline-prof.txt @@ -16701,9 +16701,7 @@ HSPLeu/kanade/tachiyomi/source/online/all/MergedSource$special$$inlined$injectLa PLeu/kanade/tachiyomi/source/online/all/MergedSource$special$$inlined$injectLazy$9;->()V Leu/kanade/tachiyomi/source/online/all/NHentai; Leu/kanade/tachiyomi/source/online/english/EightMuses; -Leu/kanade/tachiyomi/source/online/english/HBrowse; Leu/kanade/tachiyomi/source/online/english/Pururin; -Leu/kanade/tachiyomi/source/online/english/Tsumino; Leu/kanade/tachiyomi/ui/base/activity/BaseActivity; HSPLeu/kanade/tachiyomi/ui/base/activity/BaseActivity;->()V PLeu/kanade/tachiyomi/ui/base/activity/BaseActivity;->()V diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index a0653977f..f8f5fc463 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -94,9 +94,7 @@ import eu.kanade.tachiyomi.source.online.all.Lanraragi import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.english.EightMuses -import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.Pururin -import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.ui.manga.ChapterList import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.ui.manga.MergedMangaData @@ -108,12 +106,10 @@ import exh.source.getMainSource import exh.source.isEhBasedManga import exh.ui.metadata.adapters.EHentaiDescription import exh.ui.metadata.adapters.EightMusesDescription -import exh.ui.metadata.adapters.HBrowseDescription import exh.ui.metadata.adapters.LanraragiDescription import exh.ui.metadata.adapters.MangaDexDescription import exh.ui.metadata.adapters.NHentaiDescription import exh.ui.metadata.adapters.PururinDescription -import exh.ui.metadata.adapters.TsuminoDescription import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.service.missingChaptersCount import tachiyomi.domain.library.service.LibraryPreferences @@ -1438,15 +1434,9 @@ fun metadataDescription(source: Source): MetadataDescriptionComposable? { is EightMuses -> { state, openMetadataViewer, _ -> EightMusesDescription(state, openMetadataViewer) } - is HBrowse -> { state, openMetadataViewer, _ -> - HBrowseDescription(state, openMetadataViewer) - } is Pururin -> { state, openMetadataViewer, _ -> PururinDescription(state, openMetadataViewer) } - is Tsumino -> { state, openMetadataViewer, _ -> - TsuminoDescription(state, openMetadataViewer) - } is Lanraragi -> { state, openMetadataViewer, _ -> LanraragiDescription(state, openMetadataViewer) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt index e5280bc02..52ea950c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -13,9 +13,7 @@ import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.english.EightMuses -import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.Pururin -import eu.kanade.tachiyomi.source.online.english.Tsumino import exh.log.xLogD import exh.source.BlacklistedSources import exh.source.DelegatedHttpSource @@ -24,10 +22,8 @@ import exh.source.EIGHTMUSES_SOURCE_ID import exh.source.EXHENTAI_EXT_SOURCES import exh.source.EnhancedHttpSource import exh.source.ExhPreferences -import exh.source.HBROWSE_SOURCE_ID import exh.source.MERGED_SOURCE_ID import exh.source.PURURIN_SOURCE_ID -import exh.source.TSUMINO_SOURCE_ID import exh.source.handleSourceLibrary import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -282,12 +278,6 @@ class AndroidSourceManager( "eu.kanade.tachiyomi.extension.en.pururin.Pururin", Pururin::class, ), - DelegatedSource( - "Tsumino", - TSUMINO_SOURCE_ID, - "eu.kanade.tachiyomi.extension.en.tsumino.Tsumino", - Tsumino::class, - ), DelegatedSource( "MangaDex", fillInSourceId, @@ -295,12 +285,6 @@ class AndroidSourceManager( MangaDex::class, true, ), - DelegatedSource( - "HBrowse", - HBROWSE_SOURCE_ID, - "eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse", - HBrowse::class, - ), DelegatedSource( "8Muses", EIGHTMUSES_SOURCE_ID, diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HBrowse.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HBrowse.kt deleted file mode 100644 index 3712395ee..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HBrowse.kt +++ /dev/null @@ -1,95 +0,0 @@ -package eu.kanade.tachiyomi.source.online.english - -import android.content.Context -import android.net.Uri -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.SManga -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.HBrowseSearchMetadata -import exh.metadata.metadata.base.RaisedTag -import exh.source.DelegatedHttpSource -import exh.util.urlImportFetchSearchManga -import exh.util.urlImportFetchSearchMangaSuspend -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element - -class HBrowse(delegate: HttpSource, val context: Context) : - DelegatedHttpSource(delegate), - MetadataSource, - UrlImportableSource, - NamespaceSource { - override val metaClass = HBrowseSearchMetadata::class - override fun newMetaInstance() = HBrowseSearchMetadata() - override val lang = "en" - - // Support direct URL importing - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) - override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = - urlImportFetchSearchManga(context, query) { - @Suppress("DEPRECATION") - super.fetchSearchManga(page, query, filters) - } - - override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { - return urlImportFetchSearchMangaSuspend(context, query) { - super.getSearchManga(page, query, filters) - } - } - - override suspend fun getMangaDetails(manga: SManga): SManga { - val response = client.newCall(mangaDetailsRequest(manga)).awaitSuccess() - return parseToManga(manga, response.asJsoup()) - } - - override suspend fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) { - val tables = parseIntoTables(input) - with(metadata) { - hbUrl = input.location().removePrefix("$baseUrl/thumbnails") - - hbId = hbUrl!!.removePrefix("/").substringBefore("/").toLong() - - tags.clear() - ((tables[""] ?: error("")) + (tables["categories"] ?: error(""))).forEach { (k, v) -> - when (val lowercaseNs = k.lowercase()) { - "title" -> title = v.text() - "length" -> length = v.text().substringBefore(" ").toInt() - else -> { - v.getElementsByTag("a").forEach { - tags += RaisedTag( - lowercaseNs, - it.text(), - HBrowseSearchMetadata.TAG_TYPE_DEFAULT, - ) - } - } - } - } - } - } - - private fun parseIntoTables(doc: Document): Map> { - return doc.select("#main > .listTable").associate { ele -> - val tableName = ele.previousElementSibling()?.text()?.lowercase().orEmpty() - tableName to ele.select("tr") - .filter { element -> element.childrenSize() > 1 } - .associate { - it.child(0).text() to it.child(1) - } - } - } - - override val matchingHosts = listOf( - "www.hbrowse.com", - "hbrowse.com", - ) - - override suspend fun mapUrlToMangaUrl(uri: Uri): String? { - return uri.pathSegments.firstOrNull()?.let { "/$it/c00001/" } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt deleted file mode 100644 index b39aead96..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt +++ /dev/null @@ -1,151 +0,0 @@ -package eu.kanade.tachiyomi.source.online.english - -import android.content.Context -import android.net.Uri -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.SManga -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.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL -import exh.metadata.metadata.TsuminoSearchMetadata -import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT -import exh.metadata.metadata.base.RaisedTag -import exh.source.DelegatedHttpSource -import exh.util.dropBlank -import exh.util.trimAll -import exh.util.urlImportFetchSearchManga -import exh.util.urlImportFetchSearchMangaSuspend -import org.jsoup.nodes.Document -import rx.Observable -import java.text.SimpleDateFormat -import java.util.Locale - -class Tsumino(delegate: HttpSource, val context: Context) : - DelegatedHttpSource(delegate), - MetadataSource, - UrlImportableSource, - NamespaceSource { - override val metaClass = TsuminoSearchMetadata::class - override fun newMetaInstance() = TsuminoSearchMetadata() - override val lang = "en" - - // Support direct URL importing - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = - urlImportFetchSearchManga(context, query) { - @Suppress("DEPRECATION") - super.fetchSearchManga(page, query, filters) - } - - override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { - return urlImportFetchSearchMangaSuspend(context, query) { - super.getSearchManga(page, query, filters) - } - } - - override suspend fun mapUrlToMangaUrl(uri: Uri): String? { - val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.lowercase(Locale.ROOT) ?: return null - if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") { - return null - } - return "https://tsumino.com/Book/Info/${uri.lastPathSegment}" - } - - override suspend fun getMangaDetails(manga: SManga): SManga { - val response = client.newCall(mangaDetailsRequest(manga)).awaitSuccess() - return parseToManga(manga, response.asJsoup()) - } - - override suspend fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) { - with(metadata) { - tmId = TsuminoSearchMetadata.tmIdFromUrl(input.location())!!.toInt() - tags.clear() - - input.select("meta[property=og:title]").firstOrNull()?.attr("content")?.let { - title = it.trim() - } - - input.getElementById("Artist")?.children()?.first()?.attr("data-define")?.trim()?.let { artistString -> - artistString.split("|").trimAll().dropBlank().forEach { - tags.add(RaisedTag("artist", it, TAG_TYPE_DEFAULT)) - } - tags.add(RaisedTag("artist", artistString, TAG_TYPE_VIRTUAL)) - artist = artistString - } - - input.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let { - uploader = it - } - - input.getElementById("Uploaded")?.text()?.let { - uploadDate = TM_DATE_FORMAT.parse(it.trim())!!.time - } - - input.getElementById("Pages")?.text()?.let { - length = it.trim().toIntOrNull() - } - - input.getElementById("Rating")?.text()?.let { - ratingString = it.trim() - val ratingString = ratingString - if (!ratingString.isNullOrBlank()) { - averageRating = RATING_FLOAT_REGEX.find(ratingString)?.groups?.get(1)?.value?.toFloatOrNull() - userRatings = RATING_USERS_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull() - favorites = RATING_FAVORITES_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull() - } - } - - input.getElementById("Category")?.children()?.first()?.attr("data-define")?.let { - category = it.trim() - tags.add(RaisedTag("genre", it, TAG_TYPE_VIRTUAL)) - } - - input.getElementById("Collection")?.children()?.first()?.attr("data-define")?.let { - collection = it.trim() - tags.add(RaisedTag("collection", it, TAG_TYPE_DEFAULT)) - } - - input.getElementById("Group")?.children()?.first()?.attr("data-define")?.let { - group = it.trim() - tags.add(RaisedTag("group", it, TAG_TYPE_DEFAULT)) - } - - parody = input.getElementById("Parody")?.children()?.map { - val entry = it.attr("data-define").trim() - tags.add(RaisedTag("parody", entry, TAG_TYPE_DEFAULT)) - entry - }.orEmpty() - - character = input.getElementById("Character")?.children()?.map { - val entry = it.attr("data-define").trim() - tags.add(RaisedTag("character", entry, TAG_TYPE_DEFAULT)) - entry - }.orEmpty() - - input.getElementById("Tag")?.children()?.let { tagElements -> - tags.addAll( - tagElements.map { - RaisedTag("tags", it.attr("data-define").trim(), TAG_TYPE_DEFAULT) - }, - ) - } - } - } - - override val matchingHosts = listOf( - "www.tsumino.com", - "tsumino.com", - ) - - companion object { - val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US) - val RATING_FLOAT_REGEX = "([0-9].*) \\(".toRegex() - val RATING_USERS_REGEX = "\\(([0-9].*) users".toRegex() - val RATING_FAVORITES_REGEX = "/ ([0-9].*) favs".toRegex() - } -} diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index ffe00517a..2a662129c 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -177,7 +177,7 @@ object EXHMigrations { out += "#" + uri.fragment } out - } catch (e: URISyntaxException) { + } catch (_: URISyntaxException) { orig } } diff --git a/app/src/main/java/exh/source/SourceHelper.kt b/app/src/main/java/exh/source/SourceHelper.kt index ca5c6f5b6..ea279e8a6 100644 --- a/app/src/main/java/exh/source/SourceHelper.kt +++ b/app/src/main/java/exh/source/SourceHelper.kt @@ -5,9 +5,7 @@ import eu.kanade.tachiyomi.source.online.all.Lanraragi import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.english.EightMuses -import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.Pururin -import eu.kanade.tachiyomi.source.online.english.Tsumino /** * Source helpers @@ -16,8 +14,6 @@ import eu.kanade.tachiyomi.source.online.english.Tsumino private val DELEGATED_METADATA_SOURCES by lazy { listOf( Pururin::class, - Tsumino::class, - HBrowse::class, EightMuses::class, NHentai::class, Lanraragi::class, diff --git a/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt deleted file mode 100644 index 1726f5c67..000000000 --- a/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt +++ /dev/null @@ -1,66 +0,0 @@ -package exh.ui.metadata.adapters - -import android.view.LayoutInflater -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.viewinterop.AndroidView -import eu.kanade.presentation.theme.colorscheme.AndroidViewColorScheme -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.DescriptionAdapterHbBinding -import eu.kanade.tachiyomi.ui.manga.MangaScreenModel.State -import eu.kanade.tachiyomi.util.system.copyToClipboard -import eu.kanade.tachiyomi.util.system.dpToPx -import exh.metadata.metadata.HBrowseSearchMetadata -import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable -import tachiyomi.core.common.i18n.pluralStringResource -import tachiyomi.core.common.i18n.stringResource -import tachiyomi.i18n.sy.SYMR - -@Composable -fun HBrowseDescription(state: State.Success, openMetadataViewer: () -> Unit) { - val context = LocalContext.current - // KMK --> - val colorScheme = AndroidViewColorScheme(MaterialTheme.colorScheme) - val iconColor = colorScheme.iconColor - val textColor = LocalContentColor.current.toArgb() - // KMK <-- - AndroidView( - modifier = Modifier.fillMaxWidth(), - factory = { factoryContext -> - DescriptionAdapterHbBinding.inflate(LayoutInflater.from(factoryContext)).root - }, - update = { - val meta = state.meta - if (meta == null || meta !is HBrowseSearchMetadata) return@AndroidView - val binding = DescriptionAdapterHbBinding.bind(it) - - binding.pages.text = - context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0) - // KMK --> - binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor, 4.dpToPx) - binding.pages.setTextColor(textColor) - - binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor) - binding.moreInfo.text = context.stringResource(SYMR.strings.more_info) - binding.moreInfo.setTextColor(iconColor) - // KMK <-- - - binding.pages.setOnLongClickListener { - context.copyToClipboard( - binding.pages.text.toString(), - binding.pages.text.toString(), - ) - true - } - - binding.moreInfo.setOnClickListener { - openMetadataViewer() - } - }, - ) -} diff --git a/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt deleted file mode 100644 index ca2eed78d..000000000 --- a/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt +++ /dev/null @@ -1,119 +0,0 @@ -package exh.ui.metadata.adapters - -import android.annotation.SuppressLint -import android.content.res.ColorStateList -import android.view.LayoutInflater -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.viewinterop.AndroidView -import eu.kanade.presentation.theme.colorscheme.AndroidViewColorScheme -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.DescriptionAdapterTsBinding -import eu.kanade.tachiyomi.ui.manga.MangaScreenModel.State -import eu.kanade.tachiyomi.util.system.copyToClipboard -import eu.kanade.tachiyomi.util.system.dpToPx -import exh.metadata.metadata.TsuminoSearchMetadata -import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable -import exh.util.SourceTagsUtil.genreTextColor -import tachiyomi.core.common.i18n.pluralStringResource -import tachiyomi.core.common.i18n.stringResource -import tachiyomi.i18n.MR -import tachiyomi.i18n.sy.SYMR -import java.util.Date -import kotlin.math.round - -@Composable -fun TsuminoDescription(state: State.Success, openMetadataViewer: () -> Unit) { - val context = LocalContext.current - // KMK --> - val colorScheme = AndroidViewColorScheme(MaterialTheme.colorScheme) - val iconColor = colorScheme.iconColor - val ratingBarColor = colorScheme.ratingBarColor - val ratingBarSecondaryColor = colorScheme.ratingBarSecondaryColor - val textColor = LocalContentColor.current.toArgb() - // KMK <-- - AndroidView( - modifier = Modifier.fillMaxWidth(), - factory = { factoryContext -> - DescriptionAdapterTsBinding.inflate(LayoutInflater.from(factoryContext)).root - }, - update = { - val meta = state.meta - if (meta == null || meta !is TsuminoSearchMetadata) return@AndroidView - val binding = DescriptionAdapterTsBinding.bind(it) - - binding.genre.text = meta.category?.let { genre -> MetadataUIUtil.getGenreAndColour(context, genre) } - ?.let { (genre, name) -> - binding.genre.setBackgroundColor(genre.color) - // KMK --> - binding.genre.setTextColor(genreTextColor(genre)) - // KMK <-- - name - } ?: meta.category ?: context.stringResource(MR.strings.unknown) - - binding.favorites.text = (meta.favorites ?: 0).toString() - // KMK --> - binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp, iconColor) - binding.favorites.setTextColor(textColor) - // KMK <-- - - binding.whenPosted.text = TsuminoSearchMetadata.TSUMINO_DATE_FORMAT.format(Date(meta.uploadDate ?: 0)) - // KMK --> - binding.whenPosted.setTextColor(textColor) - // KMK <-- - - binding.uploader.text = meta.uploader ?: context.stringResource(MR.strings.unknown) - // KMK --> - binding.uploader.setTextColor(textColor) - // KMK <-- - - binding.pages.text = - context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0) - // KMK --> - binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor, 4.dpToPx) - binding.pages.setTextColor(textColor) - // KMK <-- - - binding.ratingBar.rating = meta.averageRating ?: 0F - @SuppressLint("SetTextI18n") - binding.rating.text = - (round((meta.averageRating ?: 0F) * 100.0) / 100.0).toString() + " - " + - MetadataUIUtil.getRatingString(context, meta.averageRating?.times(2)) - // KMK --> - binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(ratingBarColor) - binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor) - binding.rating.setTextColor(textColor) - - binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor) - binding.moreInfo.text = context.stringResource(SYMR.strings.more_info) - binding.moreInfo.setTextColor(iconColor) - // KMK <-- - - listOf( - binding.favorites, - binding.genre, - binding.pages, - binding.rating, - binding.uploader, - binding.whenPosted, - ).forEach { textView -> - textView.setOnLongClickListener { - context.copyToClipboard( - textView.text.toString(), - textView.text.toString(), - ) - true - } - } - - binding.moreInfo.setOnClickListener { - openMetadataViewer() - } - }, - ) -} diff --git a/app/src/main/java/mihon/core/migration/migrations/DelegateHBrowseMigration.kt b/app/src/main/java/mihon/core/migration/migrations/DelegateHBrowseMigration.kt deleted file mode 100644 index 2fd77047b..000000000 --- a/app/src/main/java/mihon/core/migration/migrations/DelegateHBrowseMigration.kt +++ /dev/null @@ -1,29 +0,0 @@ -package mihon.core.migration.migrations - -import eu.kanade.domain.manga.interactor.UpdateManga -import exh.source.HBROWSE_OLD_ID -import exh.source.HBROWSE_SOURCE_ID -import mihon.core.migration.MigrateUtils -import mihon.core.migration.Migration -import mihon.core.migration.MigrationContext -import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.domain.manga.interactor.GetMangaBySource -import tachiyomi.domain.manga.model.MangaUpdate - -class DelegateHBrowseMigration : Migration { - override val version: Float = 4f - - override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext { - val getMangaBySource = migrationContext.get() ?: return@withIOContext false - val updateManga = migrationContext.get() ?: return@withIOContext false - MigrateUtils.updateSourceId(migrationContext, HBROWSE_SOURCE_ID, HBROWSE_OLD_ID) - - // Migrate BHrowse URLs - val hBrowseManga = getMangaBySource.await(HBROWSE_SOURCE_ID) - val mangaUpdates = hBrowseManga.map { - MangaUpdate(it.id, url = it.url + "/c00001/") - } - updateManga.awaitAll(mangaUpdates) - return@withIOContext true - } -} diff --git a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt index 3726896cc..c2829db19 100644 --- a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt +++ b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt @@ -8,7 +8,6 @@ val migrations: List SetupLibraryUpdateMigration(), SetupEHentaiUpdateMigration(), SetupSyncDataMigration(), - // DelegateHBrowseMigration(), // DelegateNHentaiMigration(), // MergedMangaRewriteMigration(), // LogoutFromMALMigration(), diff --git a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml index 140ddc34d..6667dd4c8 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml @@ -121,7 +121,7 @@ نسخ الصفحة المدمجة إلى الحافظة عدم التحميل التلقائي للتحديث إزالة العناصر الأصلية من المكتبة؟ - الدعم: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + الدعم: MangaDex, E-H, ExH, nH, 8Muses فتح مجلد تحديث مانجا غير مكتملة diff --git a/i18n-kmk/src/commonMain/moko-resources/as/strings.xml b/i18n-kmk/src/commonMain/moko-resources/as/strings.xml index 5e73506cb..143ee9d61 100644 --- a/i18n-kmk/src/commonMain/moko-resources/as/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/as/strings.xml @@ -136,7 +136,7 @@ বৰ্তমান: %1$s আপডেট কৰিবলগীয়া মোক স্পনছৰ কৰক - সমৰ্থন: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + সমৰ্থন: MangaDex, E-H, ExH, nH, 8Muses ডিস্ক’ৰ্ড ৰিচ প্ৰেজেন্স সক্ৰিয় একাউণ্ট সংযোগ diff --git a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml index 4bfb09902..deea67a59 100644 --- a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml @@ -192,7 +192,7 @@ Failed to schedule automatic backup: %s To be updated Sponsor Me - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Special Services Connections diff --git a/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml b/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml index 769f155a4..5025a613f 100644 --- a/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml @@ -115,7 +115,7 @@ Kopírovat kombinovanou stránku do schránky Automaticky nestahovat aktualizaci Odstranit původní položky z knihovny? - Podpora: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Podpora: MangaDex, E-H, ExH, nH, 8Muses Bubblegum Discord účty Vlastní zpráva, která bude zobrazena na Discordu diff --git a/i18n-kmk/src/commonMain/moko-resources/da/strings.xml b/i18n-kmk/src/commonMain/moko-resources/da/strings.xml index 68a9b49e7..f3faca7f9 100644 --- a/i18n-kmk/src/commonMain/moko-resources/da/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/da/strings.xml @@ -115,7 +115,7 @@ Kopier kombineret side til udklipsholder Download ikke automatisk opdatering Fjern originale indgange fra biblioteket? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Sapphire Særlige Tjenester Indgange i inkluderede kategorier vil ikke blive vist i Discord diff --git a/i18n-kmk/src/commonMain/moko-resources/de/strings.xml b/i18n-kmk/src/commonMain/moko-resources/de/strings.xml index 455b90577..0deef1353 100644 --- a/i18n-kmk/src/commonMain/moko-resources/de/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/de/strings.xml @@ -116,7 +116,7 @@ Kombinierte Seite in Zwischenablage kopieren Update nicht automatisch herunterladen Originale Einträge aus der Bibliothek entfernen? - Unterstützung: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Unterstützung: MangaDex, E-H, ExH, nH, 8Muses Panorama-Cover Flow Untergang Paletten-Personalisierer diff --git a/i18n-kmk/src/commonMain/moko-resources/el/strings.xml b/i18n-kmk/src/commonMain/moko-resources/el/strings.xml index 3344b16ed..e87e186c2 100644 --- a/i18n-kmk/src/commonMain/moko-resources/el/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/el/strings.xml @@ -116,7 +116,7 @@ Αντιγραφή συνδυασμένης σελίδας στο πρόχειρο Να μην γίνεται αυτόματη λήψη ενημέρωσης Αφαιρέστε τις αρχικές καταχωρήσεις από τη βιβλιοθήκη? - Υποστήριξη: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Υποστήριξη: MangaDex, E-H, ExH, nH, 8Muses Sapphire Ειδικές Υπηρεσίες Discord diff --git a/i18n-kmk/src/commonMain/moko-resources/es/strings.xml b/i18n-kmk/src/commonMain/moko-resources/es/strings.xml index 96c6b3dd2..ba2e37e2a 100644 --- a/i18n-kmk/src/commonMain/moko-resources/es/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/es/strings.xml @@ -116,7 +116,7 @@ Copiar página combinada al portapapeles No descargar actualización automática ¿Eliminar entradas originales de la biblioteca? - Soporte: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Soporte: MangaDex, E-H, ExH, nH, 8Muses Abrir carpeta Actualizar Fallo al abrir la carpeta diff --git a/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml index 34ce68ff0..96fc35ae0 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml @@ -114,7 +114,7 @@ Kopioi yhdistetty sivu leikepöydälle Älä lataa automaattisesti päivitystä Poistetaanko alkuperäiset merkinnät kirjastosta? - Tuki: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Tuki: MangaDex, E-H, ExH, nH, 8Muses Discord Rich Presence Kirjautuminen epäonnistui Sapphire diff --git a/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml index 4a1623ca0..476858f3a 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml @@ -97,7 +97,7 @@ Kopyahin ang pinagsamang pahina sa clipboard Huwag awtomatikong i-download ang update Tanggalin ang orihinal na mga tala galing sa aklatan? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Fruit Salad Makulay Masining diff --git a/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml index ed23d640b..105a2bb82 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml @@ -114,7 +114,7 @@ Copier la page combinée dans le presse-papiers Ne pas télécharger automatiquement la mise à jour Supprimer les entrées originales de la bibliothèque ? - Compatible : MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Compatible : MangaDex, E-H, ExH, nH, 8Muses Tonal Monochrome Doom diff --git a/i18n-kmk/src/commonMain/moko-resources/in/strings.xml b/i18n-kmk/src/commonMain/moko-resources/in/strings.xml index a0e84b533..065de3308 100644 --- a/i18n-kmk/src/commonMain/moko-resources/in/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/in/strings.xml @@ -68,7 +68,7 @@ Release page Don\'t auto download update Hapus entri asli dari perpustakaan? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Sembunyikan Tampilkan kategori tersembunyi Penutup panorama diff --git a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml index 57afd2300..2b621b810 100644 --- a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml @@ -116,7 +116,7 @@ Copia la pagina combinata negli appunti Non scaricare automaticamente l\'aggiornamento Rimuovere le voci originali dalla libreria? - Supporto: MangaDex, E-H, ExH, nH, 8Muse, Tsumino + Supporto: MangaDex, E-H, ExH, nH, 8Muse Patagonia Pop Superare diff --git a/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml index c96bb7181..048c4c69f 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml @@ -116,7 +116,7 @@ 結合ページをクリップボードにコピー 更新プログラムを自動的にダウンロードしない ライブラリから元の作品を削除しますか? - サポート: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + サポート: MangaDex, E-H, ExH, nH, 8Muses フォルダーを開く フォルダーを開けませんでした 16:9 diff --git a/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml b/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml index 09dbd6810..f78ed82af 100644 --- a/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml @@ -115,7 +115,7 @@ Kopieer gecombineerde pagina naar klembord Update niet automatisch downloaden Originele invoergegevens uit bibliotheek verwijderen? - Ondersteuning: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Ondersteuning: MangaDex, E-H, ExH, nH, 8Muses Gebruikersnaam Map openen Updaten diff --git a/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml b/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml index 651dc7570..6afdebea4 100644 --- a/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml @@ -114,7 +114,7 @@ Kopiuj połączoną stronę do schowka Nie pobieraj automatycznie aktualizacji Usunąć oryginalne wpisy z biblioteki? - Wsparcie: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Wsparcie: MangaDex, E-H, ExH, nH, 8Muses Fatum Ręczna instalacja Mokka diff --git a/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml b/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml index 6e495609e..6061a4837 100644 --- a/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml @@ -118,7 +118,7 @@ Copiar combinação de páginas para a área de transferência Não baixar a atualização automaticamente Remover entradas originais da biblioteca? - Suporte: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Suporte: MangaDex, E-H, ExH, nH, 8Muses Atualizar 16:9 20:9 diff --git a/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml b/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml index 21a6c4914..b66038182 100644 --- a/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml @@ -118,7 +118,7 @@ Copiar página combinada para área de transferência Não baixar a atualização automaticamente Remover entradas originais da biblioteca? - Suporte: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Suporte: MangaDex, E-H, ExH, nH, 8Muses Instalação manual Abrir pasta Limpar mangá diff --git a/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml index 3ff59cdfa..1291a4f62 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml @@ -115,7 +115,7 @@ Copiază pagina combinată în clipboard Nu descărca automat Eliminaţi intrările originale din bibliotecă? - Suport: MangaDex, E-H, ExH, nH, 8Muse, Tsumino + Suport: MangaDex, E-H, ExH, nH, 8Muse Servicii speciale Conexiuni Cont activ diff --git a/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml index 86bbcd3c5..e3b8fb9a4 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml @@ -119,7 +119,7 @@ Скопировать объединенную страницу в буфер обмена Не скачивать автоматически Удалить оригинальные тайтлы из библиотеки? - Поддержка: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Поддержка: MangaDex, E-H, ExH, nH, 8Muses Выровнять обложку к верху Показать обложку выровненной к вершине рядом с информацией о манге Дум diff --git a/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml b/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml index debbcf93c..bebcf5ce5 100644 --- a/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml @@ -116,7 +116,7 @@ Kopiera kombinerad sida till urklipp Uppdatera inte automatisk nedladdning Ta bort ursprungliga poster från biblioteket? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Manuell installation Rullar igenom Ringer runt med diff --git a/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml b/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml index 044489c5e..3a2ed7033 100644 --- a/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml @@ -100,7 +100,7 @@ Release page Otomatik indirmeyi devre dışı bırak Remove original entries from library? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Uygulamayı otomatik güncelle Kütüphane güncelleme hataları (%d) Kütüphane güncelleme hataları diff --git a/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml b/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml index 18458cd20..dafaf0bfe 100644 --- a/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml @@ -118,7 +118,7 @@ Скопіювати комбіновану сторінку в буфер обміну Не завантажувати автоматично оновлення Видалити оригінальні записи з бібліотеки? - Підтримка: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Підтримка: MangaDex, E-H, ExH, nH, 8Muses Відкрити папку Очистити мангу Оновити diff --git a/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml b/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml index 83b4403ec..d1b6dcb78 100644 --- a/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml @@ -122,7 +122,7 @@ Sao chép trang đã kết hợp vào clipboard Không tự động tải xuống bản cập nhật Xóa các truyện gốc ra khỏi thư viện? - Hỗ trợ: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Hỗ trợ: MangaDex, E-H, ExH, nH, 8Muses Cài đặt thủ công Mở thư mục Xóa manga diff --git a/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml b/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml index aeec5a62e..777c38ac8 100644 --- a/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml @@ -122,7 +122,7 @@ 复制合并页面到剪贴板 不自动下载更新 从书架删除原始条目? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses 打开文件夹 打开文件夹时出错 手动安装 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 53fbe39e0..928b26e66 100644 --- a/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -122,7 +122,7 @@ 將雙頁複製到剪貼簿 不自動下載更新 要從書櫃移除原始條目嗎? - 支援:MangaDex、E-H、ExH、nH、8Muses、Tsumino + 支援:MangaDex、E-H、ExH、nH、8Muses 手動下載 將封面與頂部對齊 將封面與頂部對齊並顯示漫畫資訊 diff --git a/source-api/src/commonMain/kotlin/exh/metadata/metadata/HBrowseSearchMetadata.kt b/source-api/src/commonMain/kotlin/exh/metadata/metadata/HBrowseSearchMetadata.kt deleted file mode 100644 index 37fcb3886..000000000 --- a/source-api/src/commonMain/kotlin/exh/metadata/metadata/HBrowseSearchMetadata.kt +++ /dev/null @@ -1,76 +0,0 @@ -package exh.metadata.metadata - -import android.content.Context -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.model.copy -import kotlinx.serialization.Serializable -import tachiyomi.core.common.i18n.stringResource -import tachiyomi.i18n.MR -import tachiyomi.i18n.sy.SYMR - -@Serializable -class HBrowseSearchMetadata : RaisedSearchMetadata() { - var hbId: Long? = null - - var hbUrl: String? = null - - var thumbnail: String? = null - - var title: String? by titleDelegate(TITLE_TYPE_MAIN) - - // Length in pages - var length: Int? = null - - override fun createMangaInfo(manga: SManga): SManga { - val key = hbUrl - - val title = title - - // Guess thumbnail URL if manga does not have thumbnail URL - val cover = if (manga.thumbnail_url.isNullOrBlank()) { - guessThumbnailUrl(hbId.toString()) - } else { - null - } - - val artist = tags.ofNamespace(ARTIST_NAMESPACE).joinToString { it.name } - - val genres = tagsToGenreString() - - val description = null - - return manga.copy( - url = key ?: manga.url, - title = title ?: manga.title, - thumbnail_url = cover ?: manga.thumbnail_url, - artist = artist, - genre = genres, - description = description, - ) - } - - override fun getExtraInfoPairs(context: Context): List> { - return with(context) { - listOfNotNull( - getItem(hbId) { stringResource(SYMR.strings.id) }, - getItem(hbUrl) { stringResource(SYMR.strings.url) }, - getItem(thumbnail) { stringResource(SYMR.strings.thumbnail_url) }, - getItem(title) { stringResource(MR.strings.title) }, - getItem(length) { stringResource(SYMR.strings.page_count) }, - ) - } - } - - companion object { - const val BASE_URL = "https://www.hbrowse.com" - - private const val TITLE_TYPE_MAIN = 0 - - const val TAG_TYPE_DEFAULT = 0 - const val ARTIST_NAMESPACE = "artist" - - fun guessThumbnailUrl(hbid: String): String { - return "$BASE_URL/thumbnails/${hbid}_1.jpg#guessed" - } - } -} diff --git a/source-api/src/commonMain/kotlin/exh/metadata/metadata/TsuminoSearchMetadata.kt b/source-api/src/commonMain/kotlin/exh/metadata/metadata/TsuminoSearchMetadata.kt deleted file mode 100644 index dc9a640b0..000000000 --- a/source-api/src/commonMain/kotlin/exh/metadata/metadata/TsuminoSearchMetadata.kt +++ /dev/null @@ -1,114 +0,0 @@ -package exh.metadata.metadata - -import android.content.Context -import androidx.core.net.toUri -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.model.copy -import exh.metadata.MetadataUtil -import exh.util.nullIfEmpty -import kotlinx.serialization.Serializable -import tachiyomi.core.common.i18n.stringResource -import tachiyomi.i18n.MR -import tachiyomi.i18n.sy.SYMR -import java.text.SimpleDateFormat -import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime -import java.util.Locale - -@Serializable -class TsuminoSearchMetadata : RaisedSearchMetadata() { - var tmId: Int? = null - - var title by titleDelegate(TITLE_TYPE_MAIN) - - var artist: String? = null - - var uploadDate: Long? = null - - var length: Int? = null - - var ratingString: String? = null - - var averageRating: Float? = null - - var userRatings: Long? = null - - var favorites: Long? = null - - var category: String? = null - - var collection: String? = null - - var group: String? = null - - var parody: List = emptyList() - - var character: List = emptyList() - - override fun createMangaInfo(manga: SManga): SManga { - val title = title - val cover = tmId?.let { BASE_URL.replace("www", "content") + thumbUrlFromId(it.toString()) } - - val artist = artist - - val status = SManga.UNKNOWN - - // Copy tags -> genres - val genres = tagsToGenreString() - - val description = null - - return manga.copy( - title = title ?: manga.title, - thumbnail_url = cover ?: manga.thumbnail_url, - artist = artist ?: manga.artist, - status = status, - genre = genres, - description = description, - ) - } - - override fun getExtraInfoPairs(context: Context): List> { - return with(context) { - listOfNotNull( - getItem(tmId) { stringResource(SYMR.strings.id) }, - getItem(title) { stringResource(MR.strings.title) }, - getItem(uploader) { stringResource(SYMR.strings.uploader) }, - getItem( - uploadDate, - { - MetadataUtil.EX_DATE_FORMAT - .format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())) - }, - ) { - stringResource(SYMR.strings.date_posted) - }, - getItem(length) { stringResource(SYMR.strings.page_count) }, - getItem(ratingString) { stringResource(SYMR.strings.rating_string) }, - getItem(averageRating) { stringResource(SYMR.strings.average_rating) }, - getItem(userRatings) { stringResource(SYMR.strings.total_ratings) }, - getItem(favorites) { stringResource(SYMR.strings.total_favorites) }, - getItem(category) { stringResource(SYMR.strings.genre) }, - getItem(collection) { stringResource(SYMR.strings.collection) }, - getItem(group) { stringResource(SYMR.strings.group) }, - getItem(parody.nullIfEmpty(), { it.joinToString() }) { stringResource(SYMR.strings.parodies) }, - getItem(character.nullIfEmpty(), { it.joinToString() }) { stringResource(SYMR.strings.characters) }, - ) - } - } - - companion object { - private const val TITLE_TYPE_MAIN = 0 - - const val TAG_TYPE_DEFAULT = 0 - - val BASE_URL = "https://www.tsumino.com" - - val TSUMINO_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.US) - - fun tmIdFromUrl(url: String) = url.toUri().lastPathSegment - - fun thumbUrlFromId(id: String) = "/thumbs/$id/1" - } -} From 21cb1921a314af3c5937b69c780d34cf9a022b76 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Wed, 13 May 2026 13:56:39 +0700 Subject: [PATCH 21/73] Refactor AppBarActions and clean up code (#1635) * remove parentheses * Refactor AppBarActions to improve readability and maintainability * Refactor callback parameters in AppBar actions for clarity --- .../presentation/browse/SourceFeedScreen.kt | 71 ++++++++------- .../components/BrowseSourceEHentaiList.kt | 4 +- .../components/BrowseSourceSimpleToolbar.kt | 39 ++++---- .../components/BulkSelectionToolbar.kt | 90 +++++++++---------- .../library/components/CommonMangaItem.kt | 14 +-- .../presentation/updates/UpdatesUiItem.kt | 2 +- 6 files changed, 108 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index 5a5dd79ae..50ab99dbe 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -336,47 +336,46 @@ fun SourceFeedToolbar( // KMK --> actions = { AppBarActions( - actions = persistentListOf().builder() - .apply { - add(bulkSelectionButton(isRunning, toggleSelectionMode)) + actions = persistentListOf().builder().apply { + add(bulkSelectionButton(isRunning, toggleSelectionMode)) - onWebViewClick?.let { - add( - AppBar.Action( - title = stringResource(MR.strings.action_web_view), - onClick = { onWebViewClick() }, - icon = Icons.Outlined.Public, - ), - ) - } - - // KMK --> + onWebViewClick?.let { func -> add( - AppBar.OverflowAction( - title = stringResource(MR.strings.pref_incognito_mode), - onClick = onToggleIncognito, + AppBar.Action( + title = stringResource(MR.strings.action_web_view), + onClick = { func() }, + icon = Icons.Outlined.Public, ), ) - // KMK <-- - - onSortFeedClick?.let { - add( - AppBar.OverflowAction( - title = stringResource(KMR.strings.action_sort_feed), - onClick = { onSortFeedClick() }, - ), - ) - } - - onSourceSettingClick?.let { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.label_settings), - onClick = { onSourceSettingClick() }, - ), - ) - } } + + // KMK --> + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.pref_incognito_mode), + onClick = onToggleIncognito, + ), + ) + // KMK <-- + + onSortFeedClick?.let { func -> + add( + AppBar.OverflowAction( + title = stringResource(KMR.strings.action_sort_feed), + onClick = { func() }, + ), + ) + } + + onSourceSettingClick?.let { func -> + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.label_settings), + onClick = { func() }, + ), + ) + } + } .build(), ) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt index 2eb0a609d..cc2db3449 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt @@ -209,7 +209,7 @@ fun BrowseSourceEHentaiListItem( MangaCoverHide.Book( modifier = Modifier .fillMaxHeight(), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, ) } else { @@ -219,7 +219,7 @@ fun BrowseSourceEHentaiListItem( .fillMaxHeight(), // KMK --> alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, // KMK <-- data = coverData, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt index 0d6cbf5c2..6a562b74a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt @@ -39,27 +39,26 @@ fun BrowseSourceSimpleToolbar( var selectingDisplayMode by remember { mutableStateOf(false) } // KMK --> AppBarActions( - actions = persistentListOf().builder() - .apply { - displayMode?.let { - add( - AppBar.Action( - title = stringResource(MR.strings.action_display_mode), - icon = if (displayMode == LibraryDisplayMode.List) { - Icons.AutoMirrored.Filled.ViewList - } else { - Icons.Filled.ViewModule - }, - onClick = { selectingDisplayMode = true }, - ), - ) - } - toggleSelectionMode?.let { - add( - bulkSelectionButton(isRunning, toggleSelectionMode), - ) - } + actions = persistentListOf().builder().apply { + displayMode?.let { mode -> + add( + AppBar.Action( + title = stringResource(MR.strings.action_display_mode), + icon = if (mode == LibraryDisplayMode.List) { + Icons.AutoMirrored.Filled.ViewList + } else { + Icons.Filled.ViewModule + }, + onClick = { selectingDisplayMode = true }, + ), + ) } + toggleSelectionMode?.let { mode -> + add( + bulkSelectionButton(isRunning, mode), + ) + } + } .build(), ) // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/components/BulkSelectionToolbar.kt b/app/src/main/java/eu/kanade/presentation/components/BulkSelectionToolbar.kt index c6689ebda..653b0f2a1 100644 --- a/app/src/main/java/eu/kanade/presentation/components/BulkSelectionToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/BulkSelectionToolbar.kt @@ -30,53 +30,51 @@ fun BulkSelectionToolbar( titleContent = { Text(text = "$selectedCount") }, actions = { AppBarActions( - actions = persistentListOf() - .builder() - .apply { - if (onSelectAll != null) { - add( - AppBar.Action( - title = stringResource(MR.strings.action_select_all), - icon = Icons.Filled.SelectAll, - onClick = onSelectAll, - ), - ) - } - if (onReverseSelection != null) { - add( - AppBar.Action( - title = stringResource(MR.strings.action_select_inverse), - icon = Icons.Outlined.FlipToBack, - onClick = onReverseSelection, - ), - ) - } - if (isRunning) { - add( - AppBar.ActionCompose( - title = stringResource(KMR.strings.action_bulk_select), - ) { - CircularProgressIndicator( - modifier = Modifier - .size(24.dp), - strokeWidth = 2.dp, - ) + actions = persistentListOf().builder().apply { + if (onSelectAll != null) { + add( + AppBar.Action( + title = stringResource(MR.strings.action_select_all), + icon = Icons.Filled.SelectAll, + onClick = onSelectAll, + ), + ) + } + if (onReverseSelection != null) { + add( + AppBar.Action( + title = stringResource(MR.strings.action_select_inverse), + icon = Icons.Outlined.FlipToBack, + onClick = onReverseSelection, + ), + ) + } + if (isRunning) { + add( + AppBar.ActionCompose( + title = stringResource(KMR.strings.action_bulk_select), + ) { + CircularProgressIndicator( + modifier = Modifier + .size(24.dp), + strokeWidth = 2.dp, + ) + }, + ) + } else { + add( + AppBar.Action( + title = stringResource(MR.strings.add_to_library), + icon = Icons.Filled.Favorite, + onClick = { + if (selectedCount > 0) { + onChangeCategoryClick() + } }, - ) - } else { - add( - AppBar.Action( - title = stringResource(MR.strings.add_to_library), - icon = Icons.Filled.Favorite, - onClick = { - if (selectedCount > 0) { - onChangeCategoryClick() - } - }, - ), - ) - } - }.build(), + ), + ) + } + }.build(), ) }, isActionMode = true, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt index 309fa2d1a..5485474ec 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt @@ -105,7 +105,7 @@ fun MangaCompactGridItem( MangaCoverHide.Book( modifier = Modifier .fillMaxWidth(), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, ) } else { @@ -119,7 +119,7 @@ fun MangaCompactGridItem( data = coverData, // KMK --> alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, // KMK <-- ) @@ -241,7 +241,7 @@ fun MangaComfortableGridItem( MangaCoverHide.Book( modifier = Modifier .fillMaxWidth(), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, ) } else { @@ -255,7 +255,7 @@ fun MangaComfortableGridItem( data = coverData, // KMK --> alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, onCoverLoaded = { _, result -> val image = result.result.image @@ -274,7 +274,7 @@ fun MangaComfortableGridItem( data = coverData, // KMK --> alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, onCoverLoaded = { _, result -> val image = result.result.image @@ -461,7 +461,7 @@ fun MangaListItem( MangaCoverHide.Square( modifier = Modifier .fillMaxHeight(), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, ) } else { @@ -475,7 +475,7 @@ fun MangaListItem( data = coverData, // KMK --> alpha = coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, size = MangaCover.Size.Big, // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt index 8a67b478e..98ff2d78b 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -309,7 +309,7 @@ private fun UpdatesUiItem( MangaCoverHide.Book( modifier = Modifier .width(UpdateItemWidth), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { selected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { selected }, tint = onBgColor, size = MangaCover.Size.Medium, ) From 19f8fa203ddc42357b8c9bb69f8856b79f7a317b Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Wed, 13 May 2026 14:22:35 +0700 Subject: [PATCH 22/73] Add haptic feedback before actions (#1636) * perform haptic before doing action * Perform haptic feedback call in UI/main thread instead of IO * Haptic feedback on both cases of long press action --- .../libraryUpdateError/components/LibraryUpdateErrorUiItem.kt | 2 +- app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt | 2 +- .../kanade/presentation/manga/components/BaseMangaListItem.kt | 2 +- .../main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt | 2 +- .../tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt | 2 +- app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt | 2 +- app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt | 2 +- app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt index c834a61d5..a18baecd4 100644 --- a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt @@ -142,8 +142,8 @@ private fun LibraryUpdateErrorUiItem( .combinedClickable( onClick = onClick, onLongClick = { - onLongClick() haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() }, ) .padding(horizontal = MaterialTheme.padding.medium), diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index f8f5fc463..08cdfa37e 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -1372,8 +1372,8 @@ private fun LazyListScope.sharedChapterItems( chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onLongClick = { - onChapterSelected(item, !item.selected, true) haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onChapterSelected(item, !item.selected, true) }, onClick = { onChapterItemClick( diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt index e56216ba0..eb134704b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt @@ -43,8 +43,8 @@ fun BaseMangaListItem( onClick = onClickItem, // KMK --> onLongClick = { - onLongClick() haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() }, // KMK <-- ) diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt index 98ff2d78b..44bd792fc 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -286,8 +286,8 @@ private fun UpdatesUiItem( .combinedClickable( onClick = onClick, onLongClick = { - onLongClick() haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() }, ) .padding(top = if (isLeader) MaterialTheme.padding.small else 0.dp) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index cc875e266..edb69c9df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -375,6 +375,7 @@ data class BrowseSourceScreen( }, onMangaLongClick = { manga -> // KMK --> + haptic.performHapticFeedback(HapticFeedbackType.LongPress) if (bulkFavoriteState.selectionMode) { navigator.push(MangaScreen(manga.id, true)) } else { @@ -388,7 +389,6 @@ data class BrowseSourceScreen( ) else -> screenModel.addFavorite(manga) } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) } } }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index 5eda7ac7e..c35c63142 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -329,8 +329,8 @@ data object LibraryTab : Tab { }.takeIf { state.showMangaContinueButton }, onToggleSelection = screenModel::toggleSelection, onToggleRangeSelection = { category, manga -> - screenModel.toggleRangeSelection(category, manga) haptic.performHapticFeedback(HapticFeedbackType.LongPress) + screenModel.toggleRangeSelection(category, manga) }, onRefresh = { onClickRefresh(state.activeCategory) }, onGlobalSearchClicked = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index c2e296d82..9b3739ae3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -278,8 +278,8 @@ class MangaScreen( onChapterClicked = { openChapter(context, it) }, onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() }, onAddToLibraryClicked = { - screenModel.toggleFavorite() haptic.performHapticFeedback(HapticFeedbackType.LongPress) + screenModel.toggleFavorite() }, // SY --> onWebViewClicked = { diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index 71b907bb6..658823b05 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -127,6 +127,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { }, onMangaLongClick = { manga -> // KMK --> + haptic.performHapticFeedback(HapticFeedbackType.LongPress) if (bulkFavoriteState.selectionMode) { navigator.push(MangaScreen(manga.id, true)) } else { @@ -140,7 +141,6 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { ) else -> screenModel.addFavorite(manga) } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) } } }, From 768c3dcec572e15b625b8023039927ab33314dc9 Mon Sep 17 00:00:00 2001 From: Leodyver Semilla <124951048+leodyversemilla07@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:48:16 +0800 Subject: [PATCH 23/73] Fix extension actions disappearing after installing and uninstalling in same session (mihonapp/mihon#3049) Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 2f9edb551fb2255c11ccd8452a080e87b9c963eb) --- .../tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt index 5065fcf31..fbaa435ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tachiyomi.core.common.util.lang.launchIO @@ -228,6 +229,7 @@ class ExtensionsScreenModel( private suspend fun Flow.collectToInstallUpdate(extension: Extension) = this .onEach { installStep -> addDownloadState(extension, installStep) } + .takeWhile { installStep -> installStep != InstallStep.Installed } .onCompletion { removeDownloadState(extension) } .collect() From 0b285f83b109d3a5032e796da32251c6034fee70 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:12:14 +0600 Subject: [PATCH 24/73] Fix occasional crash when mass installing/uninstalling extension using `PackageManager` (cherry picked from commit 42daa3f432292be5fdfb3ecae07592cb94324164) --- .../extension/installer/PackageInstallerInstaller.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt index 931bf48af..e65862330 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt @@ -106,8 +106,13 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic override fun cancelEntry(entry: Entry): Boolean { activeSession?.let { (activeEntry, sessionId) -> if (activeEntry == entry) { - packageInstaller.abandonSession(sessionId) - return false + return try { + packageInstaller.abandonSession(sessionId) + false + } catch (_: SecurityException) { + // Highly likely the session has succeeded + true + } } } return true From dc7dd8045e31cb97dc6b66b98ee3e78e6442f56d Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Fri, 15 May 2026 15:07:15 +0700 Subject: [PATCH 25/73] Ensure extension installer's download IDs are non-negative (#1648) --- .../tachiyomi/extension/util/ExtensionInstaller.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 15b1af052..2995ae1de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -58,8 +58,8 @@ internal class ExtensionInstaller( val pkgName = extension.pkgName + // KMK --> "_${extension.signatureHash}" + val downloadId = pkgName.toDownloadId() // KMK <-- - val downloadId = pkgName.hashCode().toLong() cancelInstall(pkgName) val step = MutableStateFlow(InstallStep.Pending) @@ -158,7 +158,7 @@ internal class ExtensionInstaller( */ fun cancelInstall(pkgName: String) { activeJobs.remove(pkgName)?.cancel() - Installer.cancelInstallQueue(context, pkgName.hashCode().toLong()) + Installer.cancelInstallQueue(context, /* KMK --> */ pkgName.toDownloadId() /* KMK <-- */) } /** @@ -191,5 +191,10 @@ internal class ExtensionInstaller( companion object { const val APK_MIME = "application/vnd.android.package-archive" const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID" + + // KMK --> + /** Convert packageName to download ID avoiding negative number */ + private fun String.toDownloadId(): Long = hashCode().toLong() and 0xFFFFFFFFL + // KMK <-- } } From 94eac94ce76d85330c40e732380e2192f279e537 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Tue, 19 May 2026 15:44:58 +0700 Subject: [PATCH 26/73] ExtensionInstaller: Fix mistakenly canceling unrelated `waitingInstall` & avoid adding duplicate entry to installing `queue` (#1649) * ExtensionInstaller: Fix mistakenly canceling unrelated `waitingInstall` & avoid adding duplicate entry to installing `queue` - Changes the installer queue from a synchronized list to a synchronized set to prevent duplicate entries. - Updates queue removal logic to remove specific entries instead of using index-based removal. - Refines `cancelQueue` logic to ensure the active installation is only cancelled if its download ID matches the requested ID. * Ensure thread-safe access to the installation queue when canceling downloads --- .../tachiyomi/extension/installer/Installer.kt | 14 +++++++++++--- .../installer/PackageInstallerInstaller.kt | 7 +++---- .../extension/util/ExtensionInstallService.kt | 5 ++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt index 3a2200632..645a37d42 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt @@ -24,7 +24,9 @@ abstract class Installer(private val service: Service) { private val extensionManager: ExtensionManager by injectLazy() private var waitingInstall = AtomicReference(null) - private val queue = Collections.synchronizedList(mutableListOf()) + // KMK --> + private val queue = Collections.synchronizedSet(mutableSetOf()) + // KMK <-- private val cancelReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -104,7 +106,9 @@ abstract class Installer(private val service: Service) { } val nextEntry = queue.first() if (waitingInstall.compareAndSet(null, nextEntry)) { - queue.removeAt(0) + // KMK --> + queue.remove(nextEntry) + // KMK <-- processEntry(nextEntry) } } @@ -129,7 +133,11 @@ abstract class Installer(private val service: Service) { */ private fun cancelQueue(downloadId: Long) { val waitingInstall = this.waitingInstall.load() - val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return + // KMK --> + val toCancel = synchronized(queue) { queue.find { it.downloadId == downloadId } } + ?: waitingInstall?.takeIf { it.downloadId == downloadId } + // KMK <-- + ?: return if (cancelEntry(toCancel)) { queue.remove(toCancel) if (waitingInstall == toCancel) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt index e65862330..a3e37a6aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt @@ -15,8 +15,7 @@ import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.util.lang.use import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat import eu.kanade.tachiyomi.util.system.getUriSize -import logcat.LogPriority -import tachiyomi.core.common.util.system.logcat +import exh.log.xLogE class PackageInstallerInstaller(private val service: Service) : Installer(service) { @@ -42,7 +41,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic .sanitizeByFiltering(this) } if (userAction == null) { - logcat(LogPriority.ERROR) { "Fatal error for $intent" } + xLogE("Fatal error for $intent") continueQueue(InstallStep.Error) return } @@ -95,7 +94,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic session.commit(intentSender) } } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } + xLogE("Failed to install extension ${entry.downloadId} ${entry.uri}", e) activeSession?.let { (_, sessionId) -> packageInstaller.abandonSession(sessionId) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt index dab5df61e..f082fe4a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt @@ -16,9 +16,8 @@ import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat import eu.kanade.tachiyomi.util.system.notificationBuilder -import logcat.LogPriority +import exh.log.xLogE import tachiyomi.core.common.i18n.stringResource -import tachiyomi.core.common.util.system.logcat import tachiyomi.i18n.MR class ExtensionInstallService : Service() { @@ -53,7 +52,7 @@ class ExtensionInstallService : Service() { BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this) BasePreferences.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this) else -> { - logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" } + xLogE("Not implemented for installer $installerUsed") stopSelf() return START_NOT_STICKY } From 9a7d1b61438b2c19422a702e1f4b0e75f71a585d Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Tue, 19 May 2026 15:58:12 +0700 Subject: [PATCH 27/73] Fix ANR while installing extensions (#1652) Update foreground service type for ExtensionInstallService to dataSync --- app/src/main/AndroidManifest.xml | 2 +- .../extension/util/ExtensionInstallService.kt | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bfe5a748a..b72704c99 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -241,7 +241,7 @@ + android:foregroundServiceType="dataSync" /> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + Notifications.ID_EXTENSION_INSTALLER, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, + ) + } else { + // KMK <-- + startForeground(Notifications.ID_EXTENSION_INSTALLER, notification) + } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { From ca26501aef598a25b6575f97efb15e01a984206c Mon Sep 17 00:00:00 2001 From: Itsmechinmoy <167056923+itsmechinmoy@users.noreply.github.com> Date: Tue, 19 May 2026 16:57:36 +0530 Subject: [PATCH 28/73] feat(Anilist): Parse GraphQL error responses for downtime and token expiry (#1591) * Create ALError.kt * Update AnilistApi.kt * Update AnilistApi.kt * Update AnilistApi.kt * Add error handling for AniList API responses * Update AnilistApi.kt * Update ALError.kt * Update AnilistApi.kt * fix: improve error handling and resource management in AniList API calls * Fix interpolation sign & add comment --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- .../data/track/anilist/AnilistApi.kt | 61 ++++++++++++++++--- .../data/track/anilist/dto/ALError.kt | 14 +++++ 2 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALError.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 0b6dca2b9..1ee49927e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -5,6 +5,7 @@ import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult +import eu.kanade.tachiyomi.data.track.anilist.dto.ALError import eu.kanade.tachiyomi.data.track.anilist.dto.ALIdSearchResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth @@ -26,6 +27,7 @@ import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response import tachiyomi.core.common.util.lang.withIOContext import uy.kohesive.injekt.injectLazy import java.time.Instant @@ -43,6 +45,25 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { .rateLimit(permits = 85, period = 1.minutes) .build() + // KMK --> + private fun Response.parseALError() { + val bodyString = peekBody(1024 * 1024).string() + val errorObj = try { + json.decodeFromString(bodyString) + } catch (_: Exception) { + null + } + + errorObj?.errors?.firstOrNull()?.let { + val msg = it.message + if (msg.contains("Invalid token") || it.status == 401) { + throw Exception("AniList token expired, please login again") + } + throw Exception(msg) + } + } + // KMK <-- + suspend fun addLibManga(track: Track): Track { return withIOContext { val query = $$""" @@ -71,6 +92,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .let { track.library_id = it.data.entry.id @@ -112,6 +136,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() + // KMK --> + .use { it.parseALError() } + // KMK <-- track } } @@ -134,6 +161,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() + // KMK --> + .use { it.parseALError() } + // KMK <-- } } @@ -192,6 +222,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .data.page.media .map { it.toALManga().toTrack() } @@ -271,6 +304,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .data.page.mediaList .map { it.toALUserManga() } @@ -312,6 +348,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .let { val viewer = it.data.viewer @@ -323,9 +362,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata { return withIOContext { - val query = """ - |query (${'$'}mangaId: Int!) { - |Media (id: ${'$'}mangaId) { + val query = $$""" + |query ($mangaId: Int!) { + |Media (id: $mangaId) { |id |title { |userPreferred @@ -365,9 +404,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() - .let { - val media = it.data.media + .let { metadata -> + val media = metadata.data.media TrackMangaMetadata( remoteId = media.id, title = media.title.userPreferred, @@ -392,9 +434,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { // SY --> suspend fun searchById(id: String): TrackSearch { return withIOContext { - val query = """ - |query (${'$'}mangaId: Int!) { - |Media (id: ${'$'}mangaId) { + val query = $$""" + |query ($mangaId: Int!) { + |Media (id: $mangaId) { |id |title { |userPreferred @@ -430,6 +472,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .data.media .toALManga() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALError.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALError.kt new file mode 100644 index 000000000..7376cb881 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALError.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ALError( + val errors: List? = null, +) + +@Serializable +data class ALErrorItem( + val message: String, + val status: Int? = null, +) From 266947b0d769cbddfefff35f49424d0b8fa1414b Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Tue, 19 May 2026 18:41:03 +0700 Subject: [PATCH 29/73] Bump version --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 17d45026e..cb25dd904 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,7 +28,7 @@ android { defaultConfig { applicationId = "app.komikku" - versionCode = 78 + versionCode = 79 versionName = "1.13.6" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") From c1806e13b3364ed8fd72d16d91aa4ff0720470f5 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Fri, 22 May 2026 23:49:37 +0700 Subject: [PATCH 30/73] Agent.md (#1658) * Generate `AGENTS.md` using CoPilot * Re-generate by Cursor & manual merge * Review again with Cursor --- AGENTS.md | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..b7d84f730 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,131 @@ +# Komikku – AI Agent Guide + +Komikku is an Android manga reader (min SDK 26, target SDK 36, JVM 17 / Kotlin) forked from **Mihon** + **TachiyomiSY**. Stack: Jetpack Compose + Material3, Voyager navigation, SQLDelight, Injekt DI. `applicationId`: `app.komikku`. + +--- + +## Module layout + +| Module | Purpose | +|--------|---------| +| `app/` | UI (`eu.kanade.*`, `exh/`, `mihon/`), DI, workers, build variants | +| `domain/` | Use cases in `…/interactor/` (e.g. `GetManga`), models, repo interfaces | +| `data/` | SQLDelight DB, `*RepositoryImpl` (`tachiyomi.data.*`) | +| `core:common/` | Network (OkHttp), security, storage, shared utils | +| `core:archive/` | CBZ/archive reading with optional encryption | +| `core-metadata/` | Comic-info metadata parsing | +| `source-api/` / `source-local/` | Extension `Source` API + local source | +| `presentation-core/` | Shared Compose components | +| `presentation-widget/` | Home-screen Glance widget | +| `i18n/` | Mihon strings → `MR` (moko-resources) | +| `i18n-kmk/` | Komikku strings → `KMR` | +| `i18n-sy/` | TachiyomiSY strings → `SYMR` | +| `flagkit/` | Country-flag drawables | +| `telemetry/` | Firebase/Crashlytics (noop unless `-Pinclude-telemetry`) | +| `macrobenchmark/` | Macrobenchmark tests | + +Dependency flow: `app` → `domain` → `source-api`; `data` implements `domain` repos. + +Version catalogs: `gradle/libs.versions.toml`, `kotlinx.versions.toml`, `androidx.versions.toml`, `compose.versions.toml`, `sy.versions.toml`. + +--- + +## Architecture + +**DI** – `uy.kohesive.injekt` (not Hilt). Register in `AppModule.kt`, `DomainModule.kt`, `KMKDomainModule.kt`, `SYDomainModule.kt` via `addSingleton` / `addSingletonFactory`. Resolve with `Injekt.get()` or `injectLazy()`. + +**UI & navigation** – [Voyager](https://voyager.adriel.cafe/): `Screen` in `eu.kanade.tachiyomi.ui.*`, composables in `eu.kanade.presentation.*`. Base type: `eu.kanade.presentation.util.Screen`. State via `rememberScreenModel { … }`; most models extend `StateScreenModel` or bases like `SearchScreenModel`; some use plain `ScreenModel`. Prefer `screenModelScope` and `ioCoroutineScope`; use `launchIO` / `withIOContext` from `tachiyomi.core.common.util.lang`. `rememberCoroutineScope()` is fine in Compose; long-lived services may use their own `CoroutineScope`. + +**Activities (not Voyager)** – `MainActivity` (shell), `ReaderActivity` + `ReaderViewModel`, `WebViewActivity`, `UnlockActivity`, OAuth login activities, `DeepLinkActivity`. Reader: `ReaderActivity.newIntent(context, mangaId, chapterId)`. Web: both `WebViewScreen` (Voyager) and `WebViewActivity.newIntent(...)`. + +Example: `DeepLinkScreen` + `DeepLinkScreenModel` in `app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/`. + +**Domain / data** – One class per operation under `domain/…/interactor/` (verb names, not `*Interactor` suffix). Also `app/src/main/java/eu/kanade/domain/…/interactor/` for app-specific cases. Wire repos in `eu.kanade.domain.DomainModule.kt` (+ `KMKDomainModule`, `SYDomainModule`). + +**Database** – SQLDelight in `data/src/main/sqldelight/tachiyomi/` (`.sq` queries, `migrations/*.sqm`). After schema changes add a new `.sqm` and often `// KMK` blocks in `.sq` / mappers. Regenerate: `./gradlew :data:generateSqlDelightInterface` (or any compile that touches `:data`). + +**App preference migrations** – `app/src/main/java/mihon/core/migration/migrations/` (`mihon.core.migration.Migration`). + +**Images** – Coil 3 (`coil3.*`, `context.imageLoader`). No Glide/Picasso. + +--- + +## Komikku-specific work + +- New user-facing strings: **`KMR`** in `i18n-kmk/src/commonMain/moko-resources/base/`. Shared/upstream: **`MR`** (`i18n`). SY-only: **`SYMR`** (`i18n-sy`). +- Do not edit locale `strings.xml` in `i18n/` or `i18n-sy/` except when syncing upstream; translations via [Weblate](https://hosted.weblate.org/engage/komikku-app/). +- Komikku code/DI: search `// KMK` (e.g. `KMKDomainModule`, `HideCategory`, library-update errors). +- Prefs: `eu.kanade.domain.*.service.*Preferences` (e.g. `SourcePreferences.relatedMangas()`). + +--- + +## Extensions & sources + +- Catalog sources: installable APK extensions (not in this repo). +- In-repo: delegated sources and metadata in `exh/` (E-Hentai, NHentai, MangaDex, `exh/recs/`). +- `source-api`: `eu.kanade.tachiyomi.source.*` — avoid breaking extension ABI. + +--- + +## Build & CI + +Build types: `debug` (`.dev`), `release`, `releaseTest` (`.rt`), `foss` (`.foss`), `preview` (`.beta`, CI default), `benchmark`. + +Gradle `-P` flags (`buildSrc/.../BuildConfig.kt`): + +| Flag | Effect | +|------|--------| +| `include-telemetry` | Firebase Analytics + Crashlytics | +| `enable-updater` | In-app update checker | +| `disable-code-shrink` | Skip R8 minification | +| `include-dependency-info` | Dependency metadata in APK | + +```bash +./gradlew spotlessApply # format (CI: spotlessCheck) +./gradlew assemblePreview # main CI/dev APK +./gradlew assemblePreview -Pinclude-telemetry -Penable-updater # full upstream CI build +./gradlew testReleaseUnitTest # CI unit tests (or ./gradlew test for all modules) +./gradlew installDebug # device install +./gradlew :data:generateSqlDelightInterface # after .sq / .sqm changes +``` + +JDK **17**. + +--- + +## Fork-origin markers + +Preserve inline blocks when editing: + +```kotlin +// KMK --> … // KMK <-- Komikku +// SY --> … // SY <-- TachiyomiSY +// EXH --> … // EXH <-- E-Hentai / exh (existing); prefer KMK for new Komikku-only code +``` + +Package roots: `eu.kanade.tachiyomi.*` (legacy UI), `tachiyomi.*` (domain/data), `mihon.*` (Mihon upstream), `exh.*` (enhanced sources). + +--- + +## Tests + +- Unit tests: `domain/src/test/`; app: `app/src/test/.../MigratorTest.kt`. No broad UI test suite. + +--- + +## Conventions + +- **Logging** – Prefer `xLogE()` / `xLog()` helpers from `exh.log` for Komikku code, Mihon uses `logcat { }` from `tachiyomi.core.common.util.system`. Avoid raw `android.util.Log`. +- **Formatting** – Spotless + ktlint (`buildSrc/.../mihon.code.lint.gradle.kts`). +- **Fork edits** – New Komikku features inside `// KMK` islands; keep `// SY` / `// EXH` blocks intact when merging upstream. + +--- + +## Key files + +- `App.kt` – Injekt bootstrap, logging setup +- `MainActivity.kt` – Voyager host +- `app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt` – core DI +- `app/src/main/java/eu/kanade/domain/DomainModule.kt` – domain interactors +- `buildSrc/.../BuildConfig.kt`, `AndroidConfig.kt` – flags, SDK versions +- `app/build.gradle.kts`, `settings.gradle.kts` From 30c114c20fbc79a180c655a3b2b4a810f7ba2079 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Sat, 23 May 2026 00:31:09 +0700 Subject: [PATCH 31/73] Add Cursor Cloud specific instructions to AGENTS.md (#1659) Add development environment setup documentation for Cloud Agent VMs, including Android SDK configuration, key Gradle commands, and gotchas for headless development without an emulator. Co-authored-by: Cursor Agent Co-authored-by: Cuong-Tran --- AGENTS.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index b7d84f730..9b0d3c9bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,3 +129,38 @@ Package roots: `eu.kanade.tachiyomi.*` (legacy UI), `tachiyomi.*` (domain/data), - `app/src/main/java/eu/kanade/domain/DomainModule.kt` – domain interactors - `buildSrc/.../BuildConfig.kt`, `AndroidConfig.kt` – flags, SDK versions - `app/build.gradle.kts`, `settings.gradle.kts` + +--- + +## Cursor Cloud specific instructions + +### Environment + +The VM update script installs the Android SDK (platform 36, build-tools 35.0.1, platform-tools, cmdline-tools) into `/opt/android-sdk` and writes `local.properties` with `sdk.dir`. JDK 21 is pre-installed and works fine for compiling to JVM target 17. `ANDROID_HOME`, `JAVA_HOME`, and `PATH` are set in `~/.bashrc`. + +### Running key commands + +All Gradle commands require the environment variables set above. Export them before invoking `./gradlew` if running in a fresh shell: + +```bash +export ANDROID_HOME=/opt/android-sdk +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +``` + +| Task | Command | +|------|---------| +| Format check (lint) | `./gradlew spotlessCheck` | +| Auto-fix formatting | `./gradlew spotlessApply` | +| Debug APK build | `./gradlew assembleDebug` | +| Preview APK build (CI) | `./gradlew assemblePreview` | +| Unit tests (CI) | `./gradlew testReleaseUnitTest` | +| All module tests | `./gradlew test` | +| SQLDelight codegen | `./gradlew :data:generateSqlDelightInterface` | + +### Gotchas + +- First Gradle build downloads ~1 GB of dependencies; subsequent builds use the Gradle cache and are much faster. +- `local.properties` is `.gitignore`d — it must be recreated if missing (the update script handles this). +- No Android emulator or device is available on the Cloud VM, so `installDebug` will fail. Build verification is done via `assembleDebug`. +- `google-services.json` and `client_secrets.json` are not present (CI secrets); builds without `-Pinclude-telemetry` succeed without them. +- Gradle daemon may use significant memory (`-Xmx4g` in `gradle.properties`). If OOM occurs, kill and restart the daemon with `./gradlew --stop`. From 9ccf5ca29153eff061cf5da87cc1c91f33e5b60e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 23 May 2026 06:42:27 +0000 Subject: [PATCH 32/73] fix(library): notify when sync fails before library update Report sync failures that previously failed silently (e.g. null remote backup) and show a library update error notification when a chained sync blocks the update, including the sync failure reason. Co-authored-by: Cuong-Tran --- .../data/library/LibraryUpdateJob.kt | 12 ++++++ .../data/library/LibraryUpdateNotifier.kt | 4 +- .../data/library/LibraryUpdateSyncResult.kt | 20 ++++++++++ .../kanade/tachiyomi/data/sync/SyncDataJob.kt | 31 +++++++++++++-- .../tachiyomi/data/sync/SyncDataResult.kt | 28 +++++++++++++ .../kanade/tachiyomi/data/sync/SyncManager.kt | 39 ++++++++++++++----- .../sync/service/GoogleDriveSyncService.kt | 9 ++++- .../data/sync/service/SyncYomiSyncService.kt | 16 +++++--- .../data/sync/service/WebDavSyncService.kt | 6 ++- .../moko-resources/base/strings.xml | 2 + 10 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateSyncResult.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataResult.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index e5c2c5124..7a98c4bae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -182,6 +182,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet return withIOContext { try { + LibraryUpdateSyncResult.consumeFailure()?.let { syncError -> + notifier.showUpdateErrorNotification( + failed = 1, + errorMessage = context.stringResource( + MR.strings.library_update_skipped_sync_error, + syncError, + ), + ) + return@withIOContext Result.success() + } + when (target) { Target.CHAPTERS -> updateChapterList() // SY --> @@ -844,6 +855,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet // Define the SyncDataJob val syncDataJob = OneTimeWorkRequestBuilder() .addTag(SyncDataJob.TAG_MANUAL) + .addTag(SyncDataJob.TAG_BEFORE_LIBRARY_UPDATE) .build() // Chain SyncDataJob to run before LibraryUpdateJob diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 8267edad2..ebbbb35ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -153,7 +153,7 @@ class LibraryUpdateNotifier( * * @param failed Number of entries that failed to update. */ - fun showUpdateErrorNotification(failed: Int) { + fun showUpdateErrorNotification(failed: Int, errorMessage: String? = null) { if (failed == 0) { return } @@ -163,7 +163,7 @@ class LibraryUpdateNotifier( Notifications.CHANNEL_LIBRARY_ERROR, ) { setContentTitle(context.stringResource(MR.strings.notification_update_error, failed)) - setContentText(context.stringResource(MR.strings.action_show_errors)) + setContentText(errorMessage ?: context.stringResource(MR.strings.action_show_errors)) setSmallIcon(R.drawable.ic_komikku) setColor(ContextCompat.getColor(context, R.color.ic_launcher)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateSyncResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateSyncResult.kt new file mode 100644 index 000000000..9867d748b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateSyncResult.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.data.library + +/** + * Holds a sync failure message to be reported by [LibraryUpdateJob] when a library update + * was chained to run after [eu.kanade.tachiyomi.data.sync.SyncDataJob]. + */ +object LibraryUpdateSyncResult { + @Volatile + private var pendingError: String? = null + + fun setFailure(message: String) { + pendingError = message + } + + fun consumeFailure(): String? { + val error = pendingError + pendingError = null + return error + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt index b1bef64af..c17df974e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt @@ -14,14 +14,18 @@ import androidx.work.WorkQuery import androidx.work.WorkerParameters import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.SyncStatus +import eu.kanade.tachiyomi.data.library.LibraryUpdateSyncResult import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.setForegroundSafely import eu.kanade.tachiyomi.util.system.workManager +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.runBlocking import logcat.LogPriority +import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.system.logcat +import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.TimeUnit @@ -53,12 +57,22 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters) setForegroundSafely() return try { - SyncManager(context).syncData() - Result.success() + when (val result = SyncManager(context).syncData()) { + SyncDataResult.Success -> Result.success() + is SyncDataResult.Failure -> { + handleSyncFailure(result.message, result.errorAlreadyNotified) + Result.success() + } + } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { logcat(LogPriority.ERROR, e) - notifier.showSyncError(e.message) - Result.success() // try again next time + handleSyncFailure( + e.message ?: context.stringResource(MR.strings.unknown_error), + errorAlreadyNotified = false, + ) + Result.success() } finally { // KMK --> syncStatus.stop() @@ -78,10 +92,19 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters) ) } + private fun handleSyncFailure(message: String, errorAlreadyNotified: Boolean) { + if (tags.contains(TAG_BEFORE_LIBRARY_UPDATE)) { + LibraryUpdateSyncResult.setFailure(message) + } else if (!errorAlreadyNotified) { + notifier.showSyncError(message) + } + } + companion object { private const val TAG_JOB = "SyncDataJob" private const val TAG_AUTO = "$TAG_JOB:auto" const val TAG_MANUAL = "$TAG_JOB:manual" + const val TAG_BEFORE_LIBRARY_UPDATE = "$TAG_JOB:before-library-update" fun isRunning(context: Context): Boolean { return context.workManager.isRunning(TAG_JOB) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataResult.kt new file mode 100644 index 000000000..3c1d50292 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataResult.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.data.sync + +/** + * Captures the most recent sync failure reported by a [eu.kanade.tachiyomi.data.sync.service.SyncService] + * implementation so [SyncManager] can propagate the message without losing context. + */ +internal object SyncFailureState { + private var failure: Pair? = null + + fun report(message: String, errorAlreadyNotified: Boolean = true) { + failure = message to errorAlreadyNotified + } + + fun consume(): Pair? = failure.also { failure = null } +} + +sealed interface SyncDataResult { + data object Success : SyncDataResult + + /** + * @param message Human-readable failure reason. + * @param errorAlreadyNotified `true` when a sync service already showed a sync error notification. + */ + data class Failure( + val message: String, + val errorAlreadyNotified: Boolean = false, + ) : SyncDataResult +} 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 ec289d2e7..a4727a50c 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 @@ -19,7 +19,9 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf import logcat.LogPriority import logcat.logcat +import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.system.logcat +import tachiyomi.i18n.MR import tachiyomi.data.Chapters import tachiyomi.data.DatabaseHandler import tachiyomi.data.manga.MangaMapper.mapManga @@ -72,7 +74,9 @@ class SyncManager( * This function retrieves local data (favorites, manga, extensions, and categories) * from the database using the BackupManager, then synchronizes the data with a sync service. */ - suspend fun syncData() { + suspend fun syncData(): SyncDataResult { + SyncFailureState.consume() + // Reset isSyncing in case it was left over or failed syncing during restore. handler.await(inTransaction = true) { mangasQueries.resetIsSyncing() @@ -153,12 +157,22 @@ class SyncManager( } } - val remoteBackup = syncService?.doSync(syncData) + if (syncService == null) { + return SyncDataResult.Failure( + message = context.stringResource(MR.strings.sync_failed_generic), + ) + } + + val remoteBackup = syncService.doSync(syncData) if (remoteBackup == null) { - logcat(LogPriority.DEBUG) { "Skip restore due to network issues" } - // should we call showSyncError? - return + logcat(LogPriority.DEBUG) { "Skip restore due to sync failure" } + val (message, errorAlreadyNotified) = SyncFailureState.consume() + ?: (context.stringResource(MR.strings.sync_failed_generic) to false) + return SyncDataResult.Failure( + message = message, + errorAlreadyNotified = errorAlreadyNotified, + ) } if (remoteBackup === syncData.backup) { @@ -166,13 +180,14 @@ class SyncManager( logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" } syncPreferences.lastSyncTimestamp().set(Date().time) notifier.showSyncSuccess("Sync completed successfully") - return + return SyncDataResult.Success } // Stop the sync early if the remote backup is null or empty if (remoteBackup.backupManga.isEmpty()) { - notifier.showSyncError("No data found on remote server.") - return + val message = "No data found on remote server." + notifier.showSyncError(message) + return SyncDataResult.Failure(message = message, errorAlreadyNotified = true) } // Check if it's first sync based on lastSyncTimestamp @@ -180,7 +195,7 @@ class SyncManager( // It's first sync no need to restore data. (just update remote data) syncPreferences.lastSyncTimestamp().set(Date().time) notifier.showSyncSuccess("Updated remote data successfully") - return + return SyncDataResult.Success } val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup) @@ -208,7 +223,7 @@ class SyncManager( // update the sync timestamp syncPreferences.lastSyncTimestamp().set(Date().time) notifier.showSyncSuccess("Sync completed successfully") - return + return SyncDataResult.Success } val backupUri = writeSyncDataToCache(context, newSyncData) @@ -228,8 +243,12 @@ class SyncManager( // update the sync timestamp syncPreferences.lastSyncTimestamp().set(Date().time) + return SyncDataResult.Success } else { logcat(LogPriority.ERROR) { "Failed to write sync data to file" } + return SyncDataResult.Failure( + message = context.stringResource(MR.strings.sync_failed_generic), + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt index 0393649a7..6a34c0329 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt @@ -18,6 +18,8 @@ import com.google.api.services.drive.DriveScopes import com.google.api.services.drive.model.File import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.data.sync.SyncFailureState +import eu.kanade.tachiyomi.data.sync.SyncNotifier import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf @@ -63,6 +65,8 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync private val googleDriveService = GoogleDriveService(context) + private val notifier = SyncNotifier(context) + private val protoBuf: ProtoBuf = Injekt.get() override suspend fun doSync(syncData: SyncData): Backup? { @@ -96,7 +100,10 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync pushSyncData(syncData) return syncData.backup } catch (e: Exception) { - logcat(LogPriority.ERROR, "SyncService") { "Error syncing: ${e.message}" } + val message = e.message ?: context.stringResource(MR.strings.unknown_error) + logcat(LogPriority.ERROR, "SyncService") { "Error syncing: $message" } + notifier.showSyncError(message) + SyncFailureState.report(message) return null } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt index 3db50c859..0667085b8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.sync.service import android.content.Context import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.data.sync.SyncFailureState import eu.kanade.tachiyomi.data.sync.SyncNotifier import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -87,10 +88,11 @@ class SyncYomiSyncService( if (success) { reportSyncEvent(SyncEventStatus.SYNC_SUCCESS) } else { - reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data") - // KMK --> + val message = "Failed to push sync data" + reportSyncEvent(SyncEventStatus.SYNC_FAILED, message) + notifier.showSyncError(message) + SyncFailureState.report(message) return null - // KMK <-- } return finalSyncData.backup @@ -99,9 +101,11 @@ class SyncYomiSyncService( reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message) throw e } - logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" } - notifier.showSyncError(e.message) - reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message) + val message = e.message ?: context.stringResource(MR.strings.unknown_error) + logcat(LogPriority.ERROR) { "Error syncing: $message" } + notifier.showSyncError(message) + SyncFailureState.report(message) + reportSyncEvent(SyncEventStatus.SYNC_ERROR, message) return null } } 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 40b3366a9..77a2f0aa7 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 @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.sync.service import android.content.Context import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.data.sync.SyncFailureState import eu.kanade.tachiyomi.data.sync.SyncNotifier import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.PUT @@ -117,7 +118,9 @@ class WebDavSyncService( return finalSyncData.backup } catch (e: Exception) { xLogE("WebDAV sync error:", e) - notifier.showSyncError(e.message) + val message = e.message ?: context.stringResource(MR.strings.unknown_error) + notifier.showSyncError(message) + SyncFailureState.report(message) return null } } @@ -182,6 +185,7 @@ class WebDavSyncService( "Please retry syncing or check your WebDAV folder." xLogW(message) notifier.showSyncError(message) + SyncFailureState.report(message) } else -> { val bodyStr = response.body.string() diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index a1466b4f4..ff5b40419 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -923,6 +923,8 @@ Chapter %1$s and %2$d more Chapters %1$s %1$d update(s) failed + Library update skipped: %1$s + Could not sync with the remote service Tap to learn more Failed to update cover Please add the entry to your library before doing this From 99470e8133ec344d114a40b7fc92f32ea6cd30aa Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Wed, 27 May 2026 14:20:56 +0700 Subject: [PATCH 33/73] Revert "fix(library): notify when sync fails before library update" This reverts commit 9ccf5ca29153eff061cf5da87cc1c91f33e5b60e. --- .../data/library/LibraryUpdateJob.kt | 12 ------ .../data/library/LibraryUpdateNotifier.kt | 4 +- .../data/library/LibraryUpdateSyncResult.kt | 20 ---------- .../kanade/tachiyomi/data/sync/SyncDataJob.kt | 31 ++------------- .../tachiyomi/data/sync/SyncDataResult.kt | 28 ------------- .../kanade/tachiyomi/data/sync/SyncManager.kt | 39 +++++-------------- .../sync/service/GoogleDriveSyncService.kt | 9 +---- .../data/sync/service/SyncYomiSyncService.kt | 16 +++----- .../data/sync/service/WebDavSyncService.kt | 6 +-- .../moko-resources/base/strings.xml | 2 - 10 files changed, 24 insertions(+), 143 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateSyncResult.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataResult.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 7a98c4bae..e5c2c5124 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -182,17 +182,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet return withIOContext { try { - LibraryUpdateSyncResult.consumeFailure()?.let { syncError -> - notifier.showUpdateErrorNotification( - failed = 1, - errorMessage = context.stringResource( - MR.strings.library_update_skipped_sync_error, - syncError, - ), - ) - return@withIOContext Result.success() - } - when (target) { Target.CHAPTERS -> updateChapterList() // SY --> @@ -855,7 +844,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet // Define the SyncDataJob val syncDataJob = OneTimeWorkRequestBuilder() .addTag(SyncDataJob.TAG_MANUAL) - .addTag(SyncDataJob.TAG_BEFORE_LIBRARY_UPDATE) .build() // Chain SyncDataJob to run before LibraryUpdateJob diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index ebbbb35ca..8267edad2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -153,7 +153,7 @@ class LibraryUpdateNotifier( * * @param failed Number of entries that failed to update. */ - fun showUpdateErrorNotification(failed: Int, errorMessage: String? = null) { + fun showUpdateErrorNotification(failed: Int) { if (failed == 0) { return } @@ -163,7 +163,7 @@ class LibraryUpdateNotifier( Notifications.CHANNEL_LIBRARY_ERROR, ) { setContentTitle(context.stringResource(MR.strings.notification_update_error, failed)) - setContentText(errorMessage ?: context.stringResource(MR.strings.action_show_errors)) + setContentText(context.stringResource(MR.strings.action_show_errors)) setSmallIcon(R.drawable.ic_komikku) setColor(ContextCompat.getColor(context, R.color.ic_launcher)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateSyncResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateSyncResult.kt deleted file mode 100644 index 9867d748b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateSyncResult.kt +++ /dev/null @@ -1,20 +0,0 @@ -package eu.kanade.tachiyomi.data.library - -/** - * Holds a sync failure message to be reported by [LibraryUpdateJob] when a library update - * was chained to run after [eu.kanade.tachiyomi.data.sync.SyncDataJob]. - */ -object LibraryUpdateSyncResult { - @Volatile - private var pendingError: String? = null - - fun setFailure(message: String) { - pendingError = message - } - - fun consumeFailure(): String? { - val error = pendingError - pendingError = null - return error - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt index c17df974e..b1bef64af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataJob.kt @@ -14,18 +14,14 @@ import androidx.work.WorkQuery import androidx.work.WorkerParameters import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.SyncStatus -import eu.kanade.tachiyomi.data.library.LibraryUpdateSyncResult import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.setForegroundSafely import eu.kanade.tachiyomi.util.system.workManager -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.runBlocking import logcat.LogPriority -import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.system.logcat -import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.TimeUnit @@ -57,22 +53,12 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters) setForegroundSafely() return try { - when (val result = SyncManager(context).syncData()) { - SyncDataResult.Success -> Result.success() - is SyncDataResult.Failure -> { - handleSyncFailure(result.message, result.errorAlreadyNotified) - Result.success() - } - } - } catch (e: CancellationException) { - throw e + SyncManager(context).syncData() + Result.success() } catch (e: Exception) { logcat(LogPriority.ERROR, e) - handleSyncFailure( - e.message ?: context.stringResource(MR.strings.unknown_error), - errorAlreadyNotified = false, - ) - Result.success() + notifier.showSyncError(e.message) + Result.success() // try again next time } finally { // KMK --> syncStatus.stop() @@ -92,19 +78,10 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters) ) } - private fun handleSyncFailure(message: String, errorAlreadyNotified: Boolean) { - if (tags.contains(TAG_BEFORE_LIBRARY_UPDATE)) { - LibraryUpdateSyncResult.setFailure(message) - } else if (!errorAlreadyNotified) { - notifier.showSyncError(message) - } - } - companion object { private const val TAG_JOB = "SyncDataJob" private const val TAG_AUTO = "$TAG_JOB:auto" const val TAG_MANUAL = "$TAG_JOB:manual" - const val TAG_BEFORE_LIBRARY_UPDATE = "$TAG_JOB:before-library-update" fun isRunning(context: Context): Boolean { return context.workManager.isRunning(TAG_JOB) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataResult.kt deleted file mode 100644 index 3c1d50292..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncDataResult.kt +++ /dev/null @@ -1,28 +0,0 @@ -package eu.kanade.tachiyomi.data.sync - -/** - * Captures the most recent sync failure reported by a [eu.kanade.tachiyomi.data.sync.service.SyncService] - * implementation so [SyncManager] can propagate the message without losing context. - */ -internal object SyncFailureState { - private var failure: Pair? = null - - fun report(message: String, errorAlreadyNotified: Boolean = true) { - failure = message to errorAlreadyNotified - } - - fun consume(): Pair? = failure.also { failure = null } -} - -sealed interface SyncDataResult { - data object Success : SyncDataResult - - /** - * @param message Human-readable failure reason. - * @param errorAlreadyNotified `true` when a sync service already showed a sync error notification. - */ - data class Failure( - val message: String, - val errorAlreadyNotified: Boolean = false, - ) : SyncDataResult -} 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 a4727a50c..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 @@ -19,9 +19,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf import logcat.LogPriority import logcat.logcat -import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.system.logcat -import tachiyomi.i18n.MR import tachiyomi.data.Chapters import tachiyomi.data.DatabaseHandler import tachiyomi.data.manga.MangaMapper.mapManga @@ -74,9 +72,7 @@ class SyncManager( * This function retrieves local data (favorites, manga, extensions, and categories) * from the database using the BackupManager, then synchronizes the data with a sync service. */ - suspend fun syncData(): SyncDataResult { - SyncFailureState.consume() - + suspend fun syncData() { // Reset isSyncing in case it was left over or failed syncing during restore. handler.await(inTransaction = true) { mangasQueries.resetIsSyncing() @@ -157,22 +153,12 @@ class SyncManager( } } - if (syncService == null) { - return SyncDataResult.Failure( - message = context.stringResource(MR.strings.sync_failed_generic), - ) - } - - val remoteBackup = syncService.doSync(syncData) + val remoteBackup = syncService?.doSync(syncData) if (remoteBackup == null) { - logcat(LogPriority.DEBUG) { "Skip restore due to sync failure" } - val (message, errorAlreadyNotified) = SyncFailureState.consume() - ?: (context.stringResource(MR.strings.sync_failed_generic) to false) - return SyncDataResult.Failure( - message = message, - errorAlreadyNotified = errorAlreadyNotified, - ) + logcat(LogPriority.DEBUG) { "Skip restore due to network issues" } + // should we call showSyncError? + return } if (remoteBackup === syncData.backup) { @@ -180,14 +166,13 @@ class SyncManager( logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" } syncPreferences.lastSyncTimestamp().set(Date().time) notifier.showSyncSuccess("Sync completed successfully") - return SyncDataResult.Success + return } // Stop the sync early if the remote backup is null or empty if (remoteBackup.backupManga.isEmpty()) { - val message = "No data found on remote server." - notifier.showSyncError(message) - return SyncDataResult.Failure(message = message, errorAlreadyNotified = true) + notifier.showSyncError("No data found on remote server.") + return } // Check if it's first sync based on lastSyncTimestamp @@ -195,7 +180,7 @@ class SyncManager( // It's first sync no need to restore data. (just update remote data) syncPreferences.lastSyncTimestamp().set(Date().time) notifier.showSyncSuccess("Updated remote data successfully") - return SyncDataResult.Success + return } val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup) @@ -223,7 +208,7 @@ class SyncManager( // update the sync timestamp syncPreferences.lastSyncTimestamp().set(Date().time) notifier.showSyncSuccess("Sync completed successfully") - return SyncDataResult.Success + return } val backupUri = writeSyncDataToCache(context, newSyncData) @@ -243,12 +228,8 @@ class SyncManager( // update the sync timestamp syncPreferences.lastSyncTimestamp().set(Date().time) - return SyncDataResult.Success } else { logcat(LogPriority.ERROR) { "Failed to write sync data to file" } - return SyncDataResult.Failure( - message = context.stringResource(MR.strings.sync_failed_generic), - ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt index 6a34c0329..0393649a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt @@ -18,8 +18,6 @@ import com.google.api.services.drive.DriveScopes import com.google.api.services.drive.model.File import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.backup.models.Backup -import eu.kanade.tachiyomi.data.sync.SyncFailureState -import eu.kanade.tachiyomi.data.sync.SyncNotifier import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf @@ -65,8 +63,6 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync private val googleDriveService = GoogleDriveService(context) - private val notifier = SyncNotifier(context) - private val protoBuf: ProtoBuf = Injekt.get() override suspend fun doSync(syncData: SyncData): Backup? { @@ -100,10 +96,7 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync pushSyncData(syncData) return syncData.backup } catch (e: Exception) { - val message = e.message ?: context.stringResource(MR.strings.unknown_error) - logcat(LogPriority.ERROR, "SyncService") { "Error syncing: $message" } - notifier.showSyncError(message) - SyncFailureState.report(message) + logcat(LogPriority.ERROR, "SyncService") { "Error syncing: ${e.message}" } return null } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt index 0667085b8..3db50c859 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.sync.service import android.content.Context import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.backup.models.Backup -import eu.kanade.tachiyomi.data.sync.SyncFailureState import eu.kanade.tachiyomi.data.sync.SyncNotifier import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -88,11 +87,10 @@ class SyncYomiSyncService( if (success) { reportSyncEvent(SyncEventStatus.SYNC_SUCCESS) } else { - val message = "Failed to push sync data" - reportSyncEvent(SyncEventStatus.SYNC_FAILED, message) - notifier.showSyncError(message) - SyncFailureState.report(message) + reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data") + // KMK --> return null + // KMK <-- } return finalSyncData.backup @@ -101,11 +99,9 @@ class SyncYomiSyncService( reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message) throw e } - val message = e.message ?: context.stringResource(MR.strings.unknown_error) - logcat(LogPriority.ERROR) { "Error syncing: $message" } - notifier.showSyncError(message) - SyncFailureState.report(message) - reportSyncEvent(SyncEventStatus.SYNC_ERROR, message) + logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" } + notifier.showSyncError(e.message) + reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message) return null } } 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 77a2f0aa7..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 @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.sync.service import android.content.Context import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.backup.models.Backup -import eu.kanade.tachiyomi.data.sync.SyncFailureState import eu.kanade.tachiyomi.data.sync.SyncNotifier import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.PUT @@ -118,9 +117,7 @@ class WebDavSyncService( return finalSyncData.backup } catch (e: Exception) { xLogE("WebDAV sync error:", e) - val message = e.message ?: context.stringResource(MR.strings.unknown_error) - notifier.showSyncError(message) - SyncFailureState.report(message) + notifier.showSyncError(e.message) return null } } @@ -185,7 +182,6 @@ class WebDavSyncService( "Please retry syncing or check your WebDAV folder." xLogW(message) notifier.showSyncError(message) - SyncFailureState.report(message) } else -> { val bodyStr = response.body.string() diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index ff5b40419..a1466b4f4 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -923,8 +923,6 @@ Chapter %1$s and %2$d more Chapters %1$s %1$d update(s) failed - Library update skipped: %1$s - Could not sync with the remote service Tap to learn more Failed to update cover Please add the entry to your library before doing this From 582ea3e60d93b498c5ca46bbbb450a8b89d41848 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Wed, 27 May 2026 14:42:44 +0700 Subject: [PATCH 34/73] chore(agent): Add mandatory rules for AI agents covering Git, i18n, and formatting (#1662) * chore(agent): Add mandatory rules for AI agents covering Git, i18n, and formatting * Explicit path for strings resource Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Specify both strings.xml & plurals.xml Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Also exclude non-base local folders from i18n-kmk Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9b0d3c9bb..197429c5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,53 @@ Komikku is an Android manga reader (min SDK 26, target SDK 36, JVM 17 / Kotlin) --- +## Mandatory rules for AI agents + +**Read this section before every change.** These rules override shortcuts (e.g. copying nearby `MR` imports or only running `compileDebugKotlin`). + +### Git + +| Rule | Required behavior | +|------|-------------------| +| Branch | Create a **feature branch** for the task (`git checkout -b /`). | +| Commit | **OK** on a feature branch when work is ready. **Never** commit directly to `master` / `main` unless the user explicitly asks. | +| Push | **OK** to push the **current feature branch** when work is ready. **Never** push to `master` / `main` unless the user explicitly asks. | + +Before `git push`, confirm the current branch is not `master` or `main` (`git branch --show-current`). + +### Internationalization (strings) + +| String kind | Module | Resource class | Base folder only | +|-------------|--------|----------------|------------------| +| Komikku-only (new features, KMK UI, library-update errors, WebDAV, Discord, etc.) | `i18n-kmk/` | **`KMR`** | `i18n-kmk/src/commonMain/moko-resources/base/` | +| Shared Mihon / upstream behavior | `i18n/` | **`MR`** | `i18n/src/commonMain/moko-resources/base/` | +| TachiyomiSY-only | `i18n-sy/` | **`SYMR`** | `i18n-sy/src/commonMain/moko-resources/base/` | + +**Hard rules:** + +- **Never** add Komikku-specific strings to `i18n/` or `i18n-sy/`. +- **Never** edit non-`base` locale `strings.xml` or `plurals.xml` files in `i18n-kmk/`, `i18n/`, or `i18n-sy/` (Weblate owns translations). +- Import: `import tachiyomi.i18n.kmk.KMR` for Komikku strings. +- If a change is inside `// KMK -->` … `// KMK <--` or adds Komikku-only behavior, default to **`KMR` + `i18n-kmk`**. + +**Self-check before finishing:** `git diff` must not add new `` or `` entries under non-`base` locales in `i18n-kmk/src/`, `i18n/src/`, or `i18n-sy/src/`. + +### Formatting & build verification + +**“Build passes” is not enough.** After Kotlin/XML edits, run **in this order** before marking work complete: + +```bash +./gradlew spotlessApply # fix formatting +./gradlew spotlessCheck # must pass (same as CI) +./gradlew assembleDebug # or :app:compileDebugKotlin for a faster compile-only check +``` + +- **Do not** skip `spotlessCheck` when verifying changes. +- If `spotlessCheck` fails, run `spotlessApply` and re-run `spotlessCheck`. +- On Cloud VM, export `ANDROID_HOME` and `JAVA_HOME` first (see [Cursor Cloud](#cursor-cloud-specific-instructions)). + +--- + ## Module layout | Module | Purpose | @@ -52,11 +99,13 @@ Example: `DeepLinkScreen` + `DeepLinkScreenModel` in `app/src/main/java/eu/kanad ## Komikku-specific work -- New user-facing strings: **`KMR`** in `i18n-kmk/src/commonMain/moko-resources/base/`. Shared/upstream: **`MR`** (`i18n`). SY-only: **`SYMR`** (`i18n-sy`). +- **Strings:** see [Mandatory rules – Internationalization](#mandatory-rules-for-ai-agents). Summary: Komikku → **`KMR`** / `i18n-kmk/…/base/` only. - Do not edit locale `strings.xml` in `i18n/` or `i18n-sy/` except when syncing upstream; translations via [Weblate](https://hosted.weblate.org/engage/komikku-app/). - Komikku code/DI: search `// KMK` (e.g. `KMKDomainModule`, `HideCategory`, library-update errors). - Prefs: `eu.kanade.domain.*.service.*Preferences` (e.g. `SourcePreferences.relatedMangas()`). +**Examples (Komikku → `i18n-kmk`, not `i18n`):** library update error UI, sync-before-update messages, WebDAV/Discord settings, updater notifications, `mihon/feature/*` Komikku screens. + --- ## Extensions & sources @@ -81,7 +130,8 @@ Gradle `-P` flags (`buildSrc/.../BuildConfig.kt`): | `include-dependency-info` | Dependency metadata in APK | ```bash -./gradlew spotlessApply # format (CI: spotlessCheck) +./gradlew spotlessApply # format (run before spotlessCheck) +./gradlew spotlessCheck # REQUIRED before considering work done (CI gate) ./gradlew assemblePreview # main CI/dev APK ./gradlew assemblePreview -Pinclude-telemetry -Penable-updater # full upstream CI build ./gradlew testReleaseUnitTest # CI unit tests (or ./gradlew test for all modules) @@ -89,6 +139,8 @@ Gradle `-P` flags (`buildSrc/.../BuildConfig.kt`): ./gradlew :data:generateSqlDelightInterface # after .sq / .sqm changes ``` +**Agent verification checklist (minimum):** `spotlessApply` → `spotlessCheck` → `assembleDebug` (or `compileDebugKotlin` only if the user asked for a quick compile check—but still run Spotless). + JDK **17**. --- @@ -116,7 +168,7 @@ Package roots: `eu.kanade.tachiyomi.*` (legacy UI), `tachiyomi.*` (domain/data), ## Conventions - **Logging** – Prefer `xLogE()` / `xLog()` helpers from `exh.log` for Komikku code, Mihon uses `logcat { }` from `tachiyomi.core.common.util.system`. Avoid raw `android.util.Log`. -- **Formatting** – Spotless + ktlint (`buildSrc/.../mihon.code.lint.gradle.kts`). +- **Formatting** – Spotless + ktlint (`buildSrc/.../mihon.code.lint.gradle.kts`). Agents **must** run `spotlessApply` and `spotlessCheck` (see [Mandatory rules](#mandatory-rules-for-ai-agents)). - **Fork edits** – New Komikku features inside `// KMK` islands; keep `// SY` / `// EXH` blocks intact when merging upstream. --- @@ -149,8 +201,8 @@ export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 | Task | Command | |------|---------| -| Format check (lint) | `./gradlew spotlessCheck` | -| Auto-fix formatting | `./gradlew spotlessApply` | +| **Required format fix** | `./gradlew spotlessApply` (run first after code edits) | +| **Required format gate** | `./gradlew spotlessCheck` (must pass before task is done) | | Debug APK build | `./gradlew assembleDebug` | | Preview APK build (CI) | `./gradlew assemblePreview` | | Unit tests (CI) | `./gradlew testReleaseUnitTest` | From 3b06366fd979e7983cd42e3e092608411b70cff3 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Thu, 11 Jun 2026 03:06:27 +0700 Subject: [PATCH 35/73] chore(download): disable download URL hash inclusion by default (#1677) --- .../tachiyomi/domain/download/service/DownloadPreferences.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt index 9d2a481a1..5353c345a 100644 --- a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt @@ -42,7 +42,7 @@ class DownloadPreferences( fun parallelPageLimit() = preferenceStore.getInt("download_parallel_page_limit", 5) // SY --> - fun includeChapterUrlHash() = preferenceStore.getBoolean("download_include_chapter_url_hash", true) + fun includeChapterUrlHash() = preferenceStore.getBoolean("download_include_chapter_url_hash", false) // SY <-- // KMK --> From 612651a07608b4ac34291e3b40c782c99d5726e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:18:55 +0700 Subject: [PATCH 36/73] chore(deps): update github actions (#1512) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build_benchmark.yml | 8 ++++---- .github/workflows/build_dispatch_preview.yml | 4 ++-- .github/workflows/build_preview.yml | 10 +++++----- .github/workflows/build_pull_request.yml | 10 +++++----- .github/workflows/build_push.yml | 6 +++--- .github/workflows/build_release.yml | 10 +++++----- .github/workflows/codeberg_mirror.yml | 2 +- .github/workflows/delete_merged_branch.yml | 2 +- .github/workflows/update_website.yml | 2 +- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build_benchmark.yml b/.github/workflows/build_benchmark.yml index 4f06bcfa2..b750e964c 100644 --- a/.github/workflows/build_benchmark.yml +++ b/.github/workflows/build_benchmark.yml @@ -29,7 +29,7 @@ jobs: echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT - name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }}) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 @@ -57,18 +57,18 @@ jobs: needs: prepare-build steps: - name: Clone Repository (${{ needs.prepare-build.outputs.TAG_NAME }}) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: java-version: 17 distribution: temurin - name: Set up gradle - uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5 + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 - name: Check code format run: ./gradlew spotlessCheck diff --git a/.github/workflows/build_dispatch_preview.yml b/.github/workflows/build_dispatch_preview.yml index 8b34d254b..700abaac2 100644 --- a/.github/workflows/build_dispatch_preview.yml +++ b/.github/workflows/build_dispatch_preview.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 @@ -53,7 +53,7 @@ jobs: uses: tj-actions/branch-names@5250492686b253f06fa55861556d1027b067aeb5 # v9.0.2 - name: Invoke workflow in preview repo - uses: benc-uk/workflow-dispatch@a54f9d194fed472732282ed1597dc4909e4b4080 # v1.3.0 + uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2 with: workflow: build_app.yml repo: komikku-app/komikku-preview diff --git a/.github/workflows/build_preview.yml b/.github/workflows/build_preview.yml index 449083eb8..3c872bdaf 100644 --- a/.github/workflows/build_preview.yml +++ b/.github/workflows/build_preview.yml @@ -43,7 +43,7 @@ jobs: echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT - name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }}) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 @@ -119,12 +119,12 @@ jobs: if: github.ref == 'refs/heads/master' steps: - name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }} - ${{ needs.prepare-build.outputs.TAG_NAME }}) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: java-version: 17 distribution: temurin @@ -144,7 +144,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5 + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 - name: Check code format run: ./gradlew spotlessCheck @@ -195,7 +195,7 @@ jobs: mv app/build/outputs/apk/${{ needs.prepare-build.outputs.ARTIFACTS_PREFIX }}-x86_64-preview-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.TAG_NAME }}.apk - name: Create release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: tag_name: ${{ needs.prepare-build.outputs.TAG_NAME }} name: Komikku Preview ${{ needs.prepare-build.outputs.TAG_NAME }} (${{ needs.prepare-build.outputs.VERSION_TAG }}) diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index f2f45282e..a84978f92 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -30,18 +30,18 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Dependency Review - uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1 + uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 - name: Set up JDK - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: java-version: 17 distribution: temurin @@ -63,7 +63,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5 + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 - name: Check code format run: ./gradlew spotlessCheck diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 1386be0b6..15abcc6c9 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -27,12 +27,12 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: java-version: 17 distribution: temurin @@ -52,7 +52,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5 + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 - name: Check code format run: ./gradlew spotlessCheck diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 223ba00fe..e76a7ae27 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -31,7 +31,7 @@ jobs: echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT - name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }}) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 @@ -87,12 +87,12 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') steps: - name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }}) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: java-version: 17 distribution: temurin @@ -112,7 +112,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5 + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 - name: Check code format run: ./gradlew spotlessCheck @@ -169,7 +169,7 @@ jobs: mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.VERSION_TAG }}.apk - name: Create release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: tag_name: ${{ needs.prepare-build.outputs.VERSION_TAG }} name: Komikku ${{ needs.prepare-build.outputs.VERSION_TAG }} diff --git a/.github/workflows/codeberg_mirror.yml b/.github/workflows/codeberg_mirror.yml index e1532f75c..c7de17fb0 100644 --- a/.github/workflows/codeberg_mirror.yml +++ b/.github/workflows/codeberg_mirror.yml @@ -13,7 +13,7 @@ jobs: if: github.repository == 'komikku-app/komikku' runs-on: 'ubuntu-24.04' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Setup SSH key diff --git a/.github/workflows/delete_merged_branch.yml b/.github/workflows/delete_merged_branch.yml index 9f7323925..d530a9ff5 100644 --- a/.github/workflows/delete_merged_branch.yml +++ b/.github/workflows/delete_merged_branch.yml @@ -13,7 +13,7 @@ jobs: if: github.event.pull_request.merged == true && github.repository == github.event.pull_request.head.repo.full_name steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Delete branch run: | diff --git a/.github/workflows/update_website.yml b/.github/workflows/update_website.yml index c18f8c377..69171e9d8 100644 --- a/.github/workflows/update_website.yml +++ b/.github/workflows/update_website.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Update website on release - uses: benc-uk/workflow-dispatch@a54f9d194fed472732282ed1597dc4909e4b4080 # v1.3.0 + uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2 with: workflow: Deploy repo: komikku-app/komikku-app.github.io From 9761f3394828f90fbb55adeaa0c1523c03cd42b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:19:05 +0700 Subject: [PATCH 37/73] fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20260428-2.0.0 (#1513) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/sy.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/sy.versions.toml b/gradle/sy.versions.toml index ab714cbcd..ad62520d2 100644 --- a/gradle/sy.versions.toml +++ b/gradle/sy.versions.toml @@ -12,6 +12,6 @@ sqlcipher = "net.zetetic:sqlcipher-android:4.13.0" exifinterface = "androidx.exifinterface:exifinterface:1.4.2" -google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev20251210-2.0.0" +google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev20260428-2.0.0" zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0" From 40f1391775647480e6e2349f1cea5efb27a74c4e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:19:13 +0700 Subject: [PATCH 38/73] fix(deps): update dependency io.coil-kt.coil3:coil-bom to v3.5.0 (#1520) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dfd1f8a65..e55e0f4d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,7 @@ preferencektx = "androidx.preference:preference-ktx:1.2.1" injekt = "com.github.mihonapp:injekt:91edab2317" -coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.3.0" } +coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.5.0" } coil-core = { module = "io.coil-kt.coil3:coil" } coil-gif = { module = "io.coil-kt.coil3:coil-gif" } coil-compose = { module = "io.coil-kt.coil3:coil-compose" } From bd13620d1fde6bd275626e4273d4b05389f74d6c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:19:23 +0700 Subject: [PATCH 39/73] fix(deps): update dependency io.kotest:kotest-assertions-core to v6.2.1 (#1522) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e55e0f4d4..aee70c596 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -94,7 +94,7 @@ sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } -kotest-assertions = "io.kotest:kotest-assertions-core:6.1.3" +kotest-assertions = "io.kotest:kotest-assertions-core:6.2.1" mockk = "io.mockk:mockk:1.14.9" voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } From 0ba6598c3f873e9e93d36271156c2bc8c4104ba5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:19:41 +0700 Subject: [PATCH 40/73] fix(deps): update firebase.bom to v34.15.0 (#1526) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aee70c596..888bf11de 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ sqlite = "2.6.2" voyager = "1.1.0-beta03" spotless = "8.2.1" ktlint-core = "1.8.0" -firebase-bom = "34.9.0" +firebase-bom = "34.15.0" markdown = "0.39.2" junit = "6.0.3" materialKolor = "5.0.0-alpha06" From 72a3bb6f585ff35df46e70c6f9e3ab98f8db7c57 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:19:50 +0700 Subject: [PATCH 41/73] fix(deps): update compose.bom to v2026.06.00 (#1546) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/compose.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 5fb461b74..fe01a3b74 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,5 +1,5 @@ [versions] -compose-bom = "2026.02.00" +compose-bom = "2026.06.00" [libraries] activity = "androidx.activity:activity-compose:1.12.4" From e9bd1fc47790a50c2f93450c7ef6e0a7d10cee2d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:20:00 +0700 Subject: [PATCH 42/73] fix(deps): update dependency com.materialkolor:material-kolor to v5.0.0-alpha07 (#1551) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 888bf11de..d8d871326 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ ktlint-core = "1.8.0" firebase-bom = "34.15.0" markdown = "0.39.2" junit = "6.0.3" -materialKolor = "5.0.0-alpha06" +materialKolor = "5.0.0-alpha07" [libraries] desugar = "com.android.tools:desugar_jdk_libs:2.1.5" From 3de5da42d379466f5ba47d855ede4f2215bddc26 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:20:09 +0700 Subject: [PATCH 43/73] fix(deps): update spotless to v8.7.0 (#1552) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d8d871326..368b9f59e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ shizuku_version = "13.1.5" sqldelight = "2.2.1" sqlite = "2.6.2" voyager = "1.1.0-beta03" -spotless = "8.2.1" +spotless = "8.7.0" ktlint-core = "1.8.0" firebase-bom = "34.15.0" markdown = "0.39.2" From 1ffeebc9a1bab0cf4ce463617aa3b2d562cc2dc7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:21:07 +0700 Subject: [PATCH 44/73] chore(deps): update github actions (#1524) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build_benchmark.yml | 8 ++++---- .github/workflows/build_dispatch_preview.yml | 2 +- .github/workflows/build_preview.yml | 12 ++++++------ .github/workflows/build_pull_request.yml | 14 +++++++------- .github/workflows/build_push.yml | 10 +++++----- .github/workflows/build_release.yml | 14 +++++++------- .github/workflows/codeberg_mirror.yml | 2 +- .github/workflows/delete_merged_branch.yml | 2 +- .github/workflows/pr_label.yml | 2 +- 9 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build_benchmark.yml b/.github/workflows/build_benchmark.yml index b750e964c..f41e15132 100644 --- a/.github/workflows/build_benchmark.yml +++ b/.github/workflows/build_benchmark.yml @@ -29,7 +29,7 @@ jobs: echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT - name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }}) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -57,7 +57,7 @@ jobs: needs: prepare-build steps: - name: Clone Repository (${{ needs.prepare-build.outputs.TAG_NAME }}) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -68,7 +68,7 @@ jobs: distribution: temurin - name: Set up gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -101,7 +101,7 @@ jobs: mv app/build/outputs/apk/${{ needs.prepare-build.outputs.ARTIFACTS_PREFIX }}-x86_64-benchmark-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.TAG_NAME }}.apk - name: Upload artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: path: "**/*.apk" retention-days: 30 diff --git a/.github/workflows/build_dispatch_preview.yml b/.github/workflows/build_dispatch_preview.yml index 700abaac2..92ea4eb94 100644 --- a/.github/workflows/build_dispatch_preview.yml +++ b/.github/workflows/build_dispatch_preview.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/build_preview.yml b/.github/workflows/build_preview.yml index 3c872bdaf..9f22ada1d 100644 --- a/.github/workflows/build_preview.yml +++ b/.github/workflows/build_preview.yml @@ -43,7 +43,7 @@ jobs: echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT - name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }}) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -119,7 +119,7 @@ jobs: if: github.ref == 'refs/heads/master' steps: - name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }} - ${{ needs.prepare-build.outputs.TAG_NAME }}) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -144,7 +144,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -156,7 +156,7 @@ jobs: run: ./gradlew testReleaseUnitTest - name: Upload artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: path: "**/*.apk" retention-days: 1 @@ -168,7 +168,7 @@ jobs: - build-app steps: - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: merge-multiple: true @@ -195,7 +195,7 @@ jobs: mv app/build/outputs/apk/${{ needs.prepare-build.outputs.ARTIFACTS_PREFIX }}-x86_64-preview-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.TAG_NAME }}.apk - name: Create release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: tag_name: ${{ needs.prepare-build.outputs.TAG_NAME }} name: Komikku Preview ${{ needs.prepare-build.outputs.TAG_NAME }} (${{ needs.prepare-build.outputs.VERSION_TAG }}) diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index a84978f92..eb92261ff 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -30,15 +30,15 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Dependency Review - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - name: Set up JDK uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 @@ -63,7 +63,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -82,7 +82,7 @@ jobs: - name: Upload test report if: steps.unit_tests.outcome == 'failure' - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-report-${{ github.sha }} path: app/build/reports/tests/testReleaseUnitTest @@ -119,13 +119,13 @@ jobs: mv app/build/outputs/apk/preview/app-universal-preview${{ steps.signed_filename.outputs.SIGNED_TRAIL }}.apk Komikku-$version_tag-r$commit_count.apk - name: Upload APK - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk path: ./*.apk - name: Upload mapping - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: mapping-${{ github.sha }} path: app/build/outputs/mapping/preview diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 15abcc6c9..415d02cec 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -52,7 +52,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -66,7 +66,7 @@ jobs: - name: Upload test report if: steps.unit_tests.outcome == 'failure' - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-report-${{ github.sha }} path: app/build/reports/tests/testReleaseUnitTest @@ -95,13 +95,13 @@ jobs: mv app/build/outputs/apk/preview/app-universal-preview-signed.apk Komikku-$version_tag-r$commit_count.apk - name: Upload APK - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk path: ./*.apk - name: Upload mapping - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: mapping-${{ github.sha }} path: app/build/outputs/mapping/preview diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index e76a7ae27..958c1a6ed 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -31,7 +31,7 @@ jobs: echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT - name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }}) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -87,7 +87,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') steps: - name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }}) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -112,7 +112,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -124,13 +124,13 @@ jobs: run: ./gradlew testReleaseUnitTest - name: Upload artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: path: "**/*.apk" retention-days: 1 - name: Upload mapping - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: mapping-${{ github.sha }} path: app/build/outputs/mapping/release @@ -142,7 +142,7 @@ jobs: - build-app steps: - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: merge-multiple: true @@ -169,7 +169,7 @@ jobs: mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.VERSION_TAG }}.apk - name: Create release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: tag_name: ${{ needs.prepare-build.outputs.VERSION_TAG }} name: Komikku ${{ needs.prepare-build.outputs.VERSION_TAG }} diff --git a/.github/workflows/codeberg_mirror.yml b/.github/workflows/codeberg_mirror.yml index c7de17fb0..59ff40c15 100644 --- a/.github/workflows/codeberg_mirror.yml +++ b/.github/workflows/codeberg_mirror.yml @@ -13,7 +13,7 @@ jobs: if: github.repository == 'komikku-app/komikku' runs-on: 'ubuntu-24.04' steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Setup SSH key diff --git a/.github/workflows/delete_merged_branch.yml b/.github/workflows/delete_merged_branch.yml index d530a9ff5..6cbf2a266 100644 --- a/.github/workflows/delete_merged_branch.yml +++ b/.github/workflows/delete_merged_branch.yml @@ -13,7 +13,7 @@ jobs: if: github.event.pull_request.merged == true && github.repository == github.event.pull_request.head.repo.full_name steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Delete branch run: | diff --git a/.github/workflows/pr_label.yml b/.github/workflows/pr_label.yml index a63847a87..5e3ba18b2 100644 --- a/.github/workflows/pr_label.yml +++ b/.github/workflows/pr_label.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Check PR and Add Label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const prAuthor = context.payload.pull_request.user.login; From 25b8eb889a7a02d02af3540b593ee4d831d40908 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:39:49 +0700 Subject: [PATCH 45/73] chore(deps): update plugin firebase-crashlytics to v3.0.7 (#1700) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 368b9f59e..473cabe98 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ google-services = { id = "com.google.gms.google-services", version = "4.4.4" } aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlib_version" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" } -firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.6" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.7" } [bundles] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] From fdfd4582cb0a1b64fcb48a11e54aced9d3e5667e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:39:57 +0700 Subject: [PATCH 46/73] fix(deps): update dependency androidx.work:work-runtime to v2.11.2 (#1701) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/androidx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 95f690865..b3eed4e90 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -21,7 +21,7 @@ lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle_version" } lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" } -workmanager = "androidx.work:work-runtime:2.11.1" +workmanager = "androidx.work:work-runtime:2.11.2" paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" } paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" } From 583d649d20fd1d0381a8d530109f977923e60bb0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:31:51 +0700 Subject: [PATCH 47/73] fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20260624-2.0.0 (#1702) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/sy.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/sy.versions.toml b/gradle/sy.versions.toml index ad62520d2..0d7886283 100644 --- a/gradle/sy.versions.toml +++ b/gradle/sy.versions.toml @@ -12,6 +12,6 @@ sqlcipher = "net.zetetic:sqlcipher-android:4.13.0" exifinterface = "androidx.exifinterface:exifinterface:1.4.2" -google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev20260428-2.0.0" +google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev20260624-2.0.0" zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0" From 5048e57e7616098c682dc95de5249440d98cd62f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:32:00 +0700 Subject: [PATCH 48/73] fix(deps): update dependency com.mohamedrejeb.richeditor:richeditor-compose to v1.0.0-rc14 (#1703) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 473cabe98..760d1cf76 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,7 +55,7 @@ image-decoder = "com.github.tachiyomiorg:image-decoder:41c059e540" natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" -richeditor-compose = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc13" +richeditor-compose = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc14" material = "com.google.android.material:material:1.13.0" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" From 5c84266bc72dcabf72d02421076c9216a71f0f29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:32:10 +0700 Subject: [PATCH 49/73] fix(deps): update dependency io.mockk:mockk to v1.14.11 (#1704) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 760d1cf76..ba437b579 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -95,7 +95,7 @@ sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } kotest-assertions = "io.kotest:kotest-assertions-core:6.2.1" -mockk = "io.mockk:mockk:1.14.9" +mockk = "io.mockk:mockk:1.14.11" voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } From 7b073fed0cebafde5fcd94eb31b8151834c53c50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:32:18 +0700 Subject: [PATCH 50/73] fix(deps): update dependency org.jsoup:jsoup to v1.22.2 (#1705) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba437b579..4e5a49822 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ conscrypt-android = "org.conscrypt:conscrypt-android:2.5.3" quickjs-android = { group = "com.github.zhanghai.quickjs-java", name = "quickjs-android", version = "547f5b1597" } -jsoup = "org.jsoup:jsoup:1.22.1" +jsoup = "org.jsoup:jsoup:1.22.2" disklrucache = "com.jakewharton:disklrucache:2.0.2" unifile = "com.github.komikku-app:UniFile:084a54140a" From e95e46354328938eb71192e2b986cce070060103 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:32:28 +0700 Subject: [PATCH 51/73] fix(deps): update moko to v0.26.4 (#1706) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e5a49822..6fdeddee7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] aboutlib_version = "13.2.1" leakcanary = "2.14" -moko = "0.26.0" +moko = "0.26.4" okhttp_version = "5.3.2" shizuku_version = "13.1.5" sqldelight = "2.2.1" From e11cd536dcab5d2068ac8523ee56f80a56797dd0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:32:36 +0700 Subject: [PATCH 52/73] chore(deps): update plugin google-services to v4.5.0 (#1707) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6fdeddee7..57bac0ccd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -113,7 +113,7 @@ stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", materialKolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" } [plugins] -google-services = { id = "com.google.gms.google-services", version = "4.4.4" } +google-services = { id = "com.google.gms.google-services", version = "4.5.0" } aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlib_version" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" } From 118eeaf3ee96f3cb1bfab01371e7a8dd2f275d17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:32:43 +0700 Subject: [PATCH 53/73] fix(deps): update dependency androidx.activity:activity-compose to v1.13.0 (#1708) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/compose.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index fe01a3b74..14ce55fa6 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -2,7 +2,7 @@ compose-bom = "2026.06.00" [libraries] -activity = "androidx.activity:activity-compose:1.12.4" +activity = "androidx.activity:activity-compose:1.13.0" bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } foundation = { module = "androidx.compose.foundation:foundation" } animation = { module = "androidx.compose.animation:animation" } From 453afb3edb29ec5965856039f016e8ba7f286045 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:32:52 +0700 Subject: [PATCH 54/73] fix(deps): update dependency androidx.annotation:annotation to v1.10.0 (#1709) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/androidx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index b3eed4e90..9794cf08b 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -7,7 +7,7 @@ interpolator_version = "1.0.0" [libraries] gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" } -annotation = "androidx.annotation:annotation:1.9.1" +annotation = "androidx.annotation:annotation:1.10.0" appcompat = "androidx.appcompat:appcompat:1.7.1" biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05" constraintlayout = "androidx.constraintlayout:constraintlayout:2.2.1" From 4fd2625bf70dea4d369c7718b0bf30562969d4b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:02:29 +0700 Subject: [PATCH 55/73] chore(deps): update kotlin.version to v2.4.0 (#1710) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/kotlinx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 6e772ba01..440cedb76 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin_version = "2.3.10" +kotlin_version = "2.4.0" serialization_version = "1.10.0" xml_serialization_version = "0.91.3" From 40b19479af6a3b6f8f3bbd0d66840d925cb9ee7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:02:46 +0700 Subject: [PATCH 56/73] fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.11.0 (#1712) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/kotlinx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 440cedb76..b99e97d14 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -10,7 +10,7 @@ compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-grad immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.4.0" } -coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.10.2" } +coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.11.0" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" } From 3e07479b0e1f7f05bd5d916e541fb199136e1ab4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:02:55 +0700 Subject: [PATCH 57/73] fix(deps): update dependency androidx.compose:compose-bom to v2026.06.01 (#1713) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/compose.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 14ce55fa6..26e09a8ed 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,5 +1,5 @@ [versions] -compose-bom = "2026.06.00" +compose-bom = "2026.06.01" [libraries] activity = "androidx.activity:activity-compose:1.13.0" From 65d2bff8c7aaeb89e9d1fb71536e9b8ca324266f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:03:04 +0700 Subject: [PATCH 58/73] fix(deps): update dependency androidx.test.uiautomator:uiautomator to v2.4.0 (#1714) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/androidx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 9794cf08b..621d2ac30 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -31,7 +31,7 @@ interpolator = { group = "androidx.interpolator", name = "interpolator", version benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.4.1" test-ext = "androidx.test.ext:junit-ktx:1.3.0" test-espresso-core = "androidx.test.espresso:espresso-core:3.7.0" -test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0" +test-uiautomator = "androidx.test.uiautomator:uiautomator:2.4.0" [bundles] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] From 559a787d0fe2057319e474aea6d496d0b6cb61cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:03:13 +0700 Subject: [PATCH 59/73] fix(deps): update dependency app.cash.sqldelight:sqlite-3-38-dialect to v2.3.2 (#1717) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57bac0ccd..ec4216259 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -90,7 +90,7 @@ leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", versi sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions-jvm", version.ref = "sqldelight" } sqldelight-android-paging = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqldelight" } -sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version = "2.2.1" } +sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version = "2.3.2" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } From 16582cc50ed3570982ef52ab5f0755ea97d5638b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:03:28 +0700 Subject: [PATCH 60/73] fix(deps): update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.8.0 (#1718) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec4216259..c6c0d28ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ shizuku_version = "13.1.5" sqldelight = "2.2.1" sqlite = "2.6.2" voyager = "1.1.0-beta03" -spotless = "8.7.0" +spotless = "8.8.0" ktlint-core = "1.8.0" firebase-bom = "34.15.0" markdown = "0.39.2" From 86b60f08b1f98ec9bffcc4aca8580e3e419f5afc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:03:37 +0700 Subject: [PATCH 61/73] fix(deps): update dependency com.github.ben-manes:gradle-versions-plugin to v0.54.0 (#1719) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/sy.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/sy.versions.toml b/gradle/sy.versions.toml index 0d7886283..515fed135 100644 --- a/gradle/sy.versions.toml +++ b/gradle/sy.versions.toml @@ -6,7 +6,7 @@ xlog = "com.elvishew:xlog:1.11.1" ratingbar = "me.zhanghai.android.materialratingbar:library:1.4.0" composeRatingbar = "com.github.a914-gowtham:compose-ratingbar:1.3.12" -gradleversionsx = "com.github.ben-manes:gradle-versions-plugin:0.53.0" +gradleversionsx = "com.github.ben-manes:gradle-versions-plugin:0.54.0" sqlcipher = "net.zetetic:sqlcipher-android:4.13.0" From 3615dc23a979c6807b105a7bf7356dd117b2410b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:03:50 +0700 Subject: [PATCH 62/73] fix(deps): update dependency com.github.skydoves:colorpicker-compose to v1.2.0 (#1720) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/compose.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 26e09a8ed..fa180981a 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -17,4 +17,4 @@ material3-core = { module = "androidx.compose.material3:material3" } material-icons = { module = "androidx.compose.material:material-icons-extended" } glance = "androidx.glance:glance-appwidget:1.1.1" -colorpicker = {module = "com.github.skydoves:colorpicker-compose", version = "1.1.3"} +colorpicker = {module = "com.github.skydoves:colorpicker-compose", version = "1.2.0"} From 06107dba9bc17da1482e2dc37a77da5e27a3989e Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:01:56 +0600 Subject: [PATCH 63/73] Bump workflows JDK to 21 (mihonapp/mihon#3053) (cherry picked from commit ce5820b496dc6526b7afbf7344753c923802f737) --- .github/.java-version | 1 + .github/workflows/build_benchmark.yml | 2 +- .github/workflows/build_preview.yml | 2 +- .github/workflows/build_pull_request.yml | 2 +- .github/workflows/build_push.yml | 2 +- .github/workflows/build_release.yml | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 .github/.java-version diff --git a/.github/.java-version b/.github/.java-version new file mode 100644 index 000000000..aabe6ec39 --- /dev/null +++ b/.github/.java-version @@ -0,0 +1 @@ +21 diff --git a/.github/workflows/build_benchmark.yml b/.github/workflows/build_benchmark.yml index f41e15132..6f8216d6a 100644 --- a/.github/workflows/build_benchmark.yml +++ b/.github/workflows/build_benchmark.yml @@ -64,7 +64,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Set up gradle diff --git a/.github/workflows/build_preview.yml b/.github/workflows/build_preview.yml index 9f22ada1d..3541a7657 100644 --- a/.github/workflows/build_preview.yml +++ b/.github/workflows/build_preview.yml @@ -126,7 +126,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Write google-services.json diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index eb92261ff..c501815c1 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -43,7 +43,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Write google-services.json diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 415d02cec..e90e9f0c4 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -34,7 +34,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Write google-services.json diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 958c1a6ed..4c562f21d 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -94,7 +94,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Write google-services.json From 0f556ea9b5aef4ebe06dae62bec7614cca957eb0 Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:56:10 +0100 Subject: [PATCH 64/73] Address bundleOf deprecation (mihonapp/mihon#3073) (cherry picked from commit 33faa5a3562016a53d94b02aed69d95b10af5fc5) --- .../ui/browse/extension/details/SourcePreferencesScreen.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt index fc7811db8..003d69a69 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.os.bundleOf import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentManager @@ -182,7 +181,9 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { fun getInstance(sourceId: Long): SourcePreferencesFragment { return SourcePreferencesFragment().apply { - arguments = bundleOf(SOURCE_ID to sourceId) + arguments = Bundle().apply { + putLong(SOURCE_ID, sourceId) + } } } } From 21c1b857962d3fa716ecf0a54ab61ca910020661 Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:22:37 +0100 Subject: [PATCH 65/73] Add informative error for unapproved MAL titles (mihonapp/mihon#3155) MAL has a concept of "titles waiting for approval". These titles cannot be added to user lists, but they do show up on the website and crucially, in search results. However, trying to add such an "unapproved" title will return a 400 error response with the error "invalid_content". Previously, the awaitSuccess() call would mean the generic "HTTP 400" toast would be shown. Now, a dedicated informative error message is shown instead. (cherry picked from commit af4f17efc54a00d9abe20e0d0b701d60c3531369) --- CHANGELOG.md | 25 +++++++++++++++++++ .../data/track/myanimelist/MyAnimeListApi.kt | 21 ++++++++++++++-- .../myanimelist/MyAnimeListInterceptor.kt | 1 + 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 305497994..a6ab13892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,31 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - `Other` - for technical stuff. ## [Unreleased] +### Improved +- Show informative error when trying to add unapproved titles to list on MAL ([@MajorTanya](https://github.com/MajorTanya)) ([#3155](https://github.com/mihonapp/mihon/pull/3155)) + +## [v0.19.7] - 2026-03-23 +Same as v0.19.6 + +## [v0.19.6] - 2026-03-23 +### Fixed +- Fix app crashing when trying to add extension repo with existing signature ([@AntsyLich](https://github.com/AntsyLich)) ([`cb0e329`](https://github.com/mihonapp/mihon/commit/cb0e329)) +- Potentially fix 'database is locked' crash ([@AntsyLich](https://github.com/AntsyLich)) ([`f8e82b9`](https://github.com/mihonapp/mihon/commit/f8e82b9)) +- Fix occasional crash when mass installing/uninstalling extension using `PackageManager` ([@AntsyLich](https://github.com/AntsyLich)) ([`42daa3f`](https://github.com/mihonapp/mihon/commit/42daa3f)) +- Fix app crash on startup on some Android TV ([@AntsyLich](https://github.com/AntsyLich)) ([`b40f1d3`](https://github.com/mihonapp/mihon/commit/b40f1d3)) + +## [v0.19.5] - 2026-03-20 +### Changed +- Retry in reader now redownloads image ([@AntsyLich](https://github.com/AntsyLich)) ([#3089](https://github.com/mihonapp/mihon/pull/3089)) + +### Fixed +- Fix performance regression introduced in v0.19.4 ([@AntsyLich](https://github.com/AntsyLich)) ([#3082](https://github.com/mihonapp/mihon/pull/3082)) +- Fix duplicate key crash in duplicate detection ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3040](https://github.com/mihonapp/mihon/pull/3040)) +- Fix MangaUpdates HTTP 4XX errors ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3021](https://github.com/mihonapp/mihon/pull/3021)) +- Fix WebView JavaScript dialogs popup after screen is closed ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3041](https://github.com/mihonapp/mihon/pull/3041)) +- Fix extension actions disappearing after installing and uninstalling in same session ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3049](https://github.com/mihonapp/mihon/pull/3049)) + +## [v0.19.4] - 2026-02-25 ### Added - Automatically remove downloads on Suwayomi after reading, configurable via extension settings ([@cpiber](https://github.com/cpiber)) ([#2673](https://github.com/mihonapp/mihon/pull/2673)) - Display author & artist name in MAL search results ([@MajorTanya](https://github.com/MajorTanya)) ([#2833](https://github.com/mihonapp/mihon/pull/2833)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index d936a615a..147331dbf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -14,7 +14,9 @@ import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.PkceUtil @@ -124,8 +126,23 @@ class MyAnimeListApi( .put(formBodyBuilder.build()) .build() with(json) { - authClient.newCall(request) - .awaitSuccess() + val response = authClient + .newCall(request) + .await() + + if (!response.isSuccessful) { + if (response.body.string().contains("invalid_content")) { + // MAL returns unapproved titles in search but does not allow adding them to the list + // returns 400 with this body: {"message":"Invalid content","error":"invalid_content"} + // These unapproved titles cannot be filtered out in search and are also returned by the + // endpoint we use for id prefix search + throw MALTitleNotApproved() + } else { + throw HttpException(response.code) + } + } + + response .parseAs() .let { parseMangaItem(it, track) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 09d56f266..5620e0ad0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -80,5 +80,6 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor } } +class MALTitleNotApproved : IOException("MAL: This title can't be added because it is waiting for approval.") class MALTokenRefreshFailed : IOException("MAL: Failed to refresh account token") class MALTokenExpired : IOException("MAL: Login has expired") From da7ee11bb388237aee2de5b427b78d449f09faf8 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Wed, 3 Jun 2026 06:35:44 +0600 Subject: [PATCH 66/73] Change extension repo to extension store and add support for newer extension index format (#1744) PR: mihonapp/mihon#3349 Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Co-authored-by: Jobobby04 (cherry picked from commit b446d2a6b1844f24b2d4912ba8be8b4648549f41) (cherry picked from commit a0ae52671fe2ddab3883b99f437d521446a8539d) --- CHANGELOG.md | 18 ++ app/src/main/AndroidManifest.xml | 15 +- .../java/eu/kanade/domain/DomainModule.kt | 33 ++- .../interactor/GetExtensionLanguages.kt | 6 +- .../interactor/GetExtensionsByType.kt | 3 - .../extension/interactor/TrustExtension.kt | 10 +- .../eu/kanade/domain/sync/SyncPreferences.kt | 4 +- .../kanade/domain/sync/models/SyncSettings.kt | 2 +- .../browse/ExtensionDetailsScreen.kt | 11 +- .../presentation/browse/ExtensionsScreen.kt | 28 ++- .../settings/screen/SettingsBrowseScreen.kt | 14 +- .../browse/ExtensionReposScreenModel.kt | 205 ---------------- ...eposScreen.kt => ExtensionStoresScreen.kt} | 78 +++--- .../browse/ExtensionStoresScreenModel.kt | 223 ++++++++++++++++++ .../components/ExtensionReposDialogs.kt | 188 --------------- ...osContent.kt => ExtensionStoresContent.kt} | 90 +++---- .../components/ExtensionStoresDialogs.kt | 192 +++++++++++++++ ...eposScreen.kt => ExtensionStoresScreen.kt} | 62 ++--- .../screen/data/SyncSettingsSelector.kt | 4 +- .../data/backup/create/BackupCreator.kt | 16 +- .../data/backup/create/BackupOptions.kt | 14 +- .../creators/ExtensionRepoBackupCreator.kt | 17 -- .../creators/ExtensionStoresBackupCreator.kt | 17 ++ .../tachiyomi/data/backup/models/Backup.kt | 2 +- .../backup/models/BackupExtensionRepos.kt | 24 -- .../backup/models/BackupExtensionStore.kt | 28 +++ .../data/backup/restore/BackupRestorer.kt | 28 ++- .../data/backup/restore/RestoreOptions.kt | 14 +- .../restorers/ExtensionRepoRestorer.kt | 40 ---- .../restorers/ExtensionStoreRestorer.kt | 25 ++ .../kanade/tachiyomi/data/sync/SyncManager.kt | 8 +- .../tachiyomi/extension/ExtensionManager.kt | 10 +- .../tachiyomi/extension/api/ExtensionApi.kt | 129 +--------- .../extension/util/ExtensionLoader.kt | 32 +-- .../ui/browse/extension/ExtensionsScreen.kt | 2 +- .../ui/browse/extension/ExtensionsTab.kt | 6 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 13 +- .../migrations/DisabledRepoMigration.kt | 30 +++ .../core/migration/migrations/Migrations.kt | 3 + .../TrustExtensionRepositoryMigration.kt | 17 +- data/build.gradle.kts | 6 +- .../model/BaseNetworkExtensionStore.kt | 7 + .../extension/model/NetworkExtensionStore.kt | 114 +++++++++ .../extension/model/NetworkLegacyExtension.kt | 65 +++++ .../model/NetworkLegacyExtensionRepo.kt | 33 +++ .../ExtensionStoreRepositoryImpl.kt | 138 +++++++++++ .../service/ExtensionStoreService.kt | 106 +++++++++ .../repository/ExtensionRepoRepositoryImpl.kt | 93 -------- .../tachiyomi/data/extension_repos.sq | 57 ----- .../tachiyomi/data/extension_store.sq | 42 ++++ .../sqldelight/tachiyomi/migrations/46.sqm | 16 ++ .../tachiyomi/extension/model/Extension.kt | 17 +- .../extension/interactor/AddExtensionStore.kt | 11 + .../GetExtensionStoreCountAsFlow.kt | 9 + .../interactor/GetExtensionStores.kt | 13 + .../interactor/RemoveExtensionStore.kt | 11 + .../interactor/UpdateExtensionStores.kt | 11 + .../domain/extension/model/ExtensionStore.kt | 21 ++ .../repository/ExtensionStoreRepository.kt | 27 +++ .../exception/SaveExtensionRepoException.kt | 10 - .../interactor/CreateExtensionRepo.kt | 80 ------- .../interactor/DeleteExtensionRepo.kt | 11 - .../interactor/GetExtensionRepo.kt | 13 - .../interactor/GetExtensionRepoCount.kt | 9 - .../interactor/ReplaceExtensionRepo.kt | 12 - .../interactor/UpdateExtensionRepo.kt | 30 --- .../extensionrepo/model/ExtensionRepo.kt | 9 - .../repository/ExtensionRepoRepository.kt | 47 ---- .../extensionrepo/service/ExtensionRepoDto.kt | 27 --- .../service/ExtensionRepoService.kt | 36 --- .../moko-resources/base/strings.xml | 26 +- 71 files changed, 1463 insertions(+), 1305 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt rename app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/{ExtensionReposScreen.kt => ExtensionStoresScreen.kt} (53%) create mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt delete mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt rename app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/{ExtensionReposContent.kt => ExtensionStoresContent.kt} (71%) create mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt rename app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/{ExtensionReposScreen.kt => ExtensionStoresScreen.kt} (71%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt create mode 100644 app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt create mode 100644 data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt create mode 100644 data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt create mode 100644 data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt create mode 100644 data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt create mode 100644 data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt create mode 100644 data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt delete mode 100644 data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt delete mode 100644 data/src/main/sqldelight/tachiyomi/data/extension_repos.sq create mode 100644 data/src/main/sqldelight/tachiyomi/data/extension_store.sq create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/46.sqm rename {app => domain}/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt (86%) create mode 100644 domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt create mode 100644 domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt create mode 100644 domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt create mode 100644 domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt create mode 100644 domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt create mode 100644 domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt create mode 100644 domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt delete mode 100644 domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ab13892..8c9186091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,24 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - `Other` - for technical stuff. ## [Unreleased] +### Added +- Add support for `tachiyomix` extension index format ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349)) + +### Changed +- Change all reference of extension repo to extension store ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349)) + +### Fixed +- Add missing `outlineVariant` color to Nord theme ([@CompileConnected](https://github.com/CompileConnected)) ([#3184](https://github.com/mihonapp/mihon/pull/3184)) + +## [v0.19.9] - 2026-04-11 +### Fixed +- Regression with installing/updating extension ([@AntsyLich](https://github.com/AntsyLich)) + +## [v0.19.8] - 2026-04-11 +### Added +- Add dedicated "Support Us" screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3200](https://github.com/mihonapp/mihon/pull/3200)) +- Add a one time popup asking for donation from long time users ([@AntsyLich](https://github.com/AntsyLich)) ([#3203](https://github.com/mihonapp/mihon/pull/3203)) + ### Improved - Show informative error when trying to add unapproved titles to list on MAL ([@MajorTanya](https://github.com/MajorTanya)) ([#3155](https://github.com/mihonapp/mihon/pull/3155)) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b72704c99..3b303755c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -73,8 +73,8 @@ - - + + @@ -84,6 +84,17 @@ + + + + + + + + + + + diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 3c1c9e6bf..438e036c7 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -25,16 +25,15 @@ import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.interactor.TrackChapter -import mihon.data.repository.ExtensionRepoRepositoryImpl +import mihon.data.extension.repository.ExtensionStoreRepositoryImpl +import mihon.data.extension.service.ExtensionStoreService import mihon.domain.chapter.interactor.FilterChaptersForDownload -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo -import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount -import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo -import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository -import mihon.domain.extensionrepo.service.ExtensionRepoService +import mihon.domain.extension.interactor.AddExtensionStore +import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow +import mihon.domain.extension.interactor.GetExtensionStores +import mihon.domain.extension.interactor.RemoveExtensionStore +import mihon.domain.extension.interactor.UpdateExtensionStores +import mihon.domain.extension.repository.ExtensionStoreRepository import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.upcoming.interactor.GetUpcomingManga import tachiyomi.data.category.CategoryRepositoryImpl @@ -199,14 +198,14 @@ class DomainModule : InjektModule { addFactory { ToggleSourcePin(get()) } addFactory { TrustExtension(get(), get()) } - addSingletonFactory { ExtensionRepoRepositoryImpl(get()) } - addFactory { ExtensionRepoService(get(), get()) } - addFactory { GetExtensionRepo(get()) } - addFactory { GetExtensionRepoCount(get()) } - addFactory { CreateExtensionRepo(get(), get()) } - addFactory { DeleteExtensionRepo(get()) } - addFactory { ReplaceExtensionRepo(get()) } - addFactory { UpdateExtensionRepo(get(), get()) } + addSingletonFactory { ExtensionStoreService(get(), get(), get()) } + addSingletonFactory { ExtensionStoreRepositoryImpl(get(), get()) } + addFactory { AddExtensionStore(get()) } + addFactory { GetExtensionStoreCountAsFlow(get()) } + addFactory { GetExtensionStores(get()) } + addFactory { RemoveExtensionStore(get()) } + addFactory { UpdateExtensionStores(get()) } + addFactory { ToggleIncognito(get()) } addFactory { GetIncognitoState(get(), get(), get()) } } diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt index dc7d2c304..6b25dc37b 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt @@ -28,11 +28,7 @@ class GetExtensionLanguages( ) { enabledLanguage, availableExtensions -> availableExtensions .flatMap { ext -> - if (ext.sources.isEmpty()) { - listOf(ext.lang) - } else { - ext.sources.map { it.lang } - } + ext.sources.map { it.lang } } .distinct() .sortedWith( diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt index c7825a26a..b78b9af41 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt @@ -50,9 +50,6 @@ class GetExtensionsByType( (showNsfwSources || !extension.isNsfw) } .flatMap { ext -> - if (ext.sources.isEmpty()) { - return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList() - } ext.sources.filter { it.lang in enabledLanguages } .map { ext.copy( diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt index f70984952..7e8c12564 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt @@ -4,21 +4,21 @@ import android.content.pm.PackageInfo import androidx.core.content.pm.PackageInfoCompat import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.util.system.isDebugBuildType -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import mihon.domain.extension.model.KOMIKKU_SIGNATURE +import mihon.domain.extension.repository.ExtensionStoreRepository import tachiyomi.core.common.preference.getAndSet class TrustExtension( - private val extensionRepoRepository: ExtensionRepoRepository, + private val repository: ExtensionStoreRepository, private val preferences: SourcePreferences, ) { suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List): Boolean { // KMK --> if (isDebugBuildType) return true - if (fingerprints.contains(CreateExtensionRepo.KOMIKKU_SIGNATURE)) return true + if (fingerprints.contains(KOMIKKU_SIGNATURE)) return true // KMK <-- - val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet() + val trustedFingerprints = repository.getAll().map { it.signingKey }.toHashSet() val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}" return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get() } diff --git a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt index 463dff7d3..2537563a5 100644 --- a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt @@ -61,7 +61,7 @@ class SyncPreferences( tracking = preferenceStore.getBoolean("tracking", true).get(), history = preferenceStore.getBoolean("history", true).get(), appSettings = preferenceStore.getBoolean("appSettings", true).get(), - extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(), + extensionStores = preferenceStore.getBoolean("extensionStores", true).get(), sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(), privateSettings = preferenceStore.getBoolean("privateSettings", true).get(), @@ -80,7 +80,7 @@ class SyncPreferences( preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking) preferenceStore.getBoolean("history", true).set(syncSettings.history) preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings) - preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings) + preferenceStore.getBoolean("extensionStores", true).set(syncSettings.extensionStores) preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings) preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings) diff --git a/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt b/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt index 2563bcbc9..55c69a0e6 100644 --- a/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt +++ b/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt @@ -7,7 +7,7 @@ data class SyncSettings( val tracking: Boolean = true, val history: Boolean = true, val appSettings: Boolean = true, - val extensionRepoSettings: Boolean = true, + val extensionStores: Boolean = true, val sourceSettings: Boolean = true, val privateSettings: Boolean = false, diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 2f6bc8865..f4fce3aad 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -86,12 +86,12 @@ fun ExtensionDetailsScreen( val uriHandler = LocalUriHandler.current val url = remember(state.extension) { val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex() - regex.find(state.extension?.repoUrl.orEmpty()) + regex.find(state.extension?.store?.indexUrl.orEmpty()) ?.let { val (user, repo) = it.destructured "https://github.com/$user/$repo" } - ?: state.extension?.repoUrl + ?: state.extension?.store?.indexUrl } Scaffold( @@ -270,14 +270,17 @@ private fun DetailsHeader( if (extension is Extension.Installed) { append("\n\n") - append( + appendLine( """ Update available: ${extension.hasUpdate} Obsolete: ${extension.isObsolete} Shared: ${extension.isShared} - Repository: ${extension.repoUrl} """.trimIndent(), ) + val store = extension.store + if (store != null) { + append("Repository: ${store.indexUrl}") + } } } context.copyToClipboard("Extension Debug information", extDebugInfo) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index d287d5ac7..7e6ac8a10 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -48,7 +48,7 @@ import eu.kanade.presentation.browse.components.BaseBrowseItem import eu.kanade.presentation.browse.components.ExtensionIcon import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.tachiyomi.extension.model.Extension @@ -58,6 +58,8 @@ import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import kotlinx.collections.immutable.persistentListOf +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.model.KOMIKKU_SIGNATURE import tachiyomi.i18n.MR import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.sy.SYMR @@ -110,9 +112,9 @@ fun ExtensionScreen( modifier = Modifier.padding(contentPadding), actions = persistentListOf( EmptyScreenAction( - stringRes = MR.strings.label_extension_repos, + stringRes = MR.strings.extensionStores, icon = Icons.Outlined.Settings, - onClick = { navigator.push(ExtensionReposScreen()) }, + onClick = { navigator.push(ExtensionStoresScreen()) }, ), ), ) @@ -195,9 +197,9 @@ private fun ExtensionContent( // KMK --> KMR.strings.extensions_page_more -> { { - Button(onClick = { navigator?.push(ExtensionReposScreen()) }) { + Button(onClick = { navigator?.push(ExtensionStoresScreen()) }) { Text( - text = stringResource(MR.strings.action_add_repo), + text = stringResource(MR.strings.action_addExtensionStore), style = LocalTextStyle.current.copy( color = MaterialTheme.colorScheme.onPrimary, ), @@ -401,7 +403,7 @@ private fun ExtensionItemContent( } // KMK --> - Text(text = extension.repoName?.let { "@$it" } ?: "(?)") + Text(text = extension.storeName?.let { "@$it" } ?: "(?)") // KMK <-- val warning = when { @@ -604,11 +606,11 @@ private fun ExtensionItemContentPreview() { libVersion = 1.0, isNsfw = true, signatureHash = "900000", - repoName = "Repository", + storeName = "Repository", sources = emptyList(), - apkName = "Test", + apkUrl = "Test", iconUrl = "", - repoUrl = "", + store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), ) val extInstalled = Extension.Installed( name = "Tachiyomi", @@ -619,9 +621,9 @@ private fun ExtensionItemContentPreview() { libVersion = 1.0, isNsfw = true, signatureHash = "900000", - repoName = "Repository", + storeName = "Repository", sources = emptyList(), - repoUrl = "", + store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), pkgFactory = null, icon = null, hasUpdate = false, @@ -638,12 +640,12 @@ private fun ExtensionItemContentPreview() { libVersion = 1.0, isNsfw = true, signatureHash = "900000", - repoName = "Repository", + storeName = "Repository", ) Column { ExtensionItemContent( extension = extAvail.copy( - repoName = "Repository extensions minion multiple languages various sources", + storeName = "Repository extensions minion multiple languages various sources", ), installStep = InstallStep.Idle, ) 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 e96eb1e72..40d04db6c 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 @@ -14,11 +14,11 @@ import eu.kanade.core.preference.asState import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.more.settings.Preference -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate import kotlinx.collections.immutable.persistentListOf -import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount +import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import tachiyomi.i18n.kmk.KMR @@ -43,9 +43,9 @@ object SettingsBrowseScreen : SearchableSettings { val navigator = LocalNavigator.currentOrThrow val sourcePreferences = remember { Injekt.get() } - val getExtensionRepoCount = remember { Injekt.get() } + val getExtensionStoreCountAsFlow = remember { Injekt.get() } - val reposCount by getExtensionRepoCount.subscribe().collectAsState(0) + val reposCount by getExtensionStoreCountAsFlow().collectAsState(0) // SY --> val scope = rememberCoroutineScope() @@ -142,10 +142,10 @@ object SettingsBrowseScreen : SearchableSettings { title = stringResource(MR.strings.pref_hide_in_library_items), ), Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.label_extension_repos), - subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount), + title = stringResource(MR.strings.extensionStores), + subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.toInt(), reposCount), onClick = { - navigator.push(ExtensionReposScreen()) + navigator.push(ExtensionStoresScreen()) }, ), ), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt deleted file mode 100644 index 3cd14aa2b..000000000 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt +++ /dev/null @@ -1,205 +0,0 @@ -package eu.kanade.presentation.more.settings.screen.browse - -import androidx.compose.runtime.Immutable -import cafe.adriel.voyager.core.model.StateScreenModel -import cafe.adriel.voyager.core.model.screenModelScope -import dev.icerock.moko.resources.StringResource -import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.tachiyomi.extension.ExtensionManager -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo -import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo -import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo -import mihon.domain.extensionrepo.model.ExtensionRepo -import tachiyomi.core.common.util.lang.launchIO -import tachiyomi.i18n.MR -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionReposScreenModel( - private val getExtensionRepo: GetExtensionRepo = Injekt.get(), - private val createExtensionRepo: CreateExtensionRepo = Injekt.get(), - private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(), - private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(), - private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(), - private val extensionManager: ExtensionManager = Injekt.get(), - // KMK --> - private val sourcePreferences: SourcePreferences = Injekt.get(), - // KMK <-- -) : StateScreenModel(RepoScreenState.Loading) { - - private val _events: Channel = Channel(Int.MAX_VALUE) - val events = _events.receiveAsFlow() - - init { - screenModelScope.launchIO { - getExtensionRepo.subscribeAll() - .collectLatest { repos -> - mutableState.update { - RepoScreenState.Success( - repos = repos.toImmutableSet(), - // KMK --> - disabledRepos = sourcePreferences.disabledRepos().get(), - // KMK <-- - ) - } - } - } - - // KMK --> - sourcePreferences.disabledRepos().changes() - .onEach { disabledRepos -> - mutableState.update { - when (it) { - is RepoScreenState.Success -> it.copy(disabledRepos = disabledRepos) - else -> it - } - } - } - .launchIn(screenModelScope) - // KMK <-- - } - - /** - * Creates and adds a new repo to the database. - * - * @param baseUrl The baseUrl of the repo to create. - */ - fun createRepo(baseUrl: String) { - screenModelScope.launchIO { - when (val result = createExtensionRepo.await(baseUrl)) { - CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions() - CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl) - CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists) - is CreateExtensionRepo.Result.DuplicateFingerprint -> { - showDialog(RepoDialog.Conflict(result.oldRepo, result.newRepo)) - } - else -> {} - } - } - } - - /** - * Inserts a repo to the database, replace a matching repo with the same signing key fingerprint if found. - * - * @param newRepo The repo to insert - */ - fun replaceRepo(newRepo: ExtensionRepo) { - screenModelScope.launchIO { - replaceExtensionRepo.await(newRepo) - } - } - - /** - * Refreshes information for each repository. - */ - fun refreshRepos() { - val status = state.value - - if (status is RepoScreenState.Success) { - screenModelScope.launchIO { - updateExtensionRepo.awaitAll() - } - } - } - - /** - * Deletes the given repo from the database - */ - fun deleteRepo(baseUrl: String) { - // KMK --> - // Remove repo from disabled list - enableRepo(baseUrl) - // KMK <-- - screenModelScope.launchIO { - deleteExtensionRepo.await(baseUrl) - extensionManager.findAvailableExtensions() - } - } - - // KMK --> - fun enableRepo(baseUrl: String) { - val disabledRepos = sourcePreferences.disabledRepos().get() - if (baseUrl in disabledRepos) { - sourcePreferences.disabledRepos().set( - disabledRepos.filterNot { it == baseUrl }.toSet(), - ) - } - } - - fun disableRepo(baseUrl: String) { - val disabledRepos = sourcePreferences.disabledRepos().get() - if (baseUrl !in disabledRepos) { - sourcePreferences.disabledRepos().set( - disabledRepos + baseUrl, - ) - } - } - - fun refreshExtensionList() { - screenModelScope.launchIO { - extensionManager.findAvailableExtensions() - } - } - // KMK <-- - - fun showDialog(dialog: RepoDialog) { - mutableState.update { - when (it) { - RepoScreenState.Loading -> it - is RepoScreenState.Success -> it.copy(dialog = dialog) - } - } - } - - fun dismissDialog() { - mutableState.update { - when (it) { - RepoScreenState.Loading -> it - is RepoScreenState.Success -> it.copy(dialog = null) - } - } - } -} - -sealed class RepoEvent { - sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent() - data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name) - data object RepoAlreadyExists : LocalizedMessage(MR.strings.error_repo_exists) -} - -sealed class RepoDialog { - data object Create : RepoDialog() - data class Delete(val repo: String) : RepoDialog() - data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog() - data class Confirm(val url: String) : RepoDialog() -} - -sealed class RepoScreenState { - - @Immutable - data object Loading : RepoScreenState() - - @Immutable - data class Success( - val repos: ImmutableSet, - val oldRepos: ImmutableSet? = null, - val dialog: RepoDialog? = null, - // KMK --> - val disabledRepos: Set = emptySet(), - // KMK <-- - ) : RepoScreenState() { - - val isEmpty: Boolean - get() = repos.isEmpty() - } -} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt similarity index 53% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt index aa0217fec..af1ce92bd 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt @@ -8,20 +8,18 @@ import androidx.compose.ui.platform.LocalContext import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConfirmDialog -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreConfirmDialog +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreCreateDialog +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreDeleteDialog +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoresScreen import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast -import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.flow.collectLatest import tachiyomi.i18n.kmk.KMR import tachiyomi.presentation.core.screens.LoadingScreen -class ExtensionReposScreen( +class ExtensionStoresScreen( private val url: String? = null, ) : Screen() { @@ -30,80 +28,72 @@ class ExtensionReposScreen( val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { ExtensionReposScreenModel() } + val screenModel = rememberScreenModel { ExtensionStoresScreenModel() } val state by screenModel.state.collectAsState() LaunchedEffect(url) { - url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) } + url?.let { screenModel.addFromDeeplink(url) } } - if (state is RepoScreenState.Loading) { + if (state is ExtensionStoreScreenState.Loading) { LoadingScreen() return } - val successState = state as RepoScreenState.Success + val successState = state as ExtensionStoreScreenState.Success - ExtensionReposScreen( + ExtensionStoresScreen( state = successState, - onClickCreate = { screenModel.showDialog(RepoDialog.Create) }, - onOpenWebsite = { context.openInBrowser(it.website) }, - onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) }, + onClickCreate = { screenModel.showDialog(ExtensionStoreDialog.Create()) }, + onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) }, + onOpenWebsite = { it.contact.website.let(context::openInBrowser) }, + onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) }, // KMK --> onClickEnable = { - screenModel.enableRepo(it) + screenModel.enableStore(it.indexUrl) screenModel.refreshExtensionList() context.toast(KMR.strings.extensions_page_need_refresh) }, onClickDisable = { - screenModel.disableRepo(it) + screenModel.disableStore(it.indexUrl) screenModel.refreshExtensionList() context.toast(KMR.strings.extensions_page_need_refresh) }, // KMK <-- + onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) }, onClickRefresh = { screenModel.refreshRepos() }, navigateUp = navigator::pop, ) when (val dialog = successState.dialog) { null -> {} - is RepoDialog.Create -> { - ExtensionRepoCreateDialog( + is ExtensionStoreDialog.Create -> { + ExtensionStoreCreateDialog( onDismissRequest = screenModel::dismissDialog, onCreate = { screenModel.createRepo(it) }, - repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(), + storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(), + processing = dialog.processing, + errorMessage = dialog.errorMessage, ) } - is RepoDialog.Delete -> { - ExtensionRepoDeleteDialog( + is ExtensionStoreDialog.Delete -> { + ExtensionStoreDeleteDialog( onDismissRequest = screenModel::dismissDialog, - onDelete = { screenModel.deleteRepo(dialog.repo) }, - repo = dialog.repo, + onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) }, + storeName = dialog.store.name, + storeIndexUrl = dialog.store.indexUrl, ) } - is RepoDialog.Conflict -> { - ExtensionRepoConflictDialog( - onDismissRequest = screenModel::dismissDialog, - onMigrate = { screenModel.replaceRepo(dialog.newRepo) }, - oldRepo = dialog.oldRepo, - newRepo = dialog.newRepo, - ) - } - is RepoDialog.Confirm -> { - ExtensionRepoConfirmDialog( + is ExtensionStoreDialog.Confirm -> { + ExtensionStoreConfirmDialog( onDismissRequest = screenModel::dismissDialog, onCreate = { screenModel.createRepo(dialog.url) }, - repo = dialog.url, + storeIndexUrl = dialog.url, + storeAlreadyExists = dialog.alreadyExists, + processing = dialog.processing, + errorMessage = dialog.errorMessage, ) } } - - LaunchedEffect(Unit) { - screenModel.events.collectLatest { event -> - if (event is RepoEvent.LocalizedMessage) { - context.toast(event.stringRes) - } - } - } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt new file mode 100644 index 000000000..f794576d3 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt @@ -0,0 +1,223 @@ +package eu.kanade.presentation.more.settings.screen.browse + +import androidx.compose.runtime.Immutable +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.extension.ExtensionManager +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import mihon.domain.extension.interactor.AddExtensionStore +import mihon.domain.extension.interactor.GetExtensionStores +import mihon.domain.extension.interactor.RemoveExtensionStore +import mihon.domain.extension.interactor.UpdateExtensionStores +import mihon.domain.extension.model.ExtensionStore +import tachiyomi.core.common.util.lang.launchIO +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionStoresScreenModel( + private val getExtensionStores: GetExtensionStores = Injekt.get(), + private val addExtensionStore: AddExtensionStore = Injekt.get(), + private val removeExtensionStore: RemoveExtensionStore = Injekt.get(), + private val updateExtensionStores: UpdateExtensionStores = Injekt.get(), + private val extensionManager: ExtensionManager = Injekt.get(), + // KMK --> + private val sourcePreferences: SourcePreferences = Injekt.get(), + // KMK <-- +) : StateScreenModel(ExtensionStoreScreenState.Loading) { + + private inline fun updateSuccessState( + func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success, + ) { + mutableState.update { + when (it) { + ExtensionStoreScreenState.Loading -> it + is ExtensionStoreScreenState.Success -> func(it) + } + } + } + + init { + screenModelScope.launchIO { + getExtensionStores.subscribe() + .collectLatest { stores -> + mutableState.update { + when (it) { + ExtensionStoreScreenState.Loading -> ExtensionStoreScreenState.Success( + stores = stores, + // KMK --> + disabledRepos = sourcePreferences.disabledRepos().get(), + // KMK <-- + ) + is ExtensionStoreScreenState.Success -> it.copy(stores = stores) + } + } + } + } + + // KMK --> + sourcePreferences.disabledRepos().changes() + .onEach { disabledRepos -> + mutableState.update { + when (it) { + is ExtensionStoreScreenState.Success -> it.copy(disabledRepos = disabledRepos) + else -> it + } + } + } + .launchIn(screenModelScope) + // KMK <-- + } + + /** + * Creates and adds a new repo to the database. + * + * @param indexUrl The baseUrl of the repo to create. + */ + fun createRepo(indexUrl: String) { + // KMK --> + screenModelScope.launchIO { + // KMK <-- + updateSuccessState { + it.copy( + dialog = when (it.dialog) { + is ExtensionStoreDialog.Create -> it.dialog.copy(processing = true) + is ExtensionStoreDialog.Confirm -> it.dialog.copy(processing = true) + else -> it.dialog + }, + ) + } + addExtensionStore(indexUrl) + .onSuccess { + extensionManager.findAvailableExtensions() + dismissDialog() + } + .onFailure { throwable -> + updateSuccessState { + it.copy( + dialog = when (it.dialog) { + is ExtensionStoreDialog.Create -> it.dialog.copy( + processing = false, + errorMessage = throwable.message ?: "unknown error", + ) + is ExtensionStoreDialog.Confirm -> it.dialog.copy( + processing = false, + errorMessage = throwable.message ?: "unknown error", + ) + else -> it.dialog + }, + ) + } + } + } + } + + /** + * Refreshes information for each repository. + */ + fun refreshRepos() { + val status = state.value + + if (status is ExtensionStoreScreenState.Success) { + screenModelScope.launchIO { + updateExtensionStores() + } + } + } + + /** + * Deletes the given repo from the database + */ + fun deleteRepo(indexUrl: String) { + // KMK --> + // Remove repo from disabled list + enableStore(indexUrl) + // KMK <-- + screenModelScope.launchIO { + removeExtensionStore(indexUrl) + extensionManager.findAvailableExtensions() + } + } + + // KMK --> + fun enableStore(indexUrl: String) { + val disabledRepos = sourcePreferences.disabledRepos().get() + if (indexUrl in disabledRepos) { + sourcePreferences.disabledRepos().set( + disabledRepos.filterNot { it == indexUrl }.toSet(), + ) + } + } + + fun disableStore(indexUrl: String) { + val disabledRepos = sourcePreferences.disabledRepos().get() + if (indexUrl !in disabledRepos) { + sourcePreferences.disabledRepos().set( + disabledRepos + indexUrl, + ) + } + } + + fun refreshExtensionList() { + screenModelScope.launchIO { + extensionManager.findAvailableExtensions() + } + } + // KMK <-- + + fun addFromDeeplink(storeIndexUrl: String) { + updateSuccessState { state -> + state.copy( + dialog = ExtensionStoreDialog.Confirm( + url = storeIndexUrl, + alreadyExists = state.stores.any { it.indexUrl == storeIndexUrl }, + ), + ) + } + } + + fun showDialog(dialog: ExtensionStoreDialog) { + updateSuccessState { state -> + state.copy(dialog = dialog) + } + } + + fun dismissDialog() { + updateSuccessState { + it.copy(dialog = null) + } + } +} + +sealed class ExtensionStoreDialog { + data class Create(val processing: Boolean = false, val errorMessage: String? = null) : ExtensionStoreDialog() + data class Delete(val store: ExtensionStore) : ExtensionStoreDialog() + data class Confirm( + val url: String, + val alreadyExists: Boolean = false, + val processing: Boolean = false, + val errorMessage: String? = null, + ) : ExtensionStoreDialog() +} + +sealed class ExtensionStoreScreenState { + + @Immutable + data object Loading : ExtensionStoreScreenState() + + @Immutable + data class Success( + val stores: List, + val dialog: ExtensionStoreDialog? = null, + // KMK --> + val disabledRepos: Set = emptySet(), + // KMK <-- + ) : ExtensionStoreScreenState() { + + val isEmpty: Boolean + get() = stores.isEmpty() + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt deleted file mode 100644 index bd7c1cedd..000000000 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt +++ /dev/null @@ -1,188 +0,0 @@ -package eu.kanade.presentation.more.settings.screen.browse.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.text.input.KeyboardType -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.coroutines.delay -import mihon.domain.extensionrepo.model.ExtensionRepo -import tachiyomi.i18n.MR -import tachiyomi.presentation.core.i18n.stringResource -import kotlin.time.Duration.Companion.seconds - -@Composable -fun ExtensionRepoCreateDialog( - onDismissRequest: () -> Unit, - onCreate: (String) -> Unit, - repoUrls: ImmutableSet, -) { - var name by remember { mutableStateOf("") } - - val focusRequester = remember { FocusRequester() } - val nameAlreadyExists = remember(name) { repoUrls.contains(name) } - - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - enabled = name.isNotEmpty() && !nameAlreadyExists, - onClick = { - onCreate(name) - onDismissRequest() - }, - ) { - Text(text = stringResource(MR.strings.action_add)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - title = { - Text(text = stringResource(MR.strings.action_add_repo)) - }, - text = { - Column { - Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name))) - - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - value = name, - onValueChange = { name = it }, - label = { - Text(text = stringResource(MR.strings.label_add_repo_input)) - }, - supportingText = { - val msgRes = if (name.isNotEmpty() && nameAlreadyExists) { - MR.strings.error_repo_exists - } else { - MR.strings.information_required_plain - } - Text(text = stringResource(msgRes)) - }, - isError = name.isNotEmpty() && nameAlreadyExists, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), - singleLine = true, - ) - } - }, - ) - - LaunchedEffect(focusRequester) { - // TODO: https://issuetracker.google.com/issues/204502668 - delay(0.1.seconds) - focusRequester.requestFocus() - } -} - -@Composable -fun ExtensionRepoDeleteDialog( - onDismissRequest: () -> Unit, - onDelete: () -> Unit, - repo: String, -) { - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton(onClick = { - onDelete() - onDismissRequest() - }) { - Text(text = stringResource(MR.strings.action_ok)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - title = { - Text(text = stringResource(MR.strings.action_delete_repo)) - }, - text = { - Text(text = stringResource(MR.strings.delete_repo_confirmation, repo)) - }, - ) -} - -@Composable -fun ExtensionRepoConflictDialog( - oldRepo: ExtensionRepo, - newRepo: ExtensionRepo, - onDismissRequest: () -> Unit, - onMigrate: () -> Unit, -) { - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - onClick = { - onMigrate() - onDismissRequest() - }, - ) { - Text(text = stringResource(MR.strings.action_replace_repo)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - title = { - Text(text = stringResource(MR.strings.action_replace_repo_title)) - }, - text = { - Text(text = stringResource(MR.strings.action_replace_repo_message, newRepo.name, oldRepo.name)) - }, - ) -} - -@Composable -fun ExtensionRepoConfirmDialog( - onDismissRequest: () -> Unit, - onCreate: () -> Unit, - repo: String, -) { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { - Text(text = stringResource(MR.strings.action_add_repo)) - }, - text = { - Text(text = stringResource(MR.strings.add_repo_confirmation, repo)) - }, - confirmButton = { - TextButton( - onClick = { - onCreate() - onDismissRequest() - }, - ) { - Text(text = stringResource(MR.strings.action_add)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - ) -} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresContent.kt similarity index 71% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresContent.kt index 346e38c43..ed7ad079b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresContent.kt @@ -11,9 +11,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.ElevatedCard @@ -26,33 +26,33 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.copyToClipboard -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.persistentSetOf -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE -import mihon.domain.extensionrepo.model.ExtensionRepo +import kotlinx.collections.immutable.persistentListOf +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.model.KOMIKKU_SIGNATURE +import mihon.domain.extension.model.REPO_SIGNATURE import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.icons.CustomIcons +import tachiyomi.presentation.core.icons.Discord @Composable -fun ExtensionReposContent( - repos: ImmutableSet, +fun ExtensionStoresContent( + repos: List, lazyListState: LazyListState, paddingValues: PaddingValues, - onOpenWebsite: (ExtensionRepo) -> Unit, - onClickDelete: (String) -> Unit, + onCopy: (ExtensionStore) -> Unit, + onOpenWebsite: (ExtensionStore) -> Unit, + onOpenDiscord: (ExtensionStore) -> Unit, + onClickDelete: (ExtensionStore) -> Unit, // KMK --> - onClickEnable: (String) -> Unit, - onClickDisable: (String) -> Unit, + onClickEnable: (ExtensionStore) -> Unit, + onClickDisable: (ExtensionStore) -> Unit, disabledRepos: Set, // KMK <-- modifier: Modifier = Modifier, @@ -65,15 +65,17 @@ fun ExtensionReposContent( ) { repos.forEach { item { - ExtensionRepoListItem( + ExtensionStoresListItem( modifier = Modifier.animateItem(), - repo = it, + store = it, onOpenWebsite = { onOpenWebsite(it) }, - onDelete = { onClickDelete(it.baseUrl) }, + onOpenDiscord = { onOpenDiscord(it) }, + onCopy = { onCopy(it) }, + onDelete = { onClickDelete(it) }, // KMK --> - onEnable = { onClickEnable(it.baseUrl) }, - onDisable = { onClickDisable(it.baseUrl) }, - isDisabled = it.baseUrl in disabledRepos, + onEnable = { onClickEnable(it) }, + onDisable = { onClickDisable(it) }, + isDisabled = it.indexUrl in disabledRepos, // KMK <-- ) } @@ -82,9 +84,11 @@ fun ExtensionReposContent( } @Composable -private fun ExtensionRepoListItem( - repo: ExtensionRepo, +private fun ExtensionStoresListItem( + store: ExtensionStore, onOpenWebsite: () -> Unit, + onOpenDiscord: () -> Unit, + onCopy: () -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier, // KMK --> @@ -93,8 +97,6 @@ private fun ExtensionRepoListItem( onDisable: () -> Unit, // KMK <-- ) { - val context = LocalContext.current - ElevatedCard( modifier = modifier, ) { @@ -103,9 +105,9 @@ private fun ExtensionRepoListItem( modifier = Modifier .padding(start = MaterialTheme.padding.medium), ) { - val resId = repoResId(repo.signingKeyFingerprint) + val resId = repoResId(store.signingKey) Image( - bitmap = ImageBitmap.imageResource(id = resId), + painter = painterResource(id = resId), contentDescription = null, alpha = if (isDisabled) 0.4f else 1f, modifier = Modifier @@ -126,7 +128,7 @@ private fun ExtensionRepoListItem( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = repo.name, + text = store.name, // KMK: modifier = Modifier.padding(start = MaterialTheme.padding.medium), style = MaterialTheme.typography.titleMedium, // KMK --> @@ -142,17 +144,21 @@ private fun ExtensionRepoListItem( ) { IconButton(onClick = onOpenWebsite) { Icon( - imageVector = Icons.AutoMirrored.Outlined.OpenInNew, + imageVector = Icons.Outlined.Public, contentDescription = stringResource(MR.strings.action_open_in_browser), ) } - IconButton( - onClick = { - val url = "${repo.baseUrl}/index.min.json" - context.copyToClipboard(url, url) - }, - ) { + if (store.contact.discord != null) { + IconButton(onClick = onOpenDiscord) { + Icon( + imageVector = CustomIcons.Discord, + contentDescription = null, + ) + } + } + + IconButton(onClick = onCopy) { Icon( imageVector = Icons.Outlined.ContentCopy, contentDescription = stringResource(MR.strings.action_copy_to_clipboard), @@ -190,16 +196,18 @@ fun repoResId(signKey: String) = when (signKey) { @Preview @Composable fun ExtensionReposContentPreview() { - val repos = persistentSetOf( - ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE), - ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE), - ExtensionRepo("https://repo", "Other", "", "", "key2"), + val repos = persistentListOf( + ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true), ) - ExtensionReposContent( + ExtensionStoresContent( repos = repos, lazyListState = LazyListState(), paddingValues = PaddingValues(), + onCopy = {}, onOpenWebsite = {}, + onOpenDiscord = {}, onClickDelete = {}, onClickEnable = {}, onClickDisable = {}, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt new file mode 100644 index 000000000..19436f17a --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt @@ -0,0 +1,192 @@ +package eu.kanade.presentation.more.settings.screen.browse.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import kotlin.time.Duration.Companion.seconds + +@Composable +fun ExtensionStoreCreateDialog( + onDismissRequest: () -> Unit, + onCreate: (String) -> Unit, + storeIndexUrls: Set, + processing: Boolean, + errorMessage: String?, +) { + val state = rememberTextFieldState() + val storeAlreadyExists by remember(storeIndexUrls) { + derivedStateOf { + val indexUrl = state.text.toString() + storeIndexUrls.contains(indexUrl) + } + } + + val focusRequester = remember { FocusRequester() } + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_title)) + }, + text = { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + state = state, + label = { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStoreInput_inputLabel)) + }, + supportingText = { + val msgRes = if (storeAlreadyExists) { + MR.strings.extensionStoresScreen_addStore_alreadyExists + } else { + MR.strings.information_required_plain + } + Text(text = errorMessage ?: stringResource(msgRes)) + }, + isError = errorMessage != null || storeAlreadyExists, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + lineLimits = TextFieldLineLimits.SingleLine, + ) + }, + confirmButton = { + TextButton( + onClick = { onCreate(state.text.toString()) }, + enabled = !processing && state.text.isNotEmpty() && !storeAlreadyExists, + ) { + Text( + text = stringResource( + resource = if (processing) { + MR.strings.extensionStoresScreen_addStore_processing + } else { + MR.strings.action_add + }, + ), + ) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) + + LaunchedEffect(focusRequester) { + // TODO: https://issuetracker.google.com/issues/204502668 + delay(0.1.seconds) + focusRequester.requestFocus() + } +} + +@Composable +fun ExtensionStoreDeleteDialog( + onDismissRequest: () -> Unit, + onDelete: () -> Unit, + storeName: String, + storeIndexUrl: String, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.extensionStoresScreen_deleteStore_title)) + }, + text = { + Text(text = stringResource(MR.strings.extensionStoresScreen_deleteStore_body, storeName, storeIndexUrl)) + }, + confirmButton = { + TextButton(onClick = { + onDelete() + onDismissRequest() + }) { + Text(text = stringResource(MR.strings.action_ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) +} + +@Composable +fun ExtensionStoreConfirmDialog( + onDismissRequest: () -> Unit, + onCreate: () -> Unit, + storeIndexUrl: String, + storeAlreadyExists: Boolean, + processing: Boolean, + errorMessage: String?, +) { + val state = rememberTextFieldState(initialText = storeIndexUrl) + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_title)) + }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStoreDeeplink_bodyText)) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + state = state, + readOnly = true, + supportingText = when { + storeAlreadyExists -> { + { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_alreadyExists)) + } + } + errorMessage != null -> { + { + Text(text = errorMessage) + } + } + else -> null + }, + isError = errorMessage != null || storeAlreadyExists, + ) + } + }, + confirmButton = { + TextButton(onClick = onCreate, enabled = !storeAlreadyExists && !processing) { + Text( + text = stringResource( + resource = if (processing) { + MR.strings.extensionStoresScreen_addStore_processing + } else { + MR.strings.action_add + }, + ), + ) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresScreen.kt similarity index 71% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresScreen.kt index 651b45f51..89e8a0138 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresScreen.kt @@ -1,5 +1,3 @@ -@file:JvmName("ExtensionReposScreenKt") - package eu.kanade.presentation.more.settings.screen.browse.components import androidx.compose.foundation.layout.PaddingValues @@ -21,13 +19,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.components.AppBar -import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoreScreenState import eu.kanade.tachiyomi.util.system.openInBrowser -import kotlinx.collections.immutable.persistentSetOf -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_HELP -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE -import mihon.domain.extensionrepo.model.ExtensionRepo +import kotlinx.collections.immutable.persistentListOf +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.model.KOMIKKU_SIGNATURE +import mihon.domain.extension.model.REPO_HELP +import mihon.domain.extension.model.REPO_SIGNATURE import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding @@ -37,14 +35,16 @@ import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.util.plus @Composable -fun ExtensionReposScreen( - state: RepoScreenState.Success, +fun ExtensionStoresScreen( + state: ExtensionStoreScreenState.Success, onClickCreate: () -> Unit, - onOpenWebsite: (ExtensionRepo) -> Unit, - onClickDelete: (String) -> Unit, + onCopy: (ExtensionStore) -> Unit, + onOpenWebsite: (ExtensionStore) -> Unit, + onOpenDiscord: (ExtensionStore) -> Unit, + onClickDelete: (ExtensionStore) -> Unit, // KMK --> - onClickEnable: (String) -> Unit, - onClickDisable: (String) -> Unit, + onClickEnable: (ExtensionStore) -> Unit, + onClickDisable: (ExtensionStore) -> Unit, // KMK <-- onClickRefresh: () -> Unit, navigateUp: () -> Unit, @@ -54,7 +54,7 @@ fun ExtensionReposScreen( topBar = { scrollBehavior -> AppBar( navigateUp = navigateUp, - title = stringResource(MR.strings.label_extension_repos), + title = stringResource(MR.strings.extensionStores), scrollBehavior = scrollBehavior, actions = { IconButton(onClick = onClickRefresh) { @@ -76,7 +76,7 @@ fun ExtensionReposScreen( if (state.isEmpty) { val context = LocalContext.current EmptyScreen( - MR.strings.information_empty_repos, + MR.strings.extensionStoresScreen_emptyLabel, modifier = Modifier.padding(paddingValues), // KMK --> help = { @@ -94,12 +94,14 @@ fun ExtensionReposScreen( return@Scaffold } - ExtensionReposContent( - repos = state.repos, + ExtensionStoresContent( + repos = state.stores, lazyListState = lazyListState, paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium), + onCopy = onCopy, onOpenWebsite = onOpenWebsite, + onOpenDiscord = onOpenDiscord, onClickDelete = onClickDelete, // KMK --> onClickEnable = onClickEnable, @@ -113,19 +115,21 @@ fun ExtensionReposScreen( // KMK --> @Preview @Composable -private fun ExtensionReposScreenPreview() { - val state = RepoScreenState.Success( - repos = persistentSetOf( - ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE), - ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE), - ExtensionRepo("https://repo", "Other", "", "", "key2"), +private fun ExtensionStoresScreenPreview() { + val state = ExtensionStoreScreenState.Success( + stores = persistentListOf( + ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true), ), disabledRepos = setOf("https://repo"), ) - ExtensionReposScreen( + ExtensionStoresScreen( state = state, onClickCreate = { }, + onCopy = { }, onOpenWebsite = { }, + onOpenDiscord = { }, onClickDelete = { }, onClickEnable = { }, onClickDisable = { }, @@ -136,12 +140,14 @@ private fun ExtensionReposScreenPreview() { @Preview @Composable -private fun ExtensionReposScreenEmptyPreview() { - val state = RepoScreenState.Success(repos = persistentSetOf()) - ExtensionReposScreen( +private fun ExtensionStoresScreenEmptyPreview() { + val state = ExtensionStoreScreenState.Success(stores = persistentListOf()) + ExtensionStoresScreen( state = state, onClickCreate = { }, + onCopy = { }, onOpenWebsite = { }, + onOpenDiscord = { }, onClickDelete = { }, onClickEnable = { }, onClickDisable = { }, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt index 83e4b6c08..f198455fc 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt @@ -114,7 +114,7 @@ private class SyncSettingsSelectorModel( tracking = syncSettings.tracking, history = syncSettings.history, appSettings = syncSettings.appSettings, - extensionRepoSettings = syncSettings.extensionRepoSettings, + extensionStores = syncSettings.extensionStores, sourceSettings = syncSettings.sourceSettings, privateSettings = syncSettings.privateSettings, @@ -134,7 +134,7 @@ private class SyncSettingsSelectorModel( tracking = backupOptions.tracking, history = backupOptions.history, appSettings = backupOptions.appSettings, - extensionRepoSettings = backupOptions.extensionRepoSettings, + extensionStores = backupOptions.extensionStores, sourceSettings = backupOptions.sourceSettings, privateSettings = backupOptions.privateSettings, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt index 731f7bad4..5014f6ac5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt @@ -6,7 +6,7 @@ import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator -import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionRepoBackupCreator +import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionStoresBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.FeedBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator @@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.backup.create.creators.SavedSearchBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupCategory -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore import eu.kanade.tachiyomi.data.backup.models.BackupFeed import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference @@ -54,7 +54,7 @@ class BackupCreator( private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(), private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(), private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(), - private val extensionRepoBackupCreator: ExtensionRepoBackupCreator = ExtensionRepoBackupCreator(), + private val extensionStoresBackupCreator: ExtensionStoresBackupCreator = ExtensionStoresBackupCreator(), private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(), // KMK --> private val feedBackupCreator: FeedBackupCreator = FeedBackupCreator(), @@ -101,7 +101,7 @@ class BackupCreator( backupCategories = backupCategories(options), backupSources = backupSources(backupManga), backupPreferences = backupAppPreferences(options), - backupExtensionRepo = backupExtensionRepos(options), + backupExtensionStores = backupExtensionStores(options), backupSourcePreferences = backupSourcePreferences(options), // SY --> @@ -159,16 +159,16 @@ class BackupCreator( return sourcesBackupCreator(mangas) } - /* KMK --> */ suspend /* KMK <-- */ fun backupAppPreferences(options: BackupOptions): List { + internal fun backupAppPreferences(options: BackupOptions): List { if (!options.appSettings) return emptyList() return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings) } - suspend fun backupExtensionRepos(options: BackupOptions): List { - if (!options.extensionRepoSettings) return emptyList() + internal suspend fun backupExtensionStores(options: BackupOptions): List { + if (!options.extensionStores) return emptyList() - return extensionRepoBackupCreator() + return extensionStoresBackupCreator() } fun backupSourcePreferences(options: BackupOptions): List { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt index 469137c35..666ba9d3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt @@ -14,7 +14,7 @@ data class BackupOptions( val history: Boolean = true, val readEntries: Boolean = true, val appSettings: Boolean = true, - val extensionRepoSettings: Boolean = true, + val extensionStores: Boolean = true, val sourceSettings: Boolean = true, val privateSettings: Boolean = false, // SY --> @@ -31,7 +31,7 @@ data class BackupOptions( history, readEntries, appSettings, - extensionRepoSettings, + extensionStores, sourceSettings, privateSettings, // SY --> @@ -41,7 +41,7 @@ data class BackupOptions( ) fun canCreate() = - libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearchesFeeds + libraryEntries || categories || appSettings || extensionStores || sourceSettings || savedSearchesFeeds companion object { val libraryOptions = persistentListOf( @@ -103,9 +103,9 @@ data class BackupOptions( setter = { options, enabled -> options.copy(appSettings = enabled) }, ), Entry( - label = MR.strings.extensionRepo_settings, - getter = BackupOptions::extensionRepoSettings, - setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) }, + label = MR.strings.extensionStores, + getter = BackupOptions::extensionStores, + setter = { options, enabled -> options.copy(extensionStores = enabled) }, ), Entry( label = MR.strings.source_settings, @@ -128,7 +128,7 @@ data class BackupOptions( history = array[4], readEntries = array[5], appSettings = array[6], - extensionRepoSettings = array[7], + extensionStores = array[7], sourceSettings = array[8], privateSettings = array[9], // SY --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt deleted file mode 100644 index 2db6a6f06..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt +++ /dev/null @@ -1,17 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.create.creators - -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos -import eu.kanade.tachiyomi.data.backup.models.backupExtensionReposMapper -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionRepoBackupCreator( - private val getExtensionRepos: GetExtensionRepo = Injekt.get(), -) { - - suspend operator fun invoke(): List { - return getExtensionRepos.getAll() - .map(backupExtensionReposMapper) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt new file mode 100644 index 000000000..0ca4aa59e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.backup.create.creators + +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore +import eu.kanade.tachiyomi.data.backup.models.backupExtensionStoreMapper +import mihon.domain.extension.interactor.GetExtensionStores +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionStoresBackupCreator( + private val getExtensionStores: GetExtensionStores = Injekt.get(), +) { + + suspend operator fun invoke(): List { + return getExtensionStores.get() + .map(backupExtensionStoreMapper) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt index cfb6198c2..7c37ef1d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt @@ -11,7 +11,7 @@ data class Backup( @ProtoNumber(101) var backupSources: List = emptyList(), @ProtoNumber(104) var backupPreferences: List = emptyList(), @ProtoNumber(105) var backupSourcePreferences: List = emptyList(), - @ProtoNumber(106) var backupExtensionRepo: List = emptyList(), + @ProtoNumber(106) var backupExtensionStores: List = emptyList(), // SY specific values @ProtoNumber(600) var backupSavedSearches: List = emptyList(), // KMK --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt deleted file mode 100644 index 256def7b5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.models - -import kotlinx.serialization.Serializable -import kotlinx.serialization.protobuf.ProtoNumber -import mihon.domain.extensionrepo.model.ExtensionRepo - -@Serializable -class BackupExtensionRepos( - @ProtoNumber(1) var baseUrl: String, - @ProtoNumber(2) var name: String, - @ProtoNumber(3) var shortName: String?, - @ProtoNumber(4) var website: String, - @ProtoNumber(5) var signingKeyFingerprint: String, -) - -val backupExtensionReposMapper = { repo: ExtensionRepo -> - BackupExtensionRepos( - baseUrl = repo.baseUrl, - name = repo.name, - shortName = repo.shortName, - website = repo.website, - signingKeyFingerprint = repo.signingKeyFingerprint, - ) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt new file mode 100644 index 000000000..2304f260c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.data.backup.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import mihon.domain.extension.model.ExtensionStore + +@Serializable +class BackupExtensionStore( + @ProtoNumber(1) var indexUrl: String, + @ProtoNumber(2) var name: String, + @ProtoNumber(3) var badgeLabel: String?, + @ProtoNumber(5) var signingKey: String, + @ProtoNumber(4) var contactWebsite: String, + @ProtoNumber(6) var contactDiscord: String?, + @ProtoNumber(7) var isLegacy: Boolean?, +) + +val backupExtensionStoreMapper = { store: ExtensionStore -> + BackupExtensionStore( + indexUrl = store.indexUrl, + name = store.name, + badgeLabel = store.badgeLabel, + signingKey = store.signingKey, + contactWebsite = store.contact.website, + contactDiscord = store.contact.discord, + isLegacy = store.isLegacy, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt index 3f8d6ef22..9f578bbd3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt @@ -5,14 +5,14 @@ import android.net.Uri import eu.kanade.tachiyomi.data.backup.BackupDecoder import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.models.BackupCategory -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore import eu.kanade.tachiyomi.data.backup.models.BackupFeed import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer -import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionRepoRestorer +import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionStoreRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.FeedRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer @@ -38,7 +38,7 @@ class BackupRestorer( private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(), private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context), - private val extensionRepoRestorer: ExtensionRepoRestorer = ExtensionRepoRestorer(), + private val extensionStoreRestorer: ExtensionStoreRestorer = ExtensionStoreRestorer(), private val mangaRestorer: MangaRestorer = MangaRestorer(isSync), // SY --> private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(), @@ -96,8 +96,8 @@ class BackupRestorer( if (options.appSettings) { restoreAmount += 1 } - if (options.extensionRepoSettings) { - restoreAmount += backup.backupExtensionRepo.size + if (options.extensionStores) { + restoreAmount += backup.backupExtensionStores.size } if (options.sourceSettings) { restoreAmount += 1 @@ -126,8 +126,8 @@ class BackupRestorer( if (options.libraryEntries) { restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList()) } - if (options.extensionRepoSettings) { - restoreExtensionRepos(backup.backupExtensionRepo) + if (options.extensionStores) { + restoreExtensionStores(backup.backupExtensionStores) } // TODO: optionally trigger online library + tracker update @@ -248,23 +248,25 @@ class BackupRestorer( } } - private fun CoroutineScope.restoreExtensionRepos( - backupExtensionRepo: List, + private fun CoroutineScope.restoreExtensionStores( + backupExtensionStores: List, ) = launch { - backupExtensionRepo + backupExtensionStores .forEach { ensureActive() try { - extensionRepoRestorer(it) + extensionStoreRestorer(it) } catch (e: Exception) { - errors.add(Date() to "Error Adding Repo: ${it.name} : ${e.message}") + errors.add(Date() to "Error adding extension store: ${it.name} : ${e.message}") } restoreProgress += 1 + // KMK --> with(notifier) { + // KMK <-- showRestoreProgress( - context.stringResource(MR.strings.extensionRepo_settings), + context.stringResource(MR.strings.extensionStores), restoreProgress, restoreAmount, isSync, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt index 182b679d0..df62c7412 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt @@ -9,7 +9,7 @@ data class RestoreOptions( val libraryEntries: Boolean = true, val categories: Boolean = true, val appSettings: Boolean = true, - val extensionRepoSettings: Boolean = true, + val extensionStores: Boolean = true, val sourceSettings: Boolean = true, // SY --> val savedSearchesFeeds: Boolean = true, @@ -20,7 +20,7 @@ data class RestoreOptions( libraryEntries, categories, appSettings, - extensionRepoSettings, + extensionStores, sourceSettings, // SY --> savedSearchesFeeds, @@ -31,7 +31,7 @@ data class RestoreOptions( libraryEntries || categories || appSettings || - extensionRepoSettings || + extensionStores || sourceSettings /* SY --> */ || savedSearchesFeeds /* SY <-- */ @@ -53,9 +53,9 @@ data class RestoreOptions( setter = { options, enabled -> options.copy(appSettings = enabled) }, ), Entry( - label = MR.strings.extensionRepo_settings, - getter = RestoreOptions::extensionRepoSettings, - setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) }, + label = MR.strings.extensionStores, + getter = RestoreOptions::extensionStores, + setter = { options, enabled -> options.copy(extensionStores = enabled) }, ), Entry( label = MR.strings.source_settings, @@ -77,7 +77,7 @@ data class RestoreOptions( libraryEntries = array[0], categories = array[1], appSettings = array[2], - extensionRepoSettings = array[3], + extensionStores = array[3], sourceSettings = array[4], // SY --> savedSearchesFeeds = array[5], diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt deleted file mode 100644 index 006e48df1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.restore.restorers - -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import tachiyomi.data.DatabaseHandler -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionRepoRestorer( - private val handler: DatabaseHandler = Injekt.get(), - private val getExtensionRepos: GetExtensionRepo = Injekt.get(), -) { - - suspend operator fun invoke( - backupRepo: BackupExtensionRepos, - ) { - val dbRepos = getExtensionRepos.getAll() - val existingReposBySHA = dbRepos.associateBy { it.signingKeyFingerprint } - val existingReposByUrl = dbRepos.associateBy { it.baseUrl } - - val urlExists = existingReposByUrl[backupRepo.baseUrl] - val shaExists = existingReposBySHA[backupRepo.signingKeyFingerprint] - - if (urlExists != null && urlExists.signingKeyFingerprint != backupRepo.signingKeyFingerprint) { - error("Already Exists with different signing key fingerprint") - } else if (shaExists != null) { - error("${shaExists.name} has the same signing key fingerprint") - } else { - handler.await { - extension_reposQueries.insert( - backupRepo.baseUrl, - backupRepo.name, - backupRepo.shortName, - backupRepo.website, - backupRepo.signingKeyFingerprint, - ) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt new file mode 100644 index 000000000..70702546d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.data.backup.restore.restorers + +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore +import tachiyomi.data.Database +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionStoreRestorer( + private val database: Database = Injekt.get(), +) { + + suspend operator fun invoke( + backupStore: BackupExtensionStore, + ) { + database.extension_storeQueries.upsert( + indexUrl = backupStore.indexUrl, + name = backupStore.name, + badgeLabel = backupStore.badgeLabel ?: backupStore.name, + signingKey = backupStore.signingKey, + contactWebsite = backupStore.contactWebsite, + contactDiscord = backupStore.contactDiscord, + isLegacy = backupStore.isLegacy ?: true, + ) + } +} 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 ec289d2e7..1fa3c2929 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 @@ -88,7 +88,7 @@ class SyncManager( chapters = syncOptions.chapters, tracking = syncOptions.tracking, history = syncOptions.history, - extensionRepoSettings = syncOptions.extensionRepoSettings, + extensionStores = syncOptions.extensionStores, appSettings = syncOptions.appSettings, sourceSettings = syncOptions.sourceSettings, privateSettings = syncOptions.privateSettings, @@ -107,8 +107,8 @@ class SyncManager( backupCategories = backupCreator.backupCategories(backupOptions), backupSources = backupCreator.backupSources(backupManga), backupPreferences = backupCreator.backupAppPreferences(backupOptions), - backupExtensionRepo = backupCreator.backupExtensionRepos(backupOptions), backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions), + backupExtensionStores = backupCreator.backupExtensionStores(backupOptions), // SY --> backupSavedSearches = backupCreator.backupSavedSearches(backupOptions), @@ -192,7 +192,7 @@ class SyncManager( backupSources = remoteBackup.backupSources, backupPreferences = remoteBackup.backupPreferences, backupSourcePreferences = remoteBackup.backupSourcePreferences, - backupExtensionRepo = remoteBackup.backupExtensionRepo, + backupExtensionStores = remoteBackup.backupExtensionStores, // SY --> backupSavedSearches = remoteBackup.backupSavedSearches, @@ -222,7 +222,7 @@ class SyncManager( appSettings = true, sourceSettings = true, libraryEntries = true, - extensionRepoSettings = true, + extensionStores = true, ), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 477b61be1..1391c9263 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -273,14 +273,14 @@ class ExtensionManager( // Ext found: Update installed extensions with new information from repo // Also clear isObsolete and set new repo Name if needed val hasUpdate = extension.updateExists(availableExt) + // KMK --> installedExtensionsMap[pkgName] = extension.copy( hasUpdate = hasUpdate, - repoUrl = availableExt.repoUrl, - // KMK --> + store = availableExt.store, isObsolete = false, - repoName = extension.repoName ?: availableExt.repoName, - // KMK <-- + storeName = extension.storeName ?: availableExt.storeName, ) + // KMK <-- changed = true } } @@ -298,7 +298,7 @@ class ExtensionManager( * @param extension The extension to be installed. */ fun installExtension(extension: Extension.Available): Flow { - return installer.downloadAndInstall(api.getApkUrl(extension), extension) + return installer.downloadAndInstall(extension.apkUrl, extension) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt index 1cefa8afd..5c7f0f18a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt @@ -6,24 +6,13 @@ import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.util.ExtensionLoader -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.network.parseAs import exh.source.BlacklistedSources import exh.source.ExhPreferences -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import logcat.LogPriority -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo -import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extension.interactor.UpdateExtensionStores +import mihon.domain.extension.repository.ExtensionStoreRepository import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -32,19 +21,16 @@ import kotlin.time.Duration.Companion.days internal class ExtensionApi { - private val networkService: NetworkHelper by injectLazy() + private val repository: ExtensionStoreRepository by injectLazy() + private val preferenceStore: PreferenceStore by injectLazy() - private val getExtensionRepo: GetExtensionRepo by injectLazy() - private val updateExtensionRepo: UpdateExtensionRepo by injectLazy() + private val updateExtensionStores: UpdateExtensionStores by injectLazy() private val extensionManager: ExtensionManager by injectLazy() // SY --> private val sourcePreferences: SourcePreferences by injectLazy() - // SY <-- - private val json: Json by injectLazy() - private val lastExtCheck: Preference by lazy { preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0) } @@ -54,37 +40,11 @@ internal class ExtensionApi { val disabledRepos = sourcePreferences.disabledRepos().get() // KMK <-- return withIOContext { - getExtensionRepo.getAll() + repository.fetchExtensions( // KMK --> - .filterNot { it.baseUrl in disabledRepos } + disabledRepos, // KMK <-- - .map { async { getExtensions(it) } } - .awaitAll() - .flatten() - } - } - - private suspend fun getExtensions(extRepo: ExtensionRepo): List { - val repoBaseUrl = extRepo.baseUrl - return try { - val response = networkService.client - .newCall(GET("$repoBaseUrl/index.min.json")) - .awaitSuccess() - - with(json) { - response - .parseAs>() - .toExtensions( - repoBaseUrl, - // KMK --> - signature = extRepo.signingKeyFingerprint, - repoName = extRepo.shortName ?: extRepo.name, - // KMK <-- - ) - } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" } - emptyList() + ) } } @@ -99,8 +59,7 @@ internal class ExtensionApi { return null } - // Update extension repo details - updateExtensionRepo.awaitAll() + updateExtensionStores() val extensions = if (fromAvailableExtensionList) { extensionManager.availableExtensionsFlow.value @@ -138,47 +97,6 @@ internal class ExtensionApi { return extensionsWithUpdate } - private fun List.toExtensions( - repoUrl: String, - // KMK --> - signature: String, - repoName: String, - // KMK <-- - ): List { - return this - .filter { - val libVersion = it.extractLibVersion() - libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX - } - .map { - Extension.Available( - name = it.name.substringAfter("Tachiyomi: "), - pkgName = it.pkg, - versionName = it.version, - versionCode = it.code, - libVersion = it.extractLibVersion(), - lang = it.lang, - isNsfw = it.nsfw == 1, - sources = it.sources?.map(extensionSourceMapper).orEmpty(), - apkName = it.apk, - iconUrl = "$repoUrl/icon/${it.pkg}.png", - repoUrl = repoUrl, - // KMK --> - signatureHash = signature, - repoName = repoName, - // KMK <-- - ) - } - } - - fun getApkUrl(extension: Extension.Available): String { - return "${extension.repoUrl}/apk/${extension.apkName}" - } - - private fun ExtensionJsonObject.extractLibVersion(): Double { - return version.substringBeforeLast('.').toDouble() - } - // SY --> private fun Extension.isBlacklisted( blacklistEnabled: Boolean = sourcePreferences.enableSourceBlacklist().get(), @@ -194,32 +112,3 @@ internal class ExtensionApi { } // SY <-- } - -@Serializable -private data class ExtensionJsonObject( - val name: String, - val pkg: String, - val apk: String, - val lang: String, - val code: Long, - val version: String, - val nsfw: Int, - val sources: List?, -) - -@Serializable -private data class ExtensionSourceJsonObject( - val id: Long, - val lang: String, - val name: String, - val baseUrl: String, -) - -private val extensionSourceMapper: (ExtensionSourceJsonObject) -> Extension.Available.Source = { - Extension.Available.Source( - id = it.id, - lang = it.lang, - name = it.name, - baseUrl = it.baseUrl, - ) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 78866756d..b72b6c9c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -20,8 +20,8 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import logcat.LogPriority -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extension.interactor.GetExtensionStores +import mihon.domain.extension.model.ExtensionStore import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.injectLazy import java.io.File @@ -46,7 +46,7 @@ internal object ExtensionLoader { private val trustExtension: TrustExtension by injectLazy() // KMK --> - private val getExtensionRepo: GetExtensionRepo by injectLazy() + private val getExtensionStores: GetExtensionStores by injectLazy() // KMK <-- private val loadNsfwSource by lazy { @@ -169,7 +169,7 @@ internal object ExtensionLoader { // Load each extension concurrently and wait for completion return runBlocking { // KMK --> - val extRepos = getExtensionRepo.getAll() + val extStores = getExtensionStores.get() // KMK <-- val deferred = extPkgs.map { async { @@ -177,7 +177,7 @@ internal object ExtensionLoader { context, it, // KMK --> - extRepos, + extStores, // KMK <-- ) } @@ -228,7 +228,7 @@ internal object ExtensionLoader { isShared = true, ) } - } catch (error: PackageManager.NameNotFoundException) { + } catch (_: PackageManager.NameNotFoundException) { null } @@ -245,11 +245,11 @@ internal object ExtensionLoader { context: Context, extensionInfo: ExtensionInfo, // KMK --> - extRepos: List? = null, + extStores: List? = null, // KMK <-- ): LoadResult { // KMK --> - val repos = extRepos ?: getExtensionRepo.getAll() + val stores = extStores ?: getExtensionStores.get() // KMK <-- val pkgManager = context.packageManager val pkgInfo = extensionInfo.packageInfo @@ -288,10 +288,10 @@ internal object ExtensionLoader { libVersion, signatures.last(), // KMK --> - repoName = repos.firstOrNull { repo -> - signatures.all { it == repo.signingKeyFingerprint } - }?.let { repo -> - repo.shortName.takeIf { !it.isNullOrBlank() } ?: repo.name + storeName = stores.firstOrNull { store -> + signatures.all { it == store.signingKey } + }?.let { store -> + store.badgeLabel.takeIf(String::isNotBlank) ?: store.name }, // KMK <-- ) @@ -358,10 +358,10 @@ internal object ExtensionLoader { isShared = extensionInfo.isShared, // KMK --> signatureHash = signatures.last(), - repoName = repos.firstOrNull { repo -> - signatures.all { it == repo.signingKeyFingerprint } - }?.let { repo -> - repo.shortName.takeIf { !it.isNullOrBlank() } ?: repo.name + storeName = stores.firstOrNull { store -> + signatures.all { it == store.signingKey } + }?.let { store -> + store.badgeLabel.takeIf(String::isNotBlank) ?: store.name }, // KMK <-- ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt index d1669f879..659cb7cd3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt @@ -59,7 +59,7 @@ class ExtensionsScreen(private val searchSource: String? = null) : Screen() { /* * This will redo the searching for [searchSource] every times the screen is launched, for example when - * back from the [ExtensionFilterScreen] or from the [ExtensionReposScreen]. + * back from the [ExtensionFilterScreen] or from the [ExtensionStoresScreen]. * Not really desired but let's accept it. */ if (!searchSource.isNullOrBlank()) onChangeSearchQuery(searchSource) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt index ac9d8e920..1f4b1a244 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt @@ -20,7 +20,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.ExtensionScreen import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.TabContent -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen @@ -62,8 +62,8 @@ fun extensionsTab( onClick = { navigator.push(ExtensionFilterScreen()) }, ), AppBar.OverflowAction( - title = stringResource(MR.strings.label_extension_repos), - onClick = { navigator.push(ExtensionReposScreen()) }, + title = stringResource(MR.strings.extensionStores), + onClick = { navigator.push(ExtensionStoresScreen()) }, ), ), content = { contentPadding, _ -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 0a0f721f0..43bcfc49d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -66,7 +66,7 @@ import eu.kanade.presentation.components.UpdatingBannerBackgroundColor import eu.kanade.presentation.more.settings.screen.ConfigureExhDialog import eu.kanade.presentation.more.settings.screen.about.AboutScreen.Companion.getReleaseNotes import eu.kanade.presentation.more.settings.screen.about.WhatsNewDialog -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.DefaultNavigatorScreenTransition @@ -693,11 +693,11 @@ class MainActivity : BaseActivity() { navigator.popUntilRoot() navigator.push(RestoreBackupScreen(intent.data.toString())) } - // Deep link to add extension repo - else if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") { + // Deep link to add extension store + else if (intent.isAddExtensionStoreIntent()) { intent.data?.getQueryParameter("url")?.let { repoUrl -> navigator.popUntilRoot() - navigator.push(ExtensionReposScreen(repoUrl)) + navigator.push(ExtensionStoresScreen(repoUrl)) } } null @@ -713,6 +713,11 @@ class MainActivity : BaseActivity() { return true } + private fun Intent.isAddExtensionStoreIntent(): Boolean { + return (scheme == "tachiyomi" && data?.host == "add-repo") || + (scheme == "mihon" && data?.host == "extension-store") + } + companion object { const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH" const val INTENT_SEARCH_QUERY = "query" diff --git a/app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt b/app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt new file mode 100644 index 000000000..210db5d4b --- /dev/null +++ b/app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt @@ -0,0 +1,30 @@ +package mihon.core.migration.migrations + +import android.app.Application +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import eu.kanade.domain.source.service.SourcePreferences +import mihon.core.migration.Migration +import mihon.core.migration.MigrationContext +import tachiyomi.core.common.util.lang.withIOContext + +class DisabledRepoMigration : Migration { + override val version: Float = 80f + + override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext { + val context = migrationContext.get() ?: return@withIOContext false + val sourcePreferences = migrationContext.get() ?: return@withIOContext false + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + prefs.edit { + val disabledRepos = prefs.getStringSet(sourcePreferences.disabledRepos().key(), emptySet()) ?: return@edit + disabledRepos + .map { + it.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json" + }.toSet() + .let { + putStringSet(sourcePreferences.disabledRepos().key(), it) + } + } + return@withIOContext true + } +} diff --git a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt index c2829db19..0499e441b 100644 --- a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt +++ b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt @@ -52,4 +52,7 @@ val migrations: List TrustExtensionRepositoryMigration(), CategoryPreferencesCleanupMigration(), RemoveDuplicateReaderPreferenceMigration(), + // KMK --> + DisabledRepoMigration(), + // KMK <-- ) diff --git a/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt b/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt index b5dd80ff5..1ef23929a 100644 --- a/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt @@ -4,8 +4,7 @@ import eu.kanade.domain.source.service.SourcePreferences import logcat.LogPriority import mihon.core.migration.Migration import mihon.core.migration.MigrationContext -import mihon.domain.extensionrepo.exception.SaveExtensionRepoException -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import mihon.domain.extension.repository.ExtensionStoreRepository import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat @@ -14,18 +13,14 @@ class TrustExtensionRepositoryMigration : Migration { override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext { val sourcePreferences = migrationContext.get() ?: return@withIOContext false - val extensionRepositoryRepository = - migrationContext.get() ?: return@withIOContext false + val repository = migrationContext.get() ?: return@withIOContext false for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) { try { - extensionRepositoryRepository.upsertRepo( - source, - "Repo #${index + 1}", - null, - source, - "NOFINGERPRINT-${index + 1}", + repository.insertFromPreference( + indexUrl = source.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json", + name = "Repo #${index + 1}", ) - } catch (e: SaveExtensionRepoException) { + } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" } } } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 552bae9b3..9d44c54bf 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -25,7 +25,7 @@ android { kotlin { compilerOptions { - freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi") + optIn.add("kotlinx.serialization.ExperimentalSerializationApi") } } @@ -34,5 +34,9 @@ dependencies { implementation(projects.domain) implementation(projects.core.common) + implementation(kotlinx.serialization.json) + implementation(kotlinx.serialization.json.okio) + implementation(kotlinx.serialization.protobuf) + api(libs.bundles.sqldelight) } diff --git a/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt b/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt new file mode 100644 index 000000000..fa04cfc75 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt @@ -0,0 +1,7 @@ +package mihon.data.extension.model + +import mihon.domain.extension.model.ExtensionStore + +interface BaseNetworkExtensionStore { + fun toExtensionStore(indexUrl: String): ExtensionStore +} diff --git a/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt b/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt new file mode 100644 index 000000000..ce051ee2d --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt @@ -0,0 +1,114 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames +import kotlinx.serialization.protobuf.ProtoNumber +import mihon.domain.extension.model.ExtensionStore + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class NetworkExtensionStore( + @ProtoNumber(1) val name: String, + @ProtoNumber(2) val badgeLabel: String, + @ProtoNumber(3) val signingKey: String, + @ProtoNumber(4) val contact: Contact, + @ProtoNumber(5) val extensions: List, +) : BaseNetworkExtensionStore { + @Serializable + data class Contact( + @ProtoNumber(1) val website: String, + @ProtoNumber(2) val discord: String?, + ) + + @Serializable + data class Extension( + @ProtoNumber(1) val name: String, + @ProtoNumber(2) val packageName: String, + @ProtoNumber(3) val resources: Resources, + @ProtoNumber(4) val extensionLib: String, + @ProtoNumber(5) val versionCode: Long, + @ProtoNumber(6) val versionName: String, + @ProtoNumber(7) val sources: List, + ) + + @Serializable + data class Resources( + @ProtoNumber(1) val apkUrl: String, + @ProtoNumber(2) val iconUrl: String, + ) + + @Serializable + data class Source( + @ProtoNumber(1) val id: Long, + @ProtoNumber(2) val name: String, + @ProtoNumber(3) val language: String, + @ProtoNumber(4) val homeUrl: String = "", + @ProtoNumber(5) val mirrorUrls: List = emptyList(), + @ProtoNumber(6) val contentRating: ContentRating = ContentRating.SAFE, + @ProtoNumber(7) val message: String? = null, + ) + + @Suppress("Unused") + enum class ContentRating { + @ProtoNumber(0) + @JsonNames("CONTENT_RATING_SAFE") + SAFE, + + @ProtoNumber(1) + @JsonNames("CONTENT_RATING_SUGGESTIVE") + SUGGESTIVE, + + @ProtoNumber(2) + @JsonNames("CONTENT_RATING_EROTICA") + EROTICA, + + @ProtoNumber(3) + @JsonNames("CONTENT_RATING_PORNOGRAPHIC") + PORNOGRAPHIC, + } + + override fun toExtensionStore(indexUrl: String): ExtensionStore { + return ExtensionStore( + indexUrl = indexUrl, + name = name, + badgeLabel = badgeLabel, + signingKey = signingKey, + contact = ExtensionStore.Contact( + website = contact.website, + discord = contact.discord, + ), + isLegacy = false, + ) + } + + fun toAvailableExtensions(store: ExtensionStore): List { + return extensions.map { extension -> + val lang = extension.sources.map { it.language }.toSet() + eu.kanade.tachiyomi.extension.model.Extension.Available( + name = extension.name, + pkgName = extension.packageName, + apkUrl = extension.resources.apkUrl, + iconUrl = extension.resources.iconUrl, + libVersion = extension.extensionLib.toDouble(), + versionCode = extension.versionCode, + versionName = extension.versionName, + lang = if (lang.size == 1) lang.first() else "all", + isNsfw = extension.sources.maxOfOrNull { it.contentRating } == ContentRating.PORNOGRAPHIC, + // KMK --> + signatureHash = store.signingKey, + storeName = store.name, + // KMK <-- + sources = extension.sources.map { source -> + eu.kanade.tachiyomi.extension.model.Extension.Available.Source( + id = source.id, + name = source.name, + lang = source.language, + baseUrl = source.homeUrl, + ) + }, + store = store, + ) + } + } +} diff --git a/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt new file mode 100644 index 000000000..85d18b21a --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt @@ -0,0 +1,65 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import eu.kanade.tachiyomi.extension.model.Extension +import kotlinx.serialization.Serializable +import mihon.domain.extension.model.ExtensionStore + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class NetworkLegacyExtension( + val name: String, + val pkg: String, + val apk: String, + val lang: String, + val code: Long, + val version: String, + val nsfw: Int, + val sources: List?, +) { + @Serializable + data class Source( + val id: Long, + val lang: String, + val name: String, + val baseUrl: String, + ) + + fun toAvailableExtension(store: ExtensionStore, storeBaseUrl: String): Extension.Available { + return Extension.Available( + name = name.substringAfter("Tachiyomi: "), + pkgName = pkg, + apkUrl = "$storeBaseUrl/apk/$apk", + iconUrl = "$storeBaseUrl/icon/$pkg.png", + libVersion = version.substringBeforeLast('.').toDouble(), + versionCode = code, + versionName = version, + lang = lang, + isNsfw = nsfw == 1, + // KMK --> + signatureHash = store.signingKey, + storeName = store.name, + // KMK <-- + sources = if (sources.isNullOrEmpty()) { + listOf( + Extension.Available.Source( + id = 0, + name = name, + lang = lang, + baseUrl = "", + ), + ) + } else { + sources.map { source -> + Extension.Available.Source( + id = source.id, + name = source.name, + lang = source.lang, + baseUrl = source.baseUrl, + ) + } + }, + store = store, + ) + } +} diff --git a/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt new file mode 100644 index 000000000..09a563c30 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt @@ -0,0 +1,33 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import kotlinx.serialization.Serializable +import mihon.domain.extension.model.ExtensionStore + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class NetworkLegacyExtensionRepo( + val meta: Meta, +) : BaseNetworkExtensionStore { + @Serializable + data class Meta( + val name: String, + val shortName: String?, + val website: String, + val signingKeyFingerprint: String, + ) + + override fun toExtensionStore(indexUrl: String): ExtensionStore { + return ExtensionStore( + indexUrl = indexUrl, + name = meta.name, + badgeLabel = meta.shortName ?: meta.name, + signingKey = meta.signingKeyFingerprint, + contact = ExtensionStore.Contact( + website = meta.website, + discord = null, + ), + isLegacy = true, + ) + } +} diff --git a/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt new file mode 100644 index 000000000..9be97b8f5 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt @@ -0,0 +1,138 @@ +package mihon.data.extension.repository + +import eu.kanade.tachiyomi.extension.model.Extension +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.supervisorScope +import logcat.LogPriority +import mihon.data.extension.service.ExtensionStoreService +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.repository.ExtensionStoreRepository +import tachiyomi.core.common.util.system.logcat +import tachiyomi.data.DatabaseHandler + +class ExtensionStoreRepositoryImpl( + private val service: ExtensionStoreService, + private val handler: DatabaseHandler, +) : ExtensionStoreRepository { + override suspend fun insert(indexUrl: String): Result { + return service.fetch(indexUrl).mapCatching { upsert(it) } + } + + override suspend fun insertFromPreference(indexUrl: String, name: String) { + handler.await { + extension_storeQueries.upsert( + indexUrl = indexUrl, + name = name, + badgeLabel = name, + signingKey = "NO_SIGNING_KEY", + contactWebsite = indexUrl, + contactDiscord = null, + isLegacy = false, + ) + } + } + + override suspend fun refreshAll() { + try { + handler.awaitList { + extension_storeQueries.getAll() + }.forEach { store -> + service.fetch(store.index_url) + .mapCatching { upsert(it) } + .onFailure { + logcat(LogPriority.ERROR, it) { + "Failed to refresh extension store '${store.name} (${store.index_url})'" + } + } + } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + } + } + + private suspend fun upsert(store: ExtensionStore) { + handler.await { + extension_storeQueries.upsert( + indexUrl = store.indexUrl, + name = store.name, + badgeLabel = store.badgeLabel, + signingKey = store.signingKey, + contactWebsite = store.contact.website, + contactDiscord = store.contact.discord, + isLegacy = store.isLegacy, + ) + } + } + + override suspend fun fetchExtensions( + // KMK --> + disabledRepos: Set, + // KMK <-- + ): List { + return try { + supervisorScope { + handler.awaitList { + extension_storeQueries.getAll(::extensionStoreMapper) + } + // KMK --> + .filterNot { it.indexUrl in disabledRepos } + // KMK <-- + .map { store -> + async { + service.getExtensions(store).onFailure { + this@ExtensionStoreRepositoryImpl.logcat(LogPriority.ERROR, it) { + "Failed to fetch extensions for store '${store.name} (${store.indexUrl})'" + } + } + } + } + .awaitAll() + .flatMap { it.getOrDefault(emptyList()) } + } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + emptyList() + } + } + + override suspend fun getAll(): List { + return handler.awaitList { extension_storeQueries.getAll(::extensionStoreMapper) } + } + + override fun getAllAsFlow(): Flow> { + return handler.subscribeToList { extension_storeQueries.getAll(::extensionStoreMapper) } + } + + override fun getCountAsFlow(): Flow { + return handler.subscribeToOne { + extension_storeQueries + .getCount() + } + } + + override suspend fun remove(indexUrl: String) { + handler.await { extension_storeQueries.delete(indexUrl) } + } + + private fun extensionStoreMapper( + indexUrl: String, + name: String, + badgeLabel: String, + signingKey: String, + contactWebsite: String, + contactDiscord: String?, + isLegacy: Boolean, + ): ExtensionStore = ExtensionStore( + indexUrl = indexUrl, + name = name, + badgeLabel = badgeLabel, + signingKey = signingKey, + contact = ExtensionStore.Contact( + website = contactWebsite, + discord = contactDiscord, + ), + isLegacy = isLegacy, + ) +} diff --git a/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt new file mode 100644 index 000000000..64696fee4 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt @@ -0,0 +1,106 @@ +package mihon.data.extension.service + +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.awaitSuccess +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.okio.decodeFromBufferedSource +import kotlinx.serialization.protobuf.ProtoBuf +import logcat.LogPriority +import mihon.data.extension.model.NetworkExtensionStore +import mihon.data.extension.model.NetworkLegacyExtension +import mihon.data.extension.model.NetworkLegacyExtensionRepo +import mihon.domain.extension.model.ExtensionStore +import tachiyomi.core.common.util.system.logcat +import kotlin.coroutines.cancellation.CancellationException + +class ExtensionStoreService( + private val network: NetworkHelper, + private val json: Json, + private val protoBuf: ProtoBuf, +) { + suspend fun fetch(indexUrl: String): Result { + var updatedIndexUrl: String = indexUrl + return try { + val store = network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source -> + try { + protoBuf.decodeFromByteArray(source.peek().readByteArray()) + // KMK --> + } catch (e: Exception) { + if (e is CancellationException) throw e + // KMK <-- + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + try { + json.decodeFromBufferedSource(source.peek()) + // KMK --> + } catch (e: Exception) { + if (e is CancellationException) throw e + // KMK <-- + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + try { + json.decodeFromBufferedSource(source.peek()) + } catch (e: IllegalArgumentException) { + if (!indexUrl.endsWith("/index.min.json")) { + throw e + } + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json") + network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().use { + json.decodeFromBufferedSource(it.body.source()) + } + } + } + } + .toExtensionStore(updatedIndexUrl) + } + Result.success(store) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + Result.failure(e) + } + } + + suspend fun getExtensions(store: ExtensionStore): Result> { + return try { + val extensions = if (!store.isLegacy) { + network.client.newCall(GET(store.indexUrl)).awaitSuccess().use { response -> + val source = response.body.source() + try { + protoBuf.decodeFromByteArray(source.peek().readByteArray()) + .toAvailableExtensions(store) + // KMK --> + } catch (e: Exception) { + if (e is CancellationException) throw e + // KMK <-- + json.decodeFromBufferedSource(source.peek()) + .toAvailableExtensions(store) + } + } + } else { + val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json") + network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess().use { response -> + val source = response.body.source() + json.decodeFromBufferedSource>(source) + .map { it.toAvailableExtension(store, storeBaseUrl) } + } + } + Result.success(extensions) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.failure(e) + } + } +} diff --git a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt b/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt deleted file mode 100644 index 24910d968..000000000 --- a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt +++ /dev/null @@ -1,93 +0,0 @@ -package mihon.data.repository - -import android.database.sqlite.SQLiteException -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import mihon.domain.extensionrepo.exception.SaveExtensionRepoException -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository -import tachiyomi.data.DatabaseHandler - -class ExtensionRepoRepositoryImpl( - private val handler: DatabaseHandler, -) : ExtensionRepoRepository { - override fun subscribeAll(): Flow> { - return handler.subscribeToList { extension_reposQueries.findAll(::mapExtensionRepo) } - } - - override suspend fun getAll(): List { - return handler.awaitList { extension_reposQueries.findAll(::mapExtensionRepo) } - } - - override suspend fun getRepo(baseUrl: String): ExtensionRepo? { - return handler.awaitOneOrNull { extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) } - } - - override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? { - return handler.awaitOneOrNull { - extension_reposQueries.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo) - } - } - - override fun getCount(): Flow { - return handler.subscribeToOne { extension_reposQueries.count() }.map { it.toInt() } - } - - override suspend fun insertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) { - try { - handler.await { extension_reposQueries.insert(baseUrl, name, shortName, website, signingKeyFingerprint) } - } catch (ex: SQLiteException) { - throw SaveExtensionRepoException(ex) - } - } - - override suspend fun upsertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) { - try { - handler.await { extension_reposQueries.upsert(baseUrl, name, shortName, website, signingKeyFingerprint) } - } catch (ex: SQLiteException) { - throw SaveExtensionRepoException(ex) - } - } - - override suspend fun replaceRepo(newRepo: ExtensionRepo) { - handler.await { - extension_reposQueries.replace( - newRepo.baseUrl, - newRepo.name, - newRepo.shortName, - newRepo.website, - newRepo.signingKeyFingerprint, - ) - } - } - - override suspend fun deleteRepo(baseUrl: String) { - return handler.await { extension_reposQueries.delete(baseUrl) } - } - - private fun mapExtensionRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ): ExtensionRepo = ExtensionRepo( - baseUrl = baseUrl, - name = name, - shortName = shortName, - website = website, - signingKeyFingerprint = signingKeyFingerprint, - ) -} diff --git a/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq b/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq deleted file mode 100644 index 6db69132a..000000000 --- a/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq +++ /dev/null @@ -1,57 +0,0 @@ -CREATE TABLE extension_repos ( - base_url TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL, - short_name TEXT, - website TEXT NOT NULL, - signing_key_fingerprint TEXT UNIQUE NOT NULL -); - -findOne: -SELECT * -FROM extension_repos -WHERE base_url = :base_url; - -findOneBySigningKeyFingerprint: -SELECT * -FROM extension_repos -WHERE signing_key_fingerprint = :fingerprint; - -findAll: -SELECT * -FROM extension_repos; - -count: -SELECT COUNT(*) -FROM extension_repos; - -insert: -INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint) -VALUES (:base_url, :name, :short_name, :website, :fingerprint); - -upsert: -INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint) -VALUES (:base_url, :name, :short_name, :website, :fingerprint) -ON CONFLICT(base_url) -DO UPDATE -SET - name = :name, - short_name = :short_name, - website =: website, - signing_key_fingerprint = :fingerprint -WHERE base_url = base_url; - -replace: -INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint) -VALUES (:base_url, :name, :short_name, :website, :fingerprint) -ON CONFLICT(signing_key_fingerprint) -DO UPDATE -SET - base_url = :base_url, - name = :name, - short_name = :short_name, - website =: website -WHERE signing_key_fingerprint = signing_key_fingerprint; - -delete: -DELETE FROM extension_repos -WHERE base_url = :base_url; diff --git a/data/src/main/sqldelight/tachiyomi/data/extension_store.sq b/data/src/main/sqldelight/tachiyomi/data/extension_store.sq new file mode 100644 index 000000000..e6cd2c471 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/extension_store.sq @@ -0,0 +1,42 @@ +import kotlin.Boolean; + +CREATE TABLE extension_store( + index_url TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + badge_label TEXT NOT NULL, + signing_key TEXT NOT NULL, + contact_website TEXT NOT NULL, + contact_discord TEXT, + is_legacy INTEGER AS Boolean NOT NULL +); + +get: +SELECT * +FROM extension_store +WHERE index_url = :indexUrl; + +getAll: +SELECT * +FROM extension_store; + +getCount: +SELECT COUNT(*) +FROM extension_store; + +upsert: +INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy) +VALUES (:indexUrl, :name, :badgeLabel, :signingKey, :contactWebsite, :contactDiscord, :isLegacy) +ON CONFLICT(index_url) +DO UPDATE +SET + name = :name, + badge_label = :badgeLabel, + signing_key = :signingKey, + contact_website = :contactWebsite, + contact_discord = :contactDiscord, + is_legacy = :isLegacy +WHERE index_url = :indexUrl; + +delete: +DELETE FROM extension_store +WHERE index_url = :indexUrl; diff --git a/data/src/main/sqldelight/tachiyomi/migrations/46.sqm b/data/src/main/sqldelight/tachiyomi/migrations/46.sqm new file mode 100644 index 000000000..abe80fe3e --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/46.sqm @@ -0,0 +1,16 @@ +import kotlin.Boolean; + +CREATE TABLE extension_store( + index_url TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + badge_label TEXT NOT NULL, + signing_key TEXT NOT NULL, + contact_website TEXT NOT NULL, + contact_discord TEXT, + is_legacy INTEGER AS Boolean NOT NULL +); + +INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy) +SELECT base_url || '/repo.json', name, coalesce(short_name, name), signing_key_fingerprint, website, NULL, 1 FROM extension_repos; + +DROP TABLE extension_repos; diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt similarity index 86% rename from app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt rename to domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index 3f63534ee..fbf3298d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.model import android.graphics.drawable.Drawable import eu.kanade.tachiyomi.source.Source +import mihon.domain.extension.model.ExtensionStore import tachiyomi.domain.source.model.StubSource sealed class Extension { @@ -16,7 +17,7 @@ sealed class Extension { // KMK --> abstract val signatureHash: String - abstract val repoName: String? + abstract val storeName: String? // KMK <-- data class Installed( @@ -29,8 +30,8 @@ sealed class Extension { override val isNsfw: Boolean, // KMK --> override val signatureHash: String, - /** Guessing repo name from built-in signatures preset */ - override val repoName: String? = null, + /** Guessing store name from built-in signatures preset */ + override val storeName: String? = null, // KMK <-- val pkgFactory: String?, val sources: List, @@ -38,7 +39,7 @@ sealed class Extension { val hasUpdate: Boolean = false, val isObsolete: Boolean = false, val isShared: Boolean, - val repoUrl: String? = null, + val store: ExtensionStore? = null, // SY --> val isRedundant: Boolean = false, // SY <-- @@ -54,12 +55,12 @@ sealed class Extension { override val isNsfw: Boolean, // KMK --> override val signatureHash: String, - override val repoName: String, + override val storeName: String, // KMK <-- val sources: List, - val apkName: String, + val apkUrl: String, val iconUrl: String, - val repoUrl: String, + val store: ExtensionStore, ) : Extension() { data class Source( @@ -87,7 +88,7 @@ sealed class Extension { /* KMK --> */ override /* KMK <-- */ val signatureHash: String, // KMK --> - override val repoName: String? = null, + override val storeName: String? = null, // KMK <-- override val lang: String? = null, override val isNsfw: Boolean = false, diff --git a/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt new file mode 100644 index 000000000..7c10703e5 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt @@ -0,0 +1,11 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class AddExtensionStore( + private val repository: ExtensionStoreRepository, +) { + suspend operator fun invoke(indexUrl: String): Result { + return repository.insert(indexUrl) + } +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt new file mode 100644 index 000000000..2871e7f67 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt @@ -0,0 +1,9 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class GetExtensionStoreCountAsFlow( + private val repository: ExtensionStoreRepository, +) { + operator fun invoke() = repository.getCountAsFlow() +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt new file mode 100644 index 000000000..e463af8d6 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt @@ -0,0 +1,13 @@ +package mihon.domain.extension.interactor + +import kotlinx.coroutines.flow.Flow +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.repository.ExtensionStoreRepository + +class GetExtensionStores( + private val repository: ExtensionStoreRepository, +) { + suspend fun get(): List = repository.getAll() + + fun subscribe(): Flow> = repository.getAllAsFlow() +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt new file mode 100644 index 000000000..42a8c07e2 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt @@ -0,0 +1,11 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class RemoveExtensionStore( + private val repository: ExtensionStoreRepository, +) { + suspend operator fun invoke(indexUrl: String) { + repository.remove(indexUrl) + } +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt b/domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt new file mode 100644 index 000000000..7bbd2812e --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt @@ -0,0 +1,11 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class UpdateExtensionStores( + private val repository: ExtensionStoreRepository, +) { + suspend operator fun invoke() { + repository.refreshAll() + } +} diff --git a/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt new file mode 100644 index 000000000..1ec6a37f0 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt @@ -0,0 +1,21 @@ +package mihon.domain.extension.model + +data class ExtensionStore( + val indexUrl: String, + val name: String, + val badgeLabel: String, + val signingKey: String, + val contact: Contact, + val isLegacy: Boolean, +) { + data class Contact( + val website: String, + val discord: String?, + ) +} + +const val REPO_HELP = "https://komikku-app.github.io/docs/guides/getting-started#adding-sources" + +// cuong-tran's key +const val KOMIKKU_SIGNATURE = "cbec121aa82ebb02aaa73806992e0368a97d47b5451ed6524816d03084c45905" +const val REPO_SIGNATURE = "9add655a78e96c4ec7a53ef89dccb557cb5d767489fac5e785d671a5a75d4da2" diff --git a/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt b/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt new file mode 100644 index 000000000..69a59db06 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt @@ -0,0 +1,27 @@ +package mihon.domain.extension.repository + +import eu.kanade.tachiyomi.extension.model.Extension +import kotlinx.coroutines.flow.Flow +import mihon.domain.extension.model.ExtensionStore + +interface ExtensionStoreRepository { + suspend fun insert(indexUrl: String): Result + + suspend fun insertFromPreference(indexUrl: String, name: String) + + suspend fun refreshAll() + + suspend fun fetchExtensions( + // KMK --> + disabledRepos: Set, + // KMK <-- + ): List + + suspend fun getAll(): List + + fun getAllAsFlow(): Flow> + + fun getCountAsFlow(): Flow + + suspend fun remove(indexUrl: String) +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt b/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt deleted file mode 100644 index 4c6990be0..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt +++ /dev/null @@ -1,10 +0,0 @@ -package mihon.domain.extensionrepo.exception - -import java.io.IOException - -/** - * Exception to abstract over SQLiteException and SQLiteConstraintException for multiplatform. - * - * @param throwable the source throwable to include for tracing. - */ -class SaveExtensionRepoException(throwable: Throwable) : IOException("Error Saving Repository to Database", throwable) diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt deleted file mode 100644 index befffb4dd..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt +++ /dev/null @@ -1,80 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import logcat.LogPriority -import mihon.domain.extensionrepo.exception.SaveExtensionRepoException -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository -import mihon.domain.extensionrepo.service.ExtensionRepoService -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import tachiyomi.core.common.util.system.logcat - -class CreateExtensionRepo( - private val repository: ExtensionRepoRepository, - private val service: ExtensionRepoService, -) { - private val repoRegex = """^https://.*/index\.min\.json$""".toRegex() - - suspend fun await(indexUrl: String): Result { - val formattedIndexUrl = indexUrl.toHttpUrlOrNull() - ?.toString() - ?.takeIf { it.matches(repoRegex) } - ?: return Result.InvalidUrl - - val baseUrl = formattedIndexUrl.removeSuffix("/index.min.json") - return service.fetchRepoDetails(baseUrl)?.let { insert(it) } ?: Result.InvalidUrl - } - - private suspend fun insert(repo: ExtensionRepo): Result { - return try { - repository.insertRepo( - repo.baseUrl, - repo.name, - repo.shortName, - repo.website, - repo.signingKeyFingerprint, - ) - Result.Success - } catch (e: SaveExtensionRepoException) { - logcat(LogPriority.WARN, e) { "SQL Conflict attempting to add new repository ${repo.baseUrl}" } - return handleInsertionError(repo) - } - } - - /** - * Error Handler for insert when there are trying to create new repositories - * - * SaveExtensionRepoException doesn't provide constraint info in exceptions. - * First check if the conflict was on primary key. if so return RepoAlreadyExists - * Then check if the conflict was on fingerprint. if so Return DuplicateFingerprint - * If neither are found, there was some other Error, and return Result.Error - * - * @param repo Extension Repo holder for passing to DB/Error Dialog - */ - private suspend fun handleInsertionError(repo: ExtensionRepo): Result { - val repoExists = repository.getRepo(repo.baseUrl) - if (repoExists != null) { - return Result.RepoAlreadyExists - } - val matchingFingerprintRepo = repository.getRepoBySigningKeyFingerprint(repo.signingKeyFingerprint) - if (matchingFingerprintRepo != null) { - return Result.DuplicateFingerprint(matchingFingerprintRepo, repo) - } - return Result.Error - } - - sealed interface Result { - data class DuplicateFingerprint(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : Result - data object InvalidUrl : Result - data object RepoAlreadyExists : Result - data object Success : Result - data object Error : Result - } - - companion object { - const val REPO_HELP = "https://komikku-app.github.io/docs/guides/getting-started#adding-sources" - - // cuong-tran's key - const val KOMIKKU_SIGNATURE = "cbec121aa82ebb02aaa73806992e0368a97d47b5451ed6524816d03084c45905" - const val REPO_SIGNATURE = "9add655a78e96c4ec7a53ef89dccb557cb5d767489fac5e785d671a5a75d4da2" - } -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt deleted file mode 100644 index 4b4b678e1..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt +++ /dev/null @@ -1,11 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class DeleteExtensionRepo( - private val repository: ExtensionRepoRepository, -) { - suspend fun await(baseUrl: String) { - repository.deleteRepo(baseUrl) - } -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt deleted file mode 100644 index 25b919607..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt +++ /dev/null @@ -1,13 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import kotlinx.coroutines.flow.Flow -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class GetExtensionRepo( - private val repository: ExtensionRepoRepository, -) { - fun subscribeAll(): Flow> = repository.subscribeAll() - - suspend fun getAll(): List = repository.getAll() -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt deleted file mode 100644 index a7c4e7c6d..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt +++ /dev/null @@ -1,9 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class GetExtensionRepoCount( - private val repository: ExtensionRepoRepository, -) { - fun subscribe() = repository.getCount() -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt deleted file mode 100644 index 112ea701c..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt +++ /dev/null @@ -1,12 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class ReplaceExtensionRepo( - private val repository: ExtensionRepoRepository, -) { - suspend fun await(repo: ExtensionRepo) { - repository.replaceRepo(repo) - } -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt deleted file mode 100644 index a393e69d5..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt +++ /dev/null @@ -1,30 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository -import mihon.domain.extensionrepo.service.ExtensionRepoService - -class UpdateExtensionRepo( - private val repository: ExtensionRepoRepository, - private val service: ExtensionRepoService, -) { - - suspend fun awaitAll() = coroutineScope { - repository.getAll() - .map { async { await(it) } } - .awaitAll() - } - - suspend fun await(repo: ExtensionRepo) { - val newRepo = service.fetchRepoDetails(repo.baseUrl) ?: return - if ( - repo.signingKeyFingerprint.startsWith("NOFINGERPRINT") || - repo.signingKeyFingerprint == newRepo.signingKeyFingerprint - ) { - repository.upsertRepo(newRepo) - } - } -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt deleted file mode 100644 index ec9ccca87..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt +++ /dev/null @@ -1,9 +0,0 @@ -package mihon.domain.extensionrepo.model - -data class ExtensionRepo( - val baseUrl: String, - val name: String, - val shortName: String?, - val website: String, - val signingKeyFingerprint: String, -) diff --git a/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt b/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt deleted file mode 100644 index 47be56dcf..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt +++ /dev/null @@ -1,47 +0,0 @@ -package mihon.domain.extensionrepo.repository - -import kotlinx.coroutines.flow.Flow -import mihon.domain.extensionrepo.model.ExtensionRepo - -interface ExtensionRepoRepository { - - fun subscribeAll(): Flow> - - suspend fun getAll(): List - - suspend fun getRepo(baseUrl: String): ExtensionRepo? - - suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? - - fun getCount(): Flow - - suspend fun insertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) - - suspend fun upsertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) - - suspend fun upsertRepo(repo: ExtensionRepo) { - upsertRepo( - baseUrl = repo.baseUrl, - name = repo.name, - shortName = repo.shortName, - website = repo.website, - signingKeyFingerprint = repo.signingKeyFingerprint, - ) - } - - suspend fun replaceRepo(newRepo: ExtensionRepo) - - suspend fun deleteRepo(baseUrl: String) -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt deleted file mode 100644 index 6a0a492de..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt +++ /dev/null @@ -1,27 +0,0 @@ -package mihon.domain.extensionrepo.service - -import kotlinx.serialization.Serializable -import mihon.domain.extensionrepo.model.ExtensionRepo - -@Serializable -data class ExtensionRepoMetaDto( - val meta: ExtensionRepoDto, -) - -@Serializable -data class ExtensionRepoDto( - val name: String, - val shortName: String?, - val website: String, - val signingKeyFingerprint: String, -) - -fun ExtensionRepoMetaDto.toExtensionRepo(baseUrl: String): ExtensionRepo { - return ExtensionRepo( - baseUrl = baseUrl, - name = meta.name, - shortName = meta.shortName, - website = meta.website, - signingKeyFingerprint = meta.signingKeyFingerprint, - ) -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt deleted file mode 100644 index 2d86b89d1..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt +++ /dev/null @@ -1,36 +0,0 @@ -package mihon.domain.extensionrepo.service - -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.network.parseAs -import kotlinx.serialization.json.Json -import logcat.LogPriority -import mihon.domain.extensionrepo.model.ExtensionRepo -import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.core.common.util.system.logcat - -class ExtensionRepoService( - networkHelper: NetworkHelper, - private val json: Json, -) { - val client = networkHelper.client - - suspend fun fetchRepoDetails( - repo: String, - ): ExtensionRepo? { - return withIOContext { - try { - with(json) { - client.newCall(GET("$repo/repo.json")) - .awaitSuccess() - .parseAs() - .toExtensionRepo(baseUrl = repo) - } - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to fetch repo details" } - null - } - } - } -} diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index a1466b4f4..aba5e9ac5 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -364,20 +364,19 @@ Revoke trusted unknown extensions - Extension repos - You have no repos set. - Add repo - Repo URL - Add additional repos to %s. This should be a URL that ends with \"index.min.json\". - This repo already exists! - Delete repo - Invalid repo URL - Do you wish to delete the repo \"%s\"? - Do you wish to add the repo \"%s\"? Open source repo - Replace - Signing Key Fingerprint Already Exists - Repository %1$s has the same Signing Key Fingerprint as %2$s.\nIf this is expected, %2$s will be replaced, otherwise contact your repo maintainer. + + + Extension stores + Add extension store + You haven\'t added an extension store yet + Add extension store + This extension store already exists + Processing… + Store index URL + Do you wish to add the extension store below? + Remove extension store + Do you wish to remove the extension store \"%1$s\" (%2$s)? Fullscreen @@ -567,7 +566,6 @@ What do you want to backup? App settings Source settings - Extension repos Include sensitive settings (e.g., tracker login tokens) All read entries Creating backup From da57b3db07b0ded22ef0958695553f0d374d646f Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:53:01 +0600 Subject: [PATCH 67/73] Change the term "Obsolete" to "Orphaned" for extensions (mihonapp/mihon#3383) (cherry picked from commit 5f1d76a73a7b821542d46c96935def9158e7ff1f) --- CHANGELOG.md | 1 + .../eu/kanade/presentation/browse/ExtensionDetailsScreen.kt | 2 +- app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt | 2 +- i18n/src/commonMain/moko-resources/base/strings.xml | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c9186091..4c5abddca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co ### Changed - Change all reference of extension repo to extension store ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349)) +- Change the term "Obsolete" to "Orphaned" for extensions ([@AntsyLich](https://github.com/AntsyLich)) ([#3383](https://github.com/mihonapp/mihon/pull/3383)) ### Fixed - Add missing `outlineVariant` color to Nord theme ([@CompileConnected](https://github.com/CompileConnected)) ([#3184](https://github.com/mihonapp/mihon/pull/3184)) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index f4fce3aad..55ff3fb23 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -273,7 +273,7 @@ private fun DetailsHeader( appendLine( """ Update available: ${extension.hasUpdate} - Obsolete: ${extension.isObsolete} + Orphaned: ${extension.isObsolete} Shared: ${extension.isShared} """.trimIndent(), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt index 4ac770bbb..1d5ddeff1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -67,7 +67,7 @@ class CrashLogUtil( """ - ${it.name} Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"} - Obsolete: ${it.isObsolete} + Orphaned: ${it.isObsolete} """.trimIndent() } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index aba5e9ac5..e80e42b65 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -330,7 +330,7 @@ Updates pending Update Update all - Obsolete + Orphaned Install Pending Downloading @@ -344,7 +344,7 @@ App info Untrusted extension Malicious extensions can read any stored login credentials or execute arbitrary code.\n\nBy trusting this extension, you accept these risks. - This extension is no longer available. It may not function properly and can cause issues with the app. Uninstalling it is recommended. + This extension is not associated with any store. It will not receive updates, may not function properly, and can cause issues with the app. Uninstalling it is recommended. Do you really want to remove \"%s\" extension? Failed to fetch available extensions Version From ce6a0ea7fa4f04f7f411893bb5b0464590caba9d Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Thu, 9 Jul 2026 02:05:39 +0700 Subject: [PATCH 68/73] fix(extension-store): Migrate to extension store during restore/sync legacy backup (#1746) --- .../backup/restore/restorers/ExtensionStoreRestorer.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt index 70702546d..a424cb3d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt @@ -12,8 +12,15 @@ class ExtensionStoreRestorer( suspend operator fun invoke( backupStore: BackupExtensionStore, ) { + // KMK --> + val indexUrl = if (backupStore.isLegacy == null) { + backupStore.indexUrl.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json" + } else { + backupStore.indexUrl + } + // KMK <-- database.extension_storeQueries.upsert( - indexUrl = backupStore.indexUrl, + indexUrl = indexUrl, name = backupStore.name, badgeLabel = backupStore.badgeLabel ?: backupStore.name, signingKey = backupStore.signingKey, From 1763d947dfff7a6ae3173ddfabec12601203df36 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Thu, 9 Jul 2026 02:43:00 +0700 Subject: [PATCH 69/73] chore(sync): migrate sync preference keys (#1747) --- .../eu/kanade/domain/sync/SyncPreferences.kt | 64 ++++++++--------- .../core/migration/migrations/Migrations.kt | 1 + .../migrations/SyncPrefKeyMigration.kt | 70 +++++++++++++++++++ 3 files changed, 103 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/mihon/core/migration/migrations/SyncPrefKeyMigration.kt diff --git a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt index 2537563a5..7d0fbf1f0 100644 --- a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt @@ -10,8 +10,8 @@ import java.util.UUID class SyncPreferences( private val preferenceStore: PreferenceStore, ) { - fun clientHost() = preferenceStore.getString("sync_client_host", "https://sync.tachiyomi.org") - fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "") + fun clientHost() = preferenceStore.getString("connection_sync_client_host", "https://sync.tachiyomi.org") + fun clientAPIKey() = preferenceStore.getString("connection_sync_client_api_key", "") fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L) fun lastSyncEtag() = preferenceStore.getString("sync_etag", "") @@ -20,19 +20,19 @@ class SyncPreferences( fun syncService() = preferenceStore.getInt("sync_service", 0) // KMK --> - fun webDavUrl() = preferenceStore.getString("webdav_url", "") - fun webDavUsername() = preferenceStore.getString("webdav_username", "") - fun webDavPassword() = preferenceStore.getString("webdav_password", "") - fun webDavFolder() = preferenceStore.getString("webdav_folder", "komikku") + fun webDavUrl() = preferenceStore.getString("connection_webdav_url", "") + fun webDavUsername() = preferenceStore.getString("connection_webdav_username", "") + fun webDavPassword() = preferenceStore.getString("connection_webdav_password", "") + fun webDavFolder() = preferenceStore.getString("connection_webdav_folder", "komikku") // KMK <-- fun googleDriveAccessToken() = preferenceStore.getString( - Preference.appStateKey("google_drive_access_token"), + Preference.appStateKey("connection_google_drive_access_token"), "", ) fun googleDriveRefreshToken() = preferenceStore.getString( - Preference.appStateKey("google_drive_refresh_token"), + Preference.appStateKey("connection_google_drive_refresh_token"), "", ) @@ -55,39 +55,39 @@ class SyncPreferences( fun getSyncSettings(): SyncSettings { return SyncSettings( - libraryEntries = preferenceStore.getBoolean("library_entries", true).get(), - categories = preferenceStore.getBoolean("categories", true).get(), - chapters = preferenceStore.getBoolean("chapters", true).get(), - tracking = preferenceStore.getBoolean("tracking", true).get(), - history = preferenceStore.getBoolean("history", true).get(), - appSettings = preferenceStore.getBoolean("appSettings", true).get(), - extensionStores = preferenceStore.getBoolean("extensionStores", true).get(), - sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(), - privateSettings = preferenceStore.getBoolean("privateSettings", true).get(), + libraryEntries = preferenceStore.getBoolean("sync_library_entries", true).get(), + categories = preferenceStore.getBoolean("sync_categories", true).get(), + chapters = preferenceStore.getBoolean("sync_chapters", true).get(), + tracking = preferenceStore.getBoolean("sync_tracking", true).get(), + history = preferenceStore.getBoolean("sync_history", true).get(), + appSettings = preferenceStore.getBoolean("sync_appSettings", true).get(), + extensionStores = preferenceStore.getBoolean("sync_extensionStores", true).get(), + sourceSettings = preferenceStore.getBoolean("sync_sourceSettings", true).get(), + privateSettings = preferenceStore.getBoolean("sync_privateSettings", true).get(), // SY --> - customInfo = preferenceStore.getBoolean("customInfo", true).get(), - readEntries = preferenceStore.getBoolean("readEntries", true).get(), - savedSearchesFeeds = preferenceStore.getBoolean("savedSearchesFeeds", true).get(), + customInfo = preferenceStore.getBoolean("sync_customInfo", true).get(), + readEntries = preferenceStore.getBoolean("sync_readEntries", true).get(), + savedSearchesFeeds = preferenceStore.getBoolean("sync_savedSearchesFeeds", true).get(), // SY <-- ) } fun setSyncSettings(syncSettings: SyncSettings) { - preferenceStore.getBoolean("library_entries", true).set(syncSettings.libraryEntries) - preferenceStore.getBoolean("categories", true).set(syncSettings.categories) - preferenceStore.getBoolean("chapters", true).set(syncSettings.chapters) - preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking) - preferenceStore.getBoolean("history", true).set(syncSettings.history) - preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings) - preferenceStore.getBoolean("extensionStores", true).set(syncSettings.extensionStores) - preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings) - preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings) + preferenceStore.getBoolean("sync_library_entries", true).set(syncSettings.libraryEntries) + preferenceStore.getBoolean("sync_categories", true).set(syncSettings.categories) + preferenceStore.getBoolean("sync_chapters", true).set(syncSettings.chapters) + preferenceStore.getBoolean("sync_tracking", true).set(syncSettings.tracking) + preferenceStore.getBoolean("sync_history", true).set(syncSettings.history) + preferenceStore.getBoolean("sync_appSettings", true).set(syncSettings.appSettings) + preferenceStore.getBoolean("sync_extensionStores", true).set(syncSettings.extensionStores) + preferenceStore.getBoolean("sync_sourceSettings", true).set(syncSettings.sourceSettings) + preferenceStore.getBoolean("sync_privateSettings", true).set(syncSettings.privateSettings) // SY --> - preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo) - preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries) - preferenceStore.getBoolean("savedSearchesFeeds", true).set(syncSettings.savedSearchesFeeds) + preferenceStore.getBoolean("sync_customInfo", true).set(syncSettings.customInfo) + preferenceStore.getBoolean("sync_readEntries", true).set(syncSettings.readEntries) + preferenceStore.getBoolean("sync_savedSearchesFeeds", true).set(syncSettings.savedSearchesFeeds) // SY <-- } diff --git a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt index 0499e441b..ccb642fca 100644 --- a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt +++ b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt @@ -54,5 +54,6 @@ val migrations: List RemoveDuplicateReaderPreferenceMigration(), // KMK --> DisabledRepoMigration(), + SyncPrefKeyMigration(), // KMK <-- ) diff --git a/app/src/main/java/mihon/core/migration/migrations/SyncPrefKeyMigration.kt b/app/src/main/java/mihon/core/migration/migrations/SyncPrefKeyMigration.kt new file mode 100644 index 000000000..1cdfc7b26 --- /dev/null +++ b/app/src/main/java/mihon/core/migration/migrations/SyncPrefKeyMigration.kt @@ -0,0 +1,70 @@ +package mihon.core.migration.migrations + +import android.app.Application +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import mihon.core.migration.Migration +import mihon.core.migration.MigrationContext +import tachiyomi.core.common.preference.Preference + +/** + * Migrates sync preference storage keys to namespaced forms so that + * connection keys ("connection_*") and sync-toggle keys ("sync_*") no longer + * collide with unrelated preferences. + */ +class SyncPrefKeyMigration : Migration { + override val version: Float = 80f + + override suspend fun invoke(migrationContext: MigrationContext): Boolean { + val context = migrationContext.get() ?: return false + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + val stringRenames = listOf( + // Sync service connection + "sync_client_host" to "connection_sync_client_host", + "sync_client_api_key" to "connection_sync_client_api_key", + // WebDAV connection (KMK) + "webdav_url" to "connection_webdav_url", + "webdav_username" to "connection_webdav_username", + "webdav_password" to "connection_webdav_password", + "webdav_folder" to "connection_webdav_folder", + // Google Drive tokens — appStateKey() prepends "__APP_STATE_" + Preference.appStateKey("google_drive_access_token") to Preference.appStateKey("connection_google_drive_access_token"), + Preference.appStateKey("google_drive_refresh_token") to Preference.appStateKey("connection_google_drive_refresh_token"), + ) + + val booleanRenames = listOf( + // Core sync toggles + "library_entries" to "sync_library_entries", + "categories" to "sync_categories", + "chapters" to "sync_chapters", + "tracking" to "sync_tracking", + "history" to "sync_history", + "appSettings" to "sync_appSettings", + "extensionRepoSettings" to "sync_extensionStores", + "sourceSettings" to "sync_sourceSettings", + "privateSettings" to "sync_privateSettings", + // SY sync toggles + "customInfo" to "sync_customInfo", + "readEntries" to "sync_readEntries", + "savedSearchesFeeds" to "sync_savedSearchesFeeds", + ) + + prefs.edit { + stringRenames.forEach { (oldKey, newKey) -> + if (prefs.contains(oldKey)) { + putString(newKey, prefs.getString(oldKey, null)) + remove(oldKey) + } + } + booleanRenames.forEach { (oldKey, newKey) -> + if (prefs.contains(oldKey)) { + putBoolean(newKey, prefs.getBoolean(oldKey, true)) + remove(oldKey) + } + } + } + + return true + } +} From 5e809fefd49efb5991e7a549b89a5ef12ac1836d Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:29:27 +0600 Subject: [PATCH 70/73] Add auto migration support from legacy extension store index (mihonapp/mihon#3398) (cherry picked from commit d4327c8d48608ebc6eafc4a3df226c31d6586bbd) --- .../extension/model/NetworkLegacyExtensionRepo.kt | 3 +++ .../data/extension/service/ExtensionStoreService.kt | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt index 09a563c30..babc696bb 100644 --- a/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt +++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt @@ -1,12 +1,15 @@ package mihon.data.extension.model import android.annotation.SuppressLint +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import mihon.domain.extension.model.ExtensionStore @SuppressLint("UnsafeOptInUsageError") @Serializable data class NetworkLegacyExtensionRepo( + @SerialName("index_v2") + val indexV2: String?, val meta: Meta, ) : BaseNetworkExtensionStore { @Serializable diff --git a/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt index 64696fee4..de08e9bf4 100644 --- a/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt +++ b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt @@ -22,6 +22,10 @@ class ExtensionStoreService( private val protoBuf: ProtoBuf, ) { suspend fun fetch(indexUrl: String): Result { + return fetch(indexUrl, forceV2 = false) + } + + private suspend fun fetch(indexUrl: String, forceV2: Boolean): Result { var updatedIndexUrl: String = indexUrl return try { val store = network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source -> @@ -40,10 +44,11 @@ class ExtensionStoreService( } catch (e: Exception) { if (e is CancellationException) throw e // KMK <-- + if (forceV2) throw e logcat(LogPriority.ERROR, e) { "Failed to add extension store '$updatedIndexUrl'" } - try { + val legacyIndex = try { json.decodeFromBufferedSource(source.peek()) } catch (e: IllegalArgumentException) { if (!indexUrl.endsWith("/index.min.json")) { @@ -57,6 +62,12 @@ class ExtensionStoreService( json.decodeFromBufferedSource(it.body.source()) } } + + if (legacyIndex.indexV2 != null) { + return fetch(legacyIndex.indexV2, forceV2 = true) + } else { + legacyIndex + } } } .toExtensionStore(updatedIndexUrl) From eb84ab84393411d7f020e6fa1f0cf9cdb71a134b Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+antsylich@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:14:16 +0600 Subject: [PATCH 71/73] Ensure old extension store is removed if index url changes (mihonapp/mihon#3408) (cherry picked from commit c15f7bee85b8fbe258abe81433730aa5bcba58a1) --- .../extension/repository/ExtensionStoreRepositoryImpl.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt index 9be97b8f5..71bb959dd 100644 --- a/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt +++ b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt @@ -40,7 +40,14 @@ class ExtensionStoreRepositoryImpl( extension_storeQueries.getAll() }.forEach { store -> service.fetch(store.index_url) - .mapCatching { upsert(it) } + .mapCatching { + handler.await { + upsert(it) + if (store.index_url != it.indexUrl) { + extension_storeQueries.delete(store.index_url) + } + } + } .onFailure { logcat(LogPriority.ERROR, it) { "Failed to refresh extension store '${store.name} (${store.index_url})'" From 4ae732a17b744951b5d43f2ef07b6507374970a2 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:41:40 +0700 Subject: [PATCH 72/73] fix(deps): Matching coroutines version --- gradle/kotlinx.versions.toml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index b99e97d14..f32ab7994 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -2,6 +2,7 @@ kotlin_version = "2.4.0" serialization_version = "1.10.0" xml_serialization_version = "0.91.3" +coroutines_version = "1.11.0" [libraries] reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" } @@ -10,11 +11,11 @@ compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-grad immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.4.0" } -coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.11.0" } -coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } -coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } -coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" } -coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } +coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "coroutines_version" } +coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines_version" } +coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines_version" } +coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "coroutines_version" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines_version" } serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" } serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization_version" } From 87a173929bd3355e4ad2b66b6025de49c9feeaf5 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sun, 14 Jun 2026 02:10:55 +0600 Subject: [PATCH 73/73] Adapt network implementation for TachiyomiX 1.6 (mihonapp/mihon#3248) (cherry picked from commit 430b13bb81c8f51331109fa3ff35296f8bde9d27) --- .../kanade/tachiyomi/network/HttpException.kt | 13 +++++ .../kanade/tachiyomi/network/NetworkHelper.kt | 2 - .../tachiyomi/network/OkHttpExtensions.kt | 56 ++++++++----------- .../tachiyomi/source/online/HttpSource.kt | 7 +++ 4 files changed, 42 insertions(+), 36 deletions(-) create mode 100644 core/common/src/main/kotlin/eu/kanade/tachiyomi/network/HttpException.kt diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/HttpException.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/HttpException.kt new file mode 100644 index 000000000..55f4b00d6 --- /dev/null +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/HttpException.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.network + +import okhttp3.Response + +/** + * Exception that handles HTTP codes considered not successful by OkHttp. + * Use it to have a standardized error message in the app across the extensions. + * + * @see Response.isSuccessful + * @since tachiyomix 1.6 + * @param code [Int] the HTTP status code + */ +class HttpException(val code: Int) : IllegalStateException("HTTP error $code") diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt index 37dc1e480..090099820 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -82,8 +82,6 @@ import kotlin.random.Random } } - val nonCloudflareClient /* KMK --> */ by lazy /* KMK <-- */ { clientBuilder().build() } - /* SY --> */ open /* SY <-- */ val client /* KMK --> */ by lazy /* KMK <-- */ { clientBuilder() .addInterceptor( diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index 0a860d069..fd7917278 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.network -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.json.Json @@ -23,6 +22,7 @@ import kotlin.coroutines.resumeWithException val jsonMime = "application/json; charset=utf-8".toMediaType() @OptIn(ExperimentalAtomicApi::class) +@Deprecated("Use suspend APIs instead") fun Call.asObservable(): Observable { return Observable.unsafeCreate { subscriber -> // Since Call is a one-shot type, clone it for each new subscriber. @@ -61,6 +61,7 @@ fun Call.asObservable(): Observable { } } +@Deprecated("Use suspend APIs instead") fun Call.asObservableSuccess(): Observable { return asObservable().doOnNext { response -> if (!response.isSuccessful) { @@ -70,35 +71,31 @@ fun Call.asObservableSuccess(): Observable { } } -// Based on https://github.com/gildor/kotlin-coroutines-okhttp -@OptIn(ExperimentalCoroutinesApi::class) +// Based on https://github.com/square/okhttp/blob/master/okhttp-coroutines/src/main/kotlin/okhttp3/coroutines/ExecuteAsync.kt +// and https://github.com/gildor/kotlin-coroutines-okhttp private suspend fun Call.await(callStack: Array): Response { return suspendCancellableCoroutine { continuation -> - val callback = - object : Callback { - override fun onResponse(call: Call, response: Response) { - continuation.resume(response) { - response.body.close() - } - } - - override fun onFailure(call: Call, e: IOException) { - // Don't bother with resuming the continuation if it is already cancelled. - if (continuation.isCancelled) return - val exception = IOException(e.message, e).apply { stackTrace = callStack } - continuation.resumeWithException(exception) - } - } - - enqueue(callback) - continuation.invokeOnCancellation { try { - cancel() - } catch (ex: Throwable) { - // Ignore cancel exception + this.cancel() + } catch (_: Throwable) { + // ignore } } + + this.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + if (continuation.isCancelled) return + val exception = IOException(e.message, e).apply { stackTrace = callStack } + continuation.resumeWithException(exception) + } + + override fun onResponse(call: Call, response: Response) { + continuation.resume(response) { _, value, _ -> + value.close() + } + } + }) } } @@ -108,7 +105,7 @@ suspend fun Call.await(): Response { } /** - * @since extensions-lib 1.5 + * Similar to [await] but throws [HttpException] if [Response.isSuccessful] returns false */ suspend fun Call.awaitSuccess(): Response { val callStack = Exception().stackTrace.run { copyOfRange(1, size) } @@ -148,12 +145,3 @@ fun decodeFromJsonResponse( json.decodeFromBufferedSource(deserializer, it) } } - -/** - * Exception that handles HTTP codes considered not successful by OkHttp. - * Use it to have a standardized error message in the app across the extensions. - * - * @since extensions-lib 1.5 - * @param code [Int] the HTTP status code - */ -class HttpException(val code: Int) : IllegalStateException("HTTP error $code") diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 399ad9fad..762efb2c7 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -158,6 +158,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga(page)")) open fun fetchPopularManga(page: Int): Observable { return client.newCall(popularMangaRequest(page)) @@ -203,6 +204,7 @@ abstract class HttpSource : CatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga(page, query, filters)")) open fun fetchSearchManga( page: Int, @@ -261,6 +263,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates(page)")) open fun fetchLatestUpdates(page: Int): Observable { return client.newCall(latestUpdatesRequest(page)) @@ -305,6 +308,7 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to be updated. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails(manga)")) open fun fetchMangaDetails(manga: SManga): Observable { return client.newCall(mangaDetailsRequest(manga)) @@ -400,6 +404,7 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to look for chapters. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList(manga)")) open fun fetchChapterList(manga: SManga): Observable> { return client.newCall(chapterListRequest(manga)) @@ -453,6 +458,7 @@ abstract class HttpSource : CatalogueSource { * * @param chapter the chapter whose page list has to be fetched. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList(chapter)")) open fun fetchPageList(chapter: SChapter): Observable> { return client.newCall(pageListRequest(chapter)) @@ -499,6 +505,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the page whose source image has to be fetched. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl(page)")) open fun fetchImageUrl(page: Page): Observable { return client.newCall(imageUrlRequest(page))