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