diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1dbee966e6..84b3f13cd6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,7 +11,10 @@ plugins { kotlin("plugin.serialization") } -if (gradle.startParameter.taskRequests.toString().contains("Standard")) { +if (gradle.startParameter.taskRequests + .toString() + .contains("Standard") +) { apply() } @@ -158,7 +161,7 @@ dependencies { implementation(compose.ui.tooling.preview) implementation(compose.ui.util) implementation(compose.accompanist.systemuicontroller) - + implementation(androidx.interpolator) implementation(androidx.paging.runtime) @@ -236,7 +239,6 @@ dependencies { implementation(libs.compose.webview) implementation(libs.compose.grid) - // Logging implementation(libs.logcat) @@ -260,15 +262,17 @@ androidComponents { beforeVariants { variantBuilder -> // Disables standardBenchmark if (variantBuilder.buildType == "benchmark") { - variantBuilder.enable = variantBuilder.productFlavors.containsAll( - listOf("default" to "dev"), - ) + variantBuilder.enable = + variantBuilder.productFlavors.containsAll( + listOf("default" to "dev"), + ) } } onVariants(selector().withFlavor("default" to "standard")) { // Only excluding in standard flavor because this breaks // Layout Inspector's Compose tree - it.packaging.resources.excludes.add("META-INF/*.version") + it.packaging.resources.excludes + .add("META-INF/*.version") } } diff --git a/app/src/main/java/eu/kanade/core/preference/CheckboxState.kt b/app/src/main/java/eu/kanade/core/preference/CheckboxState.kt index 0f59a885db..06c16aee33 100644 --- a/app/src/main/java/eu/kanade/core/preference/CheckboxState.kt +++ b/app/src/main/java/eu/kanade/core/preference/CheckboxState.kt @@ -3,8 +3,9 @@ package eu.kanade.core.preference import androidx.compose.ui.state.ToggleableState import tachiyomi.core.common.preference.CheckboxState -fun CheckboxState.TriState.asToggleableState() = when (this) { - is CheckboxState.TriState.Exclude -> ToggleableState.Indeterminate - is CheckboxState.TriState.Include -> ToggleableState.On - is CheckboxState.TriState.None -> ToggleableState.Off -} +fun CheckboxState.TriState.asToggleableState() = + when (this) { + is CheckboxState.TriState.Exclude -> ToggleableState.Indeterminate + is CheckboxState.TriState.Include -> ToggleableState.On + is CheckboxState.TriState.None -> ToggleableState.Off + } diff --git a/app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt b/app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt index 1c7d9e7617..5e77a3bb2c 100644 --- a/app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt +++ b/app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt @@ -11,11 +11,11 @@ class PreferenceMutableState( private val preference: Preference, scope: CoroutineScope, ) : MutableState { - private val state = mutableStateOf(preference.get()) init { - preference.changes() + preference + .changes() .onEach { state.value = it } .launchIn(scope) } @@ -26,13 +26,9 @@ class PreferenceMutableState( preference.set(value) } - override fun component1(): T { - return state.value - } + override fun component1(): T = state.value - override fun component2(): (T) -> Unit { - return preference::set - } + override fun component2(): (T) -> Unit = preference::set } fun Preference.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope) diff --git a/app/src/main/java/eu/kanade/core/util/CollectionUtils.kt b/app/src/main/java/eu/kanade/core/util/CollectionUtils.kt index 8dab8a054c..ab50b826d3 100644 --- a/app/src/main/java/eu/kanade/core/util/CollectionUtils.kt +++ b/app/src/main/java/eu/kanade/core/util/CollectionUtils.kt @@ -4,9 +4,7 @@ import androidx.compose.ui.util.fastForEach import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract -fun List.insertSeparators( - generator: (T?, T?) -> R?, -): List { +fun List.insertSeparators(generator: (T?, T?) -> R?): List { if (isEmpty()) return emptyList() val newList = mutableListOf() for (i in -1..lastIndex) { @@ -19,7 +17,10 @@ fun List.insertSeparators( return newList } -fun HashSet.addOrRemove(value: E, shouldAdd: Boolean) { +fun HashSet.addOrRemove( + value: E, + shouldAdd: Boolean, +) { if (shouldAdd) { add(value) } else { diff --git a/app/src/main/java/eu/kanade/core/util/SourceUtil.kt b/app/src/main/java/eu/kanade/core/util/SourceUtil.kt index c976e1e6ac..163649c62a 100644 --- a/app/src/main/java/eu/kanade/core/util/SourceUtil.kt +++ b/app/src/main/java/eu/kanade/core/util/SourceUtil.kt @@ -8,6 +8,4 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @Composable -fun ifSourcesLoaded(): Boolean { - return remember { Injekt.get().isInitialized }.collectAsState().value -} +fun ifSourcesLoaded(): Boolean = remember { Injekt.get().isInitialized }.collectAsState().value diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 4c769f7037..f8756eaf63 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -97,7 +97,6 @@ import uy.kohesive.injekt.api.addSingletonFactory import uy.kohesive.injekt.api.get class DomainModule : InjektModule { - override fun InjektRegistrar.registerInjectables() { addSingletonFactory { CategoryRepositoryImpl(get()) } addFactory { GetCategories(get()) } diff --git a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt index 1700f0a02f..3c367b6426 100644 --- a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt @@ -10,11 +10,11 @@ class BasePreferences( val context: Context, private val preferenceStore: PreferenceStore, ) { - - fun downloadedOnly() = preferenceStore.getBoolean( - Preference.appStateKey("pref_downloaded_only"), - false, - ) + fun downloadedOnly() = + preferenceStore.getBoolean( + Preference.appStateKey("pref_downloaded_only"), + false, + ) fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false) @@ -22,7 +22,10 @@ class BasePreferences( fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false) - enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) { + enum class ExtensionInstaller( + val titleRes: StringResource, + val requiresSystemPermission: Boolean, + ) { LEGACY(MR.strings.ext_installer_legacy, true), PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true), SHIZUKU(MR.strings.ext_installer_shizuku, false), diff --git a/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt b/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt index 6dd6ef4fa5..ccee631e2f 100644 --- a/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt +++ b/app/src/main/java/eu/kanade/domain/base/ExtensionInstallerPreference.kt @@ -13,24 +13,25 @@ class ExtensionInstallerPreference( private val context: Context, preferenceStore: PreferenceStore, ) : Preference { - private val basePref = preferenceStore.getEnum(key(), defaultValue()) override fun key() = "extension_installer" - val entries get() = ExtensionInstaller.entries.run { + val entries get() = + ExtensionInstaller.entries.run { + if (context.hasMiuiPackageInstaller) { + filter { it != ExtensionInstaller.PACKAGEINSTALLER } + } else { + toList() + } + } + + override fun defaultValue() = if (context.hasMiuiPackageInstaller) { - filter { it != ExtensionInstaller.PACKAGEINSTALLER } + ExtensionInstaller.LEGACY } else { - toList() + ExtensionInstaller.PACKAGEINSTALLER } - } - - override fun defaultValue() = if (context.hasMiuiPackageInstaller) { - ExtensionInstaller.LEGACY - } else { - ExtensionInstaller.PACKAGEINSTALLER - } private fun check(value: ExtensionInstaller): ExtensionInstaller { when (value) { diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/GetAvailableScanlators.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/GetAvailableScanlators.kt index 13bd35e1ff..ba26b281ee 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/GetAvailableScanlators.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/GetAvailableScanlators.kt @@ -7,18 +7,15 @@ import tachiyomi.domain.chapter.repository.ChapterRepository class GetAvailableScanlators( private val repository: ChapterRepository, ) { + private fun List.cleanupAvailableScanlators(): Set = mapNotNull { it.ifBlank { null } }.toSet() - private fun List.cleanupAvailableScanlators(): Set { - return mapNotNull { it.ifBlank { null } }.toSet() - } - - suspend fun await(mangaId: Long): Set { - return repository.getScanlatorsByMangaId(mangaId) + suspend fun await(mangaId: Long): Set = + repository + .getScanlatorsByMangaId(mangaId) .cleanupAvailableScanlators() - } - fun subscribe(mangaId: Long): Flow> { - return repository.getScanlatorsByMangaIdAsFlow(mangaId) + fun subscribe(mangaId: Long): Flow> = + repository + .getScanlatorsByMangaIdAsFlow(mangaId) .map { it.cleanupAvailableScanlators() } - } } diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SetReadStatus.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SetReadStatus.kt index 346384f04f..e65da43c9c 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SetReadStatus.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SetReadStatus.kt @@ -17,7 +17,6 @@ class SetReadStatus( private val mangaRepository: MangaRepository, private val chapterRepository: ChapterRepository, ) { - private val mapper = { chapter: Chapter, read: Boolean -> ChapterUpdate( read = read, @@ -26,55 +25,71 @@ class SetReadStatus( ) } - suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext { - val chaptersToUpdate = chapters.filter { - when (read) { - true -> !it.read - false -> it.read || it.lastPageRead > 0 + suspend fun await( + read: Boolean, + vararg chapters: Chapter, + ): Result = + withNonCancellableContext { + val chaptersToUpdate = + chapters.filter { + when (read) { + true -> !it.read + false -> it.read || it.lastPageRead > 0 + } + } + if (chaptersToUpdate.isEmpty()) { + return@withNonCancellableContext Result.NoChapters } - } - if (chaptersToUpdate.isEmpty()) { - return@withNonCancellableContext Result.NoChapters - } - try { - chapterRepository.updateAll( - chaptersToUpdate.map { mapper(it, read) }, - ) - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - return@withNonCancellableContext Result.InternalError(e) - } + try { + chapterRepository.updateAll( + chaptersToUpdate.map { mapper(it, read) }, + ) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + return@withNonCancellableContext Result.InternalError(e) + } - if (read && downloadPreferences.removeAfterMarkedAsRead().get()) { - chaptersToUpdate - .groupBy { it.mangaId } - .forEach { (mangaId, chapters) -> - deleteDownload.awaitAll( - manga = mangaRepository.getMangaById(mangaId), - chapters = chapters.toTypedArray(), - ) - } - } + if (read && downloadPreferences.removeAfterMarkedAsRead().get()) { + chaptersToUpdate + .groupBy { it.mangaId } + .forEach { (mangaId, chapters) -> + deleteDownload.awaitAll( + manga = mangaRepository.getMangaById(mangaId), + chapters = chapters.toTypedArray(), + ) + } + } - Result.Success - } + Result.Success + } - suspend fun await(mangaId: Long, read: Boolean): Result = withNonCancellableContext { - await( - read = read, - chapters = chapterRepository - .getChapterByMangaId(mangaId) - .toTypedArray(), - ) - } + suspend fun await( + mangaId: Long, + read: Boolean, + ): Result = + withNonCancellableContext { + await( + read = read, + chapters = + chapterRepository + .getChapterByMangaId(mangaId) + .toTypedArray(), + ) + } - suspend fun await(manga: Manga, read: Boolean) = - await(manga.id, read) + suspend fun await( + manga: Manga, + read: Boolean, + ) = await(manga.id, read) sealed interface Result { data object Success : Result + data object NoChapters : Result - data class InternalError(val error: Throwable) : Result + + data class InternalError( + val error: Throwable, + ) : Result } } 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 300794b4c1..0b7944fc20 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 @@ -35,7 +35,6 @@ class SyncChaptersWithSource( private val getChaptersByMangaId: GetChaptersByMangaId, private val getExcludedScanlators: GetExcludedScanlators, ) { - /** * Method to synchronize db chapters with source ones * @@ -58,24 +57,27 @@ class SyncChaptersWithSource( val now = ZonedDateTime.now() val nowMillis = now.toInstant().toEpochMilli() - val sourceChapters = rawSourceChapters - .distinctBy { it.url } - .mapIndexed { i, sChapter -> - Chapter.create() - .copyFromSChapter(sChapter) - .copy(name = with(ChapterSanitizer) { sChapter.name.sanitize(manga.title) }) - .copy(mangaId = manga.id, sourceOrder = i.toLong()) - } + val sourceChapters = + rawSourceChapters + .distinctBy { it.url } + .mapIndexed { i, sChapter -> + Chapter + .create() + .copyFromSChapter(sChapter) + .copy(name = with(ChapterSanitizer) { sChapter.name.sanitize(manga.title) }) + .copy(mangaId = manga.id, sourceOrder = i.toLong()) + } val dbChapters = getChaptersByMangaId.await(manga.id) val newChapters = mutableListOf() val updatedChapters = mutableListOf() - val removedChapters = dbChapters.filterNot { dbChapter -> - sourceChapters.any { sourceChapter -> - dbChapter.url == sourceChapter.url + val removedChapters = + dbChapters.filterNot { dbChapter -> + sourceChapters.any { sourceChapter -> + dbChapter.url == sourceChapter.url + } } - } // Used to not set upload date of older chapters // to a higher value than newer chapters @@ -98,30 +100,36 @@ class SyncChaptersWithSource( val dbChapter = dbChapters.find { it.url == chapter.url } if (dbChapter == null) { - val toAddChapter = if (chapter.dateUpload == 0L) { - val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate - chapter.copy(dateUpload = altDateUpload) - } else { - maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload) - chapter - } + val toAddChapter = + if (chapter.dateUpload == 0L) { + val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate + chapter.copy(dateUpload = altDateUpload) + } else { + maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload) + chapter + } newChapters.add(toAddChapter) } else { if (shouldUpdateDbChapter.await(dbChapter, chapter)) { - val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) && - downloadManager.isChapterDownloaded( - dbChapter.name, dbChapter.scanlator, manga.title, manga.source, - ) + val shouldRenameChapter = + downloadProvider.isChapterDirNameChanged(dbChapter, chapter) && + downloadManager.isChapterDownloaded( + dbChapter.name, + dbChapter.scanlator, + manga.title, + manga.source, + ) if (shouldRenameChapter) { downloadManager.renameChapter(source, manga, dbChapter, chapter) } - var toChangeChapter = dbChapter.copy( - name = chapter.name, - chapterNumber = chapter.chapterNumber, - scanlator = chapter.scanlator, - sourceOrder = chapter.sourceOrder, - ) + var toChangeChapter = + dbChapter.copy( + name = chapter.name, + chapterNumber = chapter.chapterNumber, + scanlator = chapter.scanlator, + sourceOrder = chapter.sourceOrder, + ) if (chapter.dateUpload != 0L) { toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload) } @@ -154,31 +162,35 @@ class SyncChaptersWithSource( deletedChapterNumbers.add(chapter.chapterNumber) } - val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } - .associate { it.chapterNumber to it.dateFetch } + val deletedChapterNumberDateFetchMap = + removedChapters + .sortedByDescending { it.dateFetch } + .associate { it.chapterNumber to it.dateFetch } // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones // Sources MUST return the chapters from most to less recent, which is common. var itemCount = newChapters.size - var updatedToAdd = newChapters.map { toAddItem -> - var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) + var updatedToAdd = + newChapters.map { toAddItem -> + var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) - if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter + if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter - chapter = chapter.copy( - read = chapter.chapterNumber in deletedReadChapterNumbers, - bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers, - ) + chapter = + chapter.copy( + read = chapter.chapterNumber in deletedReadChapterNumbers, + bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers, + ) - // Try to to use the fetch date of the original entry to not pollute 'Updates' tab - deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let { - chapter = chapter.copy(dateFetch = it) - } + // Try to to use the fetch date of the original entry to not pollute 'Updates' tab + deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let { + chapter = chapter.copy(dateFetch = it) + } - reAdded.add(chapter) + reAdded.add(chapter) - chapter - } + chapter + } if (removedChapters.isNotEmpty()) { val toDeleteIds = removedChapters.map { it.id } diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt index 32d2381b11..a3eba3302b 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt @@ -6,37 +6,36 @@ import tachiyomi.domain.chapter.model.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter // TODO: Remove when all deps are migrated -fun Chapter.toSChapter(): SChapter { - return SChapter.create().also { +fun Chapter.toSChapter(): SChapter = + SChapter.create().also { it.url = url it.name = name it.date_upload = dateUpload it.chapter_number = chapterNumber.toFloat() it.scanlator = scanlator } -} -fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter { - return this.copy( +fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter = + this.copy( name = sChapter.name, url = sChapter.url, dateUpload = sChapter.date_upload, chapterNumber = sChapter.chapter_number.toDouble(), scanlator = sChapter.scanlator?.ifBlank { null }?.trim(), ) -} -fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also { - it.id = id - it.manga_id = mangaId - it.url = url - it.name = name - it.scanlator = scanlator - it.read = read - it.bookmark = bookmark - it.last_page_read = lastPageRead.toInt() - it.date_fetch = dateFetch - it.date_upload = dateUpload - it.chapter_number = chapterNumber.toFloat() - it.source_order = sourceOrder.toInt() -} +fun Chapter.toDbChapter(): DbChapter = + ChapterImpl().also { + it.id = id + it.manga_id = mangaId + it.url = url + it.name = name + it.scanlator = scanlator + it.read = read + it.bookmark = bookmark + it.last_page_read = lastPageRead.toInt() + it.date_fetch = dateFetch + it.date_upload = dateUpload + it.chapter_number = chapterNumber.toFloat() + it.source_order = sourceOrder.toInt() + } diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/ChapterFilter.kt b/app/src/main/java/eu/kanade/domain/chapter/model/ChapterFilter.kt index ad476418f0..35dac2f46b 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/model/ChapterFilter.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/model/ChapterFilter.kt @@ -13,7 +13,10 @@ import tachiyomi.source.local.isLocal * Applies the view filters to the list of chapters obtained from the database. * @return an observable of the list of chapters filtered and sorted. */ -fun List.applyFilters(manga: Manga, downloadManager: DownloadManager): List { +fun List.applyFilters( + manga: Manga, + downloadManager: DownloadManager, +): List { val isLocalManga = manga.isLocal() val unreadFilter = manga.unreadFilter val downloadedFilter = manga.downloadedFilter @@ -23,16 +26,16 @@ fun List.applyFilters(manga: Manga, downloadManager: DownloadManager): .filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } } .filter { chapter -> applyFilter(downloadedFilter) { - val downloaded = downloadManager.isChapterDownloaded( - chapter.name, - chapter.scanlator, - manga.title, - manga.source, - ) + val downloaded = + downloadManager.isChapterDownloaded( + chapter.name, + chapter.scanlator, + manga.title, + manga.source, + ) downloaded || isLocalManga } - } - .sortedWith(getChapterSort(manga)) + }.sortedWith(getChapterSort(manga)) } /** diff --git a/app/src/main/java/eu/kanade/domain/download/interactor/DeleteDownload.kt b/app/src/main/java/eu/kanade/domain/download/interactor/DeleteDownload.kt index 18a2bf3f01..9eafcd23ce 100644 --- a/app/src/main/java/eu/kanade/domain/download/interactor/DeleteDownload.kt +++ b/app/src/main/java/eu/kanade/domain/download/interactor/DeleteDownload.kt @@ -10,8 +10,10 @@ class DeleteDownload( private val sourceManager: SourceManager, private val downloadManager: DownloadManager, ) { - - suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext { + suspend fun awaitAll( + manga: Manga, + vararg chapters: Chapter, + ) = withNonCancellableContext { sourceManager.get(manga.source)?.let { source -> downloadManager.deleteChapters(chapters.toList(), manga, source) } 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 968cfbabe7..6f309750dd 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 @@ -10,8 +10,8 @@ class GetExtensionLanguages( private val preferences: SourcePreferences, private val extensionManager: ExtensionManager, ) { - fun subscribe(): Flow> { - return combine( + fun subscribe(): Flow> = + combine( preferences.enabledLanguages().changes(), extensionManager.availableExtensionsFlow, ) { enabledLanguage, availableExtensions -> @@ -22,11 +22,9 @@ class GetExtensionLanguages( } else { ext.sources.map { it.lang } } - } - .distinct() + }.distinct() .sortedWith( compareBy { it !in enabledLanguage }.then(LocaleHelper.comparator), ) } - } } diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt index 7c7a6a9199..e5bdea5741 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt @@ -9,11 +9,14 @@ import kotlinx.coroutines.flow.map class GetExtensionSources( private val preferences: SourcePreferences, ) { - fun subscribe(extension: Extension.Installed): Flow> { val isMultiSource = extension.sources.size > 1 val isMultiLangSingleSource = - isMultiSource && extension.sources.map { it.name }.distinct().size == 1 + isMultiSource && + extension.sources + .map { it.name } + .distinct() + .size == 1 return preferences.disabledSources().changes().map { disabledSources -> fun Source.isEnabled() = id.toString() !in disabledSources 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 4894034072..d776047347 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 @@ -11,7 +11,6 @@ class GetExtensionsByType( private val preferences: SourcePreferences, private val extensionManager: ExtensionManager, ) { - fun subscribe(): Flow { val showNsfwSources = preferences.showNsfwSource().get() @@ -21,38 +20,39 @@ class GetExtensionsByType( extensionManager.untrustedExtensionsFlow, extensionManager.availableExtensionsFlow, ) { enabledLanguages, _installed, _untrusted, _available -> - val (updates, installed) = _installed - .filter { (showNsfwSources || !it.isNsfw) } - .sortedWith( - compareBy { !it.isObsolete } - .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, - ) - .partition { it.hasUpdate } + val (updates, installed) = + _installed + .filter { (showNsfwSources || !it.isNsfw) } + .sortedWith( + compareBy { !it.isObsolete } + .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, + ).partition { it.hasUpdate } - val untrusted = _untrusted - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) + val untrusted = + _untrusted + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) - val available = _available - .filter { extension -> - _installed.none { it.pkgName == extension.pkgName } && - _untrusted.none { it.pkgName == extension.pkgName } && - (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( - name = it.name, - lang = it.lang, - pkgName = "${ext.pkgName}-${it.id}", - sources = listOf(it), - ) + val available = + _available + .filter { extension -> + _installed.none { it.pkgName == extension.pkgName } && + _untrusted.none { it.pkgName == extension.pkgName } && + (showNsfwSources || !extension.isNsfw) + }.flatMap { ext -> + if (ext.sources.isEmpty()) { + return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList() } - } - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) + ext.sources + .filter { it.lang in enabledLanguages } + .map { + ext.copy( + name = it.name, + lang = it.lang, + pkgName = "${ext.pkgName}-${it.id}", + sources = listOf(it), + ) + } + }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) Extensions(updates, installed, available, untrusted) } 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 b4259945d1..001284eedf 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 @@ -10,14 +10,20 @@ class TrustExtension( private val extensionRepoRepository: ExtensionRepoRepository, private val preferences: SourcePreferences, ) { - - suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List): Boolean { + suspend fun isTrusted( + pkgInfo: PackageInfo, + fingerprints: List, + ): Boolean { val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet() val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}" return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get() } - fun trust(pkgName: String, versionCode: Long, signatureHash: String) { + fun trust( + pkgName: String, + versionCode: Long, + signatureHash: String, + ) { preferences.trustedExtensions().getAndSet { exts -> // Remove previously trusted versions val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet() diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt index dc326f209b..630da2fd29 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt @@ -7,18 +7,15 @@ import tachiyomi.data.DatabaseHandler class GetExcludedScanlators( private val handler: DatabaseHandler, ) { + suspend fun await(mangaId: Long): Set = + handler + .awaitList { + excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) + }.toSet() - suspend fun await(mangaId: Long): Set { - return handler.awaitList { - excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) - } - .toSet() - } - - fun subscribe(mangaId: Long): Flow> { - return handler.subscribeToList { - excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) - } - .map { it.toSet() } - } + fun subscribe(mangaId: Long): Flow> = + handler + .subscribeToList { + excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) + }.map { it.toSet() } } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt index a52fb9afd1..bcc7b7403f 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt @@ -5,12 +5,16 @@ import tachiyomi.data.DatabaseHandler class SetExcludedScanlators( private val handler: DatabaseHandler, ) { - - suspend fun await(mangaId: Long, excludedScanlators: Set) { + suspend fun await( + mangaId: Long, + excludedScanlators: Set, + ) { handler.await(inTransaction = true) { - val currentExcluded = handler.awaitList { - excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) - }.toSet() + val currentExcluded = + handler + .awaitList { + excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) + }.toSet() val toAdd = excludedScanlators.minus(currentExcluded) for (scanlator in toAdd) { excluded_scanlatorsQueries.insert(mangaId, scanlator) diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/SetMangaViewerFlags.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/SetMangaViewerFlags.kt index 083d26e985..49caa3dc98 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/SetMangaViewerFlags.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/SetMangaViewerFlags.kt @@ -8,8 +8,10 @@ import tachiyomi.domain.manga.repository.MangaRepository class SetMangaViewerFlags( private val mangaRepository: MangaRepository, ) { - - suspend fun awaitSetReadingMode(id: Long, flag: Long) { + suspend fun awaitSetReadingMode( + id: Long, + flag: Long, + ) { val manga = mangaRepository.getMangaById(id) mangaRepository.update( MangaUpdate( @@ -19,7 +21,10 @@ class SetMangaViewerFlags( ) } - suspend fun awaitSetOrientation(id: Long, flag: Long) { + suspend fun awaitSetOrientation( + id: Long, + flag: Long, + ) { val manga = mangaRepository.getMangaById(id) mangaRepository.update( MangaUpdate( @@ -29,7 +34,8 @@ class SetMangaViewerFlags( ) } - private fun Long.setFlag(flag: Long, mask: Long): Long { - return this and mask.inv() or (flag and mask) - } + private fun Long.setFlag( + flag: Long, + mask: Long, + ): Long = this and mask.inv() or (flag and mask) } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt index d5cfc248dd..1187be27cf 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt @@ -17,14 +17,9 @@ class UpdateManga( private val mangaRepository: MangaRepository, private val fetchInterval: FetchInterval, ) { + suspend fun await(mangaUpdate: MangaUpdate): Boolean = mangaRepository.update(mangaUpdate) - suspend fun await(mangaUpdate: MangaUpdate): Boolean { - return mangaRepository.update(mangaUpdate) - } - - suspend fun awaitAll(mangaUpdates: List): Boolean { - return mangaRepository.updateAll(mangaUpdates) - } + suspend fun awaitAll(mangaUpdates: List): Boolean = mangaRepository.updateAll(mangaUpdates) suspend fun awaitUpdateFromSource( localManga: Manga, @@ -32,11 +27,12 @@ class UpdateManga( manualFetch: Boolean, coverCache: CoverCache = Injekt.get(), ): Boolean { - val remoteTitle = try { - remoteManga.title - } catch (_: UninitializedPropertyAccessException) { - "" - } + val remoteTitle = + try { + remoteManga.title + } catch (_: UninitializedPropertyAccessException) { + "" + } // if the manga isn't a favorite, set its title from source and update in db val title = if (remoteTitle.isEmpty() || localManga.favorite) null else remoteTitle @@ -80,25 +76,26 @@ class UpdateManga( manga: Manga, dateTime: ZonedDateTime = ZonedDateTime.now(), window: Pair = fetchInterval.getWindow(dateTime), - ): Boolean { - return mangaRepository.update( + ): Boolean = + mangaRepository.update( fetchInterval.toMangaUpdate(manga, dateTime, window), ) - } - suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { - return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli())) - } + suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean = + mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli())) - suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean { - return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli())) - } + suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean = + mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli())) - suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean { - val dateAdded = when (favorite) { - true -> Instant.now().toEpochMilli() - false -> 0 - } + suspend fun awaitUpdateFavorite( + mangaId: Long, + favorite: Boolean, + ): Boolean { + val dateAdded = + when (favorite) { + true -> Instant.now().toEpochMilli() + false -> 0 + } return mangaRepository.update( MangaUpdate(id = mangaId, favorite = favorite, dateAdded = dateAdded), ) diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt index ebd2ad3e92..086ce9dffd 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -29,36 +29,37 @@ val Manga.downloadedFilter: TriState else -> TriState.DISABLED } } -fun Manga.chaptersFiltered(): Boolean { - return unreadFilter != TriState.DISABLED || + +fun Manga.chaptersFiltered(): Boolean = + unreadFilter != TriState.DISABLED || downloadedFilter != TriState.DISABLED || bookmarkedFilter != TriState.DISABLED -} -fun Manga.forceDownloaded(): Boolean { - return favorite && Injekt.get().downloadedOnly().get() -} -fun Manga.toSManga(): SManga = SManga.create().also { - it.url = url - it.title = title - it.artist = artist - it.author = author - it.description = description - it.genre = genre.orEmpty().joinToString() - it.status = status.toInt() - it.thumbnail_url = thumbnailUrl - it.initialized = initialized -} +fun Manga.forceDownloaded(): Boolean = favorite && Injekt.get().downloadedOnly().get() + +fun Manga.toSManga(): SManga = + SManga.create().also { + it.url = url + it.title = title + it.artist = artist + it.author = author + it.description = description + it.genre = genre.orEmpty().joinToString() + it.status = status.toInt() + it.thumbnail_url = thumbnailUrl + it.initialized = initialized + } fun Manga.copyFrom(other: SManga): Manga { val author = other.author ?: author val artist = other.artist ?: artist val description = other.description ?: description - val genres = if (other.genre != null) { - other.getGenres() - } else { - genre - } + val genres = + if (other.genre != null) { + other.getGenres() + } else { + genre + } val thumbnailUrl = other.thumbnail_url ?: thumbnailUrl return this.copy( author = author, @@ -72,8 +73,8 @@ fun Manga.copyFrom(other: SManga): Manga { ) } -fun SManga.toDomainManga(sourceId: Long): Manga { - return Manga.create().copy( +fun SManga.toDomainManga(sourceId: Long): Manga = + Manga.create().copy( url = url, title = title, artist = artist, @@ -86,11 +87,8 @@ fun SManga.toDomainManga(sourceId: Long): Manga { initialized = initialized, source = sourceId, ) -} -fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean { - return coverCache.getCustomCoverFile(id).exists() -} +fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean = coverCache.getCustomCoverFile(id).exists() /** * Creates a ComicInfo instance based on the manga and chapter metadata. @@ -104,22 +102,24 @@ fun getComicInfo( ) = ComicInfo( title = ComicInfo.Title(chapter.name), series = ComicInfo.Series(manga.title), - number = chapter.chapterNumber.takeIf { it >= 0 }?.let { - if ((it.rem(1) == 0.0)) { - ComicInfo.Number(it.toInt().toString()) - } else { - ComicInfo.Number(it.toString()) - } - }, + number = + chapter.chapterNumber.takeIf { it >= 0 }?.let { + if ((it.rem(1) == 0.0)) { + ComicInfo.Number(it.toInt().toString()) + } else { + ComicInfo.Number(it.toString()) + } + }, web = ComicInfo.Web(urls.joinToString(" ")), summary = manga.description?.let { ComicInfo.Summary(it) }, writer = manga.author?.let { ComicInfo.Writer(it) }, penciller = manga.artist?.let { ComicInfo.Penciller(it) }, translator = chapter.scanlator?.let { ComicInfo.Translator(it) }, genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) }, - publishingStatus = ComicInfo.PublishingStatusTachiyomi( - ComicInfoPublishingStatus.toComicInfoValue(manga.status), - ), + publishingStatus = + ComicInfo.PublishingStatusTachiyomi( + ComicInfoPublishingStatus.toComicInfoValue(manga.status), + ), categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) }, source = ComicInfo.SourceMihon(sourceName), inker = null, diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt index 046d5055d0..bc72583f6a 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt @@ -14,9 +14,8 @@ class GetEnabledSources( private val repository: SourceRepository, private val preferences: SourcePreferences, ) { - - fun subscribe(): Flow> { - return combine( + fun subscribe(): Flow> = + combine( preferences.pinnedSources().changes(), preferences.enabledLanguages().changes(), preferences.disabledSources().changes(), @@ -36,7 +35,5 @@ class GetEnabledSources( } toFlatten } - } - .distinctUntilChanged() - } + }.distinctUntilChanged() } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetLanguagesWithSources.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetLanguagesWithSources.kt index 9fc927d4e3..6c9b55b25a 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/GetLanguagesWithSources.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetLanguagesWithSources.kt @@ -12,17 +12,17 @@ class GetLanguagesWithSources( private val repository: SourceRepository, private val preferences: SourcePreferences, ) { - - fun subscribe(): Flow>> { - return combine( + fun subscribe(): Flow>> = + combine( preferences.enabledLanguages().changes(), preferences.disabledSources().changes(), repository.getOnlineSources(), ) { enabledLanguage, disabledSource, onlineSources -> - val sortedSources = onlineSources.sortedWith( - compareBy { it.id.toString() in disabledSource } - .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, - ) + val sortedSources = + onlineSources.sortedWith( + compareBy { it.id.toString() in disabledSource } + .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, + ) sortedSources .groupBy { it.lang } @@ -30,5 +30,4 @@ class GetLanguagesWithSources( compareBy { it !in enabledLanguage }.then(LocaleHelper.comparator), ) } - } } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt index 962fbb8a6e..7c7b31cf9a 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt @@ -13,9 +13,8 @@ class GetSourcesWithFavoriteCount( private val repository: SourceRepository, private val preferences: SourcePreferences, ) { - - fun subscribe(): Flow>> { - return combine( + fun subscribe(): Flow>> = + combine( preferences.migrationSortingDirection().changes(), preferences.migrationSortingMode().changes(), repository.getSourcesWithFavoriteCount(), @@ -24,7 +23,6 @@ class GetSourcesWithFavoriteCount( .filterNot { it.first.isLocal() } .sortedWith(sortFn(direction, mode)) } - } private fun sortFn( direction: SetMigrateSorting.Direction, @@ -36,7 +34,10 @@ class GetSourcesWithFavoriteCount( when { a.first.isStub && !b.first.isStub -> -1 b.first.isStub && !a.first.isStub -> 1 - else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase()) + else -> + a.first.name + .lowercase() + .compareToWithCollator(b.first.name.lowercase()) } } SetMigrateSorting.Mode.TOTAL -> { diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt b/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt index 4e4ae4bdd3..c4b7d8f9d7 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt @@ -5,8 +5,10 @@ import eu.kanade.domain.source.service.SourcePreferences class SetMigrateSorting( private val preferences: SourcePreferences, ) { - - fun await(mode: Mode, direction: Direction) { + fun await( + mode: Mode, + direction: Direction, + ) { preferences.migrationSortingMode().set(mode) preferences.migrationSortingDirection().set(direction) } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleLanguage.kt b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleLanguage.kt index c813890d22..6f197d2425 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleLanguage.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleLanguage.kt @@ -6,7 +6,6 @@ import tachiyomi.core.common.preference.getAndSet class ToggleLanguage( val preferences: SourcePreferences, ) { - fun await(language: String) { val isEnabled = language in preferences.enabledLanguages().get() preferences.enabledLanguages().getAndSet { enabled -> diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt index ec13f8d4b5..28cad770b3 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt @@ -7,25 +7,31 @@ import tachiyomi.domain.source.model.Source class ToggleSource( private val preferences: SourcePreferences, ) { - - fun await(source: Source, enable: Boolean = isEnabled(source.id)) { + fun await( + source: Source, + enable: Boolean = isEnabled(source.id), + ) { await(source.id, enable) } - fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) { + fun await( + sourceId: Long, + enable: Boolean = isEnabled(sourceId), + ) { preferences.disabledSources().getAndSet { disabled -> if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId") } } - fun await(sourceIds: List, enable: Boolean) { + fun await( + sourceIds: List, + enable: Boolean, + ) { val transformedSourceIds = sourceIds.map { it.toString() } preferences.disabledSources().getAndSet { disabled -> if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds) } } - private fun isEnabled(sourceId: Long): Boolean { - return sourceId.toString() in preferences.disabledSources().get() - } + private fun isEnabled(sourceId: Long): Boolean = sourceId.toString() in preferences.disabledSources().get() } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSourcePin.kt b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSourcePin.kt index 0a49a53ba6..bb21d93854 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSourcePin.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSourcePin.kt @@ -7,7 +7,6 @@ import tachiyomi.domain.source.model.Source class ToggleSourcePin( private val preferences: SourcePreferences, ) { - fun await(source: Source) { val isPinned = source.id.toString() in preferences.pinnedSources().get() preferences.pinnedSources().getAndSet { pinned -> diff --git a/app/src/main/java/eu/kanade/domain/source/model/Source.kt b/app/src/main/java/eu/kanade/domain/source/model/Source.kt index 0e3919718b..bfadb74048 100644 --- a/app/src/main/java/eu/kanade/domain/source/model/Source.kt +++ b/app/src/main/java/eu/kanade/domain/source/model/Source.kt @@ -10,7 +10,9 @@ import uy.kohesive.injekt.api.get val Source.icon: ImageBitmap? get() { - return Injekt.get().getAppIconForSource(id) + return Injekt + .get() + .getAppIconForSource(id) ?.toBitmap() ?.asImageBitmap() } diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index d4d3989c0f..0666b0e934 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -10,13 +10,13 @@ import tachiyomi.domain.library.model.LibraryDisplayMode class SourcePreferences( private val preferenceStore: PreferenceStore, ) { - - fun sourceDisplayMode() = preferenceStore.getObject( - "pref_display_mode_catalogue", - LibraryDisplayMode.default, - LibraryDisplayMode.Serializer::serialize, - LibraryDisplayMode.Serializer::deserialize, - ) + fun sourceDisplayMode() = + preferenceStore.getObject( + "pref_display_mode_catalogue", + LibraryDisplayMode.default, + LibraryDisplayMode.Serializer::serialize, + LibraryDisplayMode.Serializer::deserialize, + ) fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages()) @@ -24,19 +24,21 @@ class SourcePreferences( fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) - fun lastUsedSource() = preferenceStore.getLong( - Preference.appStateKey("last_catalogue_source"), - -1, - ) + fun lastUsedSource() = + preferenceStore.getLong( + Preference.appStateKey("last_catalogue_source"), + -1, + ) fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true) fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL) - fun migrationSortingDirection() = preferenceStore.getEnum( - "pref_migration_direction", - SetMigrateSorting.Direction.ASCENDING, - ) + fun migrationSortingDirection() = + preferenceStore.getEnum( + "pref_migration_direction", + SetMigrateSorting.Direction.ASCENDING, + ) fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) @@ -44,8 +46,9 @@ class SourcePreferences( fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) - fun trustedExtensions() = preferenceStore.getStringSet( - Preference.appStateKey("trusted_extensions"), - emptySet(), - ) + fun trustedExtensions() = + preferenceStore.getStringSet( + Preference.appStateKey("trusted_extensions"), + emptySet(), + ) } diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt index 2d31c64e7b..65d362600d 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt @@ -26,9 +26,12 @@ class AddTracks( private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, private val getChaptersByMangaId: GetChaptersByMangaId, ) { - // TODO: update all trackers based on common data - suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withNonCancellableContext { + suspend fun bind( + tracker: Tracker, + item: Track, + mangaId: Long, + ) = withNonCancellableContext { withIOContext { val allChapters = getChaptersByMangaId.await(mangaId) val hasReadChapters = allChapters.any { it.read } @@ -41,33 +44,40 @@ class AddTracks( // TODO: merge into [SyncChapterProgressWithTrack]? // Update chapter progress if newer chapters marked read locally if (hasReadChapters) { - val latestLocalReadChapterNumber = allChapters - .sortedBy { it.chapterNumber } - .takeWhile { it.read } - .lastOrNull() - ?.chapterNumber ?: -1.0 + val latestLocalReadChapterNumber = + allChapters + .sortedBy { it.chapterNumber } + .takeWhile { it.read } + .lastOrNull() + ?.chapterNumber ?: -1.0 if (latestLocalReadChapterNumber > track.lastChapterRead) { - track = track.copy( - lastChapterRead = latestLocalReadChapterNumber, - ) + track = + track.copy( + lastChapterRead = latestLocalReadChapterNumber, + ) tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt()) } if (track.startDate <= 0) { - val firstReadChapterDate = Injekt.get().await(mangaId) - .sortedBy { it.readAt } - .firstOrNull() - ?.readAt + val firstReadChapterDate = + Injekt + .get() + .await(mangaId) + .sortedBy { it.readAt } + .firstOrNull() + ?.readAt firstReadChapterDate?.let { - val startDate = firstReadChapterDate.time.convertEpochMillisZone( - ZoneOffset.systemDefault(), - ZoneOffset.UTC, - ) - track = track.copy( - startDate = startDate, - ) + val startDate = + firstReadChapterDate.time.convertEpochMillisZone( + ZoneOffset.systemDefault(), + ZoneOffset.UTC, + ) + track = + track.copy( + startDate = startDate, + ) tracker.setRemoteStartDate(track.toDbTrack(), startDate) } } @@ -77,9 +87,13 @@ class AddTracks( } } - suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext { + suspend fun bindEnhancedTrackers( + manga: Manga, + source: Source, + ) = withNonCancellableContext { withIOContext { - getTracks.await(manga.id) + getTracks + .await(manga.id) .filterIsInstance() .filter { it.accept(source) } .forEach { service -> diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt index 3d1e337ffa..1b65113276 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt @@ -16,7 +16,6 @@ class RefreshTracks( private val insertTrack: InsertTrack, private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, ) { - /** * Fetches updated tracking data from all logged in trackers. * @@ -24,7 +23,8 @@ class RefreshTracks( */ suspend fun await(mangaId: Long): List> { return supervisorScope { - return@supervisorScope getTracks.await(mangaId) + return@supervisorScope getTracks + .await(mangaId) .map { it to trackerManager.get(it.trackerId) } .filter { (_, service) -> service?.isLoggedIn == true } .map { (track, service) -> @@ -38,8 +38,7 @@ class RefreshTracks( service to e } } - } - .awaitAll() + }.awaitAll() .filterNotNull() } } diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt b/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt index 8e6df22899..40411644bf 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt @@ -16,7 +16,6 @@ class SyncChapterProgressWithTrack( private val insertTrack: InsertTrack, private val getChaptersByMangaId: GetChaptersByMangaId, ) { - suspend fun await( mangaId: Long, remoteTrack: Track, @@ -26,13 +25,16 @@ class SyncChapterProgressWithTrack( return } - val sortedChapters = getChaptersByMangaId.await(mangaId) - .sortedBy { it.chapterNumber } - .filter { it.isRecognizedNumber } + val sortedChapters = + getChaptersByMangaId + .await(mangaId) + .sortedBy { it.chapterNumber } + .filter { it.isRecognizedNumber } - val chapterUpdates = sortedChapters - .filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read } - .map { it.copy(read = true).toChapterUpdate() } + val chapterUpdates = + sortedChapters + .filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read } + .map { it.copy(read = true).toChapterUpdate() } // only take into account continuous reading val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt b/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt index fdf24ec4e9..32b02314a1 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt @@ -20,38 +20,44 @@ class TrackChapter( private val insertTrack: InsertTrack, private val delayedTrackingStore: DelayedTrackingStore, ) { - - suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) { + suspend fun await( + context: Context, + mangaId: Long, + chapterNumber: Double, + setupJobOnFailure: Boolean = true, + ) { withNonCancellableContext { val tracks = getTracks.await(mangaId) if (tracks.isEmpty()) return@withNonCancellableContext - tracks.mapNotNull { track -> - val service = trackerManager.get(track.trackerId) - if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) { - return@mapNotNull null - } + tracks + .mapNotNull { track -> + val service = trackerManager.get(track.trackerId) + if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) { + return@mapNotNull null + } - async { - runCatching { - try { - val updatedTrack = service.refresh(track.toDbTrack()) - .toDomainTrack(idRequired = true)!! - .copy(lastChapterRead = chapterNumber) - service.update(updatedTrack.toDbTrack(), true) - insertTrack.await(updatedTrack) - delayedTrackingStore.remove(track.id) - } catch (e: Exception) { - delayedTrackingStore.add(track.id, chapterNumber) - if (setupJobOnFailure) { - DelayedTrackingUpdateJob.setupTask(context) + async { + runCatching { + try { + val updatedTrack = + service + .refresh(track.toDbTrack()) + .toDomainTrack(idRequired = true)!! + .copy(lastChapterRead = chapterNumber) + service.update(updatedTrack.toDbTrack(), true) + insertTrack.await(updatedTrack) + delayedTrackingStore.remove(track.id) + } catch (e: Exception) { + delayedTrackingStore.add(track.id, chapterNumber) + if (setupJobOnFailure) { + DelayedTrackingUpdateJob.setupTask(context) + } + throw e } - throw e } } - } - } - .awaitAll() + }.awaitAll() .mapNotNull { it.exceptionOrNull() } .forEach { logcat(LogPriority.WARN, it) } } diff --git a/app/src/main/java/eu/kanade/domain/track/model/Track.kt b/app/src/main/java/eu/kanade/domain/track/model/Track.kt index e441334339..da7a0cce53 100644 --- a/app/src/main/java/eu/kanade/domain/track/model/Track.kt +++ b/app/src/main/java/eu/kanade/domain/track/model/Track.kt @@ -3,30 +3,30 @@ package eu.kanade.domain.track.model import tachiyomi.domain.track.model.Track import eu.kanade.tachiyomi.data.database.models.Track as DbTrack -fun Track.copyPersonalFrom(other: Track): Track { - return this.copy( +fun Track.copyPersonalFrom(other: Track): Track = + this.copy( lastChapterRead = other.lastChapterRead, score = other.score, status = other.status, startDate = other.startDate, finishDate = other.finishDate, ) -} -fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also { - it.id = id - it.manga_id = mangaId - it.remote_id = remoteId - it.library_id = libraryId - it.title = title - it.last_chapter_read = lastChapterRead - it.total_chapters = totalChapters - it.status = status - it.score = score - it.tracking_url = remoteUrl - it.started_reading_date = startDate - it.finished_reading_date = finishDate -} +fun Track.toDbTrack(): DbTrack = + DbTrack.create(trackerId).also { + it.id = id + it.manga_id = mangaId + it.remote_id = remoteId + it.library_id = libraryId + it.title = title + it.last_chapter_read = lastChapterRead + it.total_chapters = totalChapters + it.status = status + it.score = score + it.tracking_url = remoteUrl + it.started_reading_date = startDate + it.finished_reading_date = finishDate + } fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { val trackId = id ?: if (!idRequired) -1 else return null diff --git a/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt b/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt index 50589ae9d6..6f000c3e86 100644 --- a/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt +++ b/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt @@ -19,9 +19,10 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.TimeUnit -class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) : - CoroutineWorker(context, workerParams) { - +class DelayedTrackingUpdateJob( + private val context: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { if (runAttemptCount > 3) { return Result.failure() @@ -33,15 +34,15 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke val delayedTrackingStore = Injekt.get() withIOContext { - delayedTrackingStore.getItems() + delayedTrackingStore + .getItems() .mapNotNull { val track = getTracks.awaitOne(it.trackId) if (track == null) { delayedTrackingStore.remove(it.trackId) } track?.copy(lastChapterRead = it.lastChapterRead.toDouble()) - } - .forEach { track -> + }.forEach { track -> logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" } @@ -56,15 +57,17 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke private const val TAG = "DelayedTrackingUpdate" fun setupTask(context: Context) { - val constraints = Constraints( - requiredNetworkType = NetworkType.CONNECTED, - ) + val constraints = + Constraints( + requiredNetworkType = NetworkType.CONNECTED, + ) - val request = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) - .addTag(TAG) - .build() + val request = + OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) + .addTag(TAG) + .build() context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request) } diff --git a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt index ab000a9ea7..401107633a 100644 --- a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt @@ -8,23 +8,29 @@ import tachiyomi.core.common.preference.PreferenceStore class TrackPreferences( private val preferenceStore: PreferenceStore, ) { - - fun trackUsername(tracker: Tracker) = preferenceStore.getString( - Preference.privateKey("pref_mangasync_username_${tracker.id}"), - "", - ) - - fun trackPassword(tracker: Tracker) = preferenceStore.getString( - Preference.privateKey("pref_mangasync_password_${tracker.id}"), - "", - ) - - fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean( - Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"), - false, - ) - - fun setCredentials(tracker: Tracker, username: String, password: String) { + fun trackUsername(tracker: Tracker) = + preferenceStore.getString( + Preference.privateKey("pref_mangasync_username_${tracker.id}"), + "", + ) + + fun trackPassword(tracker: Tracker) = + preferenceStore.getString( + Preference.privateKey("pref_mangasync_password_${tracker.id}"), + "", + ) + + fun trackAuthExpired(tracker: Tracker) = + preferenceStore.getBoolean( + Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"), + false, + ) + + fun setCredentials( + tracker: Tracker, + username: String, + password: String, + ) { trackUsername(tracker).set(username) trackPassword(tracker).set(password) trackAuthExpired(tracker).set(false) diff --git a/app/src/main/java/eu/kanade/domain/track/store/DelayedTrackingStore.kt b/app/src/main/java/eu/kanade/domain/track/store/DelayedTrackingStore.kt index 10779a2198..7b45bdb8aa 100644 --- a/app/src/main/java/eu/kanade/domain/track/store/DelayedTrackingStore.kt +++ b/app/src/main/java/eu/kanade/domain/track/store/DelayedTrackingStore.kt @@ -5,14 +5,18 @@ import androidx.core.content.edit import logcat.LogPriority import tachiyomi.core.common.util.system.logcat -class DelayedTrackingStore(context: Context) { - +class DelayedTrackingStore( + context: Context, +) { /** * Preference file where queued tracking updates are stored. */ private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE) - fun add(trackId: Long, lastChapterRead: Double) { + fun add( + trackId: Long, + lastChapterRead: Double, + ) { val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f) if (lastChapterRead > previousLastChapterRead) { logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" } @@ -28,14 +32,13 @@ class DelayedTrackingStore(context: Context) { } } - fun getItems(): List { - return preferences.all.mapNotNull { + fun getItems(): List = + preferences.all.mapNotNull { DelayedTrackingItem( trackId = it.key.toLong(), lastChapterRead = it.value.toString().toFloat(), ) } - } data class DelayedTrackingItem( val trackId: Long, diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt index 2efefb5174..19dead595c 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -14,13 +14,17 @@ import java.util.Locale class UiPreferences( private val preferenceStore: PreferenceStore, ) { - fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM) - fun appTheme() = preferenceStore.getEnum( - "pref_app_theme", - if (DeviceUtil.isDynamicColorAvailable) { AppTheme.MONET } else { AppTheme.DEFAULT }, - ) + fun appTheme() = + preferenceStore.getEnum( + "pref_app_theme", + if (DeviceUtil.isDynamicColorAvailable) { + AppTheme.MONET + } else { + AppTheme.DEFAULT + }, + ) fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false) @@ -31,9 +35,10 @@ class UiPreferences( fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) companion object { - fun dateFormat(format: String): DateTimeFormatter = when (format) { - "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - else -> DateTimeFormatter.ofPattern(format, Locale.getDefault()) - } + fun dateFormat(format: String): DateTimeFormatter = + when (format) { + "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) + else -> DateTimeFormatter.ofPattern(format, Locale.getDefault()) + } } } diff --git a/app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt b/app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt index 7c8a2416d4..15f51260cf 100644 --- a/app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt +++ b/app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt @@ -5,7 +5,9 @@ import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.isPreviewBuildType import tachiyomi.i18n.MR -enum class AppTheme(val titleRes: StringResource?) { +enum class AppTheme( + val titleRes: StringResource?, +) { DEFAULT(MR.strings.label_default), MONET(MR.strings.theme_monet), GREEN_APPLE(MR.strings.theme_greenapple), diff --git a/app/src/main/java/eu/kanade/domain/ui/model/TabletUiMode.kt b/app/src/main/java/eu/kanade/domain/ui/model/TabletUiMode.kt index cf1e957b09..fa9cc8d827 100644 --- a/app/src/main/java/eu/kanade/domain/ui/model/TabletUiMode.kt +++ b/app/src/main/java/eu/kanade/domain/ui/model/TabletUiMode.kt @@ -3,7 +3,9 @@ package eu.kanade.domain.ui.model import dev.icerock.moko.resources.StringResource import tachiyomi.i18n.MR -enum class TabletUiMode(val titleRes: StringResource) { +enum class TabletUiMode( + val titleRes: StringResource, +) { AUTOMATIC(MR.strings.automatic_background), ALWAYS(MR.strings.lock_always), LANDSCAPE(MR.strings.landscape), diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt index 6a428506f1..226b0cc800 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt @@ -52,8 +52,9 @@ fun BrowseSourceContent( ) { val context = LocalContext.current - val errorState = mangaList.loadState.refresh.takeIf { it is LoadState.Error } - ?: mangaList.loadState.append.takeIf { it is LoadState.Error } + val errorState = + mangaList.loadState.refresh.takeIf { it is LoadState.Error } + ?: mangaList.loadState.append.takeIf { it is LoadState.Error } val getErrorMessage: (LoadState.Error) -> String = { state -> with(context) { state.error.formattedMessage } @@ -61,11 +62,12 @@ fun BrowseSourceContent( LaunchedEffect(errorState) { if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) { - val result = snackbarHostState.showSnackbar( - message = getErrorMessage(errorState), - actionLabel = context.stringResource(MR.strings.action_retry), - duration = SnackbarDuration.Indefinite, - ) + val result = + snackbarHostState.showSnackbar( + message = getErrorMessage(errorState), + actionLabel = context.stringResource(MR.strings.action_retry), + duration = SnackbarDuration.Indefinite, + ) when (result) { SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss() SnackbarResult.ActionPerformed -> mangaList.retry() @@ -77,33 +79,34 @@ fun BrowseSourceContent( EmptyScreen( modifier = Modifier.padding(contentPadding), message = getErrorMessage(errorState), - actions = if (source is LocalSource) { - persistentListOf( - EmptyScreenAction( - stringRes = MR.strings.local_source_help_guide, - icon = Icons.AutoMirrored.Outlined.HelpOutline, - onClick = onLocalSourceHelpClick, - ), - ) - } else { - persistentListOf( - EmptyScreenAction( - stringRes = MR.strings.action_retry, - icon = Icons.Outlined.Refresh, - onClick = mangaList::refresh, - ), - EmptyScreenAction( - stringRes = MR.strings.action_open_in_web_view, - icon = Icons.Outlined.Public, - onClick = onWebViewClick, - ), - EmptyScreenAction( - stringRes = MR.strings.label_help, - icon = Icons.AutoMirrored.Outlined.HelpOutline, - onClick = onHelpClick, - ), - ) - }, + actions = + if (source is LocalSource) { + persistentListOf( + EmptyScreenAction( + stringRes = MR.strings.local_source_help_guide, + icon = Icons.AutoMirrored.Outlined.HelpOutline, + onClick = onLocalSourceHelpClick, + ), + ) + } else { + persistentListOf( + EmptyScreenAction( + stringRes = MR.strings.action_retry, + icon = Icons.Outlined.Refresh, + onClick = mangaList::refresh, + ), + EmptyScreenAction( + stringRes = MR.strings.action_open_in_web_view, + icon = Icons.Outlined.Public, + onClick = onWebViewClick, + ), + EmptyScreenAction( + stringRes = MR.strings.label_help, + icon = Icons.AutoMirrored.Outlined.HelpOutline, + onClick = onHelpClick, + ), + ) + }, ) return 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 277dc5e65e..775ba7f011 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -74,15 +74,17 @@ fun ExtensionDetailsScreen( onClickSource: (sourceId: Long) -> Unit, ) { val uriHandler = LocalUriHandler.current - val url = remember(state.extension) { - val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex() - regex.find(state.extension?.repoUrl.orEmpty()) - ?.let { - val (user, repo) = it.destructured - "https://github.com/$user/$repo" - } - ?: state.extension?.repoUrl - } + val url = + remember(state.extension) { + val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex() + regex + .find(state.extension?.repoUrl.orEmpty()) + ?.let { + val (user, repo) = it.destructured + "https://github.com/$user/$repo" + } + ?: state.extension?.repoUrl + } Scaffold( topBar = { scrollBehavior -> @@ -91,37 +93,38 @@ fun ExtensionDetailsScreen( navigateUp = navigateUp, actions = { AppBarActions( - actions = persistentListOf().builder() - .apply { - if (url != null) { - add( - AppBar.Action( - title = stringResource(MR.strings.action_open_repo), - icon = Icons.AutoMirrored.Outlined.Launch, - onClick = { - uriHandler.openUri(url) - }, + actions = + persistentListOf() + .builder() + .apply { + if (url != null) { + add( + AppBar.Action( + title = stringResource(MR.strings.action_open_repo), + icon = Icons.AutoMirrored.Outlined.Launch, + onClick = { + uriHandler.openUri(url) + }, + ), + ) + } + addAll( + listOf( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_enable_all), + onClick = onClickEnableAll, + ), + AppBar.OverflowAction( + title = stringResource(MR.strings.action_disable_all), + onClick = onClickDisableAll, + ), + AppBar.OverflowAction( + title = stringResource(MR.strings.pref_clear_cookies), + onClick = onClickClearCookies, + ), ), ) - } - addAll( - listOf( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_enable_all), - onClick = onClickEnableAll, - ), - AppBar.OverflowAction( - title = stringResource(MR.strings.action_disable_all), - onClick = onClickDisableAll, - ), - AppBar.OverflowAction( - title = stringResource(MR.strings.pref_clear_cookies), - onClick = onClickClearCookies, - ), - ), - ) - } - .build(), + }.build(), ) }, scrollBehavior = scrollBehavior, @@ -172,13 +175,14 @@ private fun ExtensionDetails( DetailsHeader( extension = extension, onClickUninstall = onClickUninstall, - onClickAppInfo = { - Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", extension.pkgName, null) - context.startActivity(this) - } - Unit - }.takeIf { extension.isShared }, + onClickAppInfo = + { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", extension.pkgName, null) + context.startActivity(this) + } + Unit + }.takeIf { extension.isShared }, onClickAgeRating = { showNsfwWarning = true }, @@ -217,43 +221,45 @@ private fun DetailsHeader( Column { Column( - modifier = Modifier - .fillMaxWidth() - .padding( - start = MaterialTheme.padding.medium, - end = MaterialTheme.padding.medium, - top = MaterialTheme.padding.medium, - bottom = MaterialTheme.padding.small, - ) - .clickable { - val extDebugInfo = buildString { - append( - """ - Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName}) - Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode}) - NSFW: ${extension.isNsfw} - """.trimIndent() - ) + modifier = + Modifier + .fillMaxWidth() + .padding( + start = MaterialTheme.padding.medium, + end = MaterialTheme.padding.medium, + top = MaterialTheme.padding.medium, + bottom = MaterialTheme.padding.small, + ).clickable { + val extDebugInfo = + buildString { + append( + """ + Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName}) + Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode}) + NSFW: ${extension.isNsfw} + """.trimIndent(), + ) - if (extension is Extension.Installed) { - append("\n\n") - append( - """ - Update available: ${extension.hasUpdate} - Obsolete: ${extension.isObsolete} - Shared: ${extension.isShared} - Repository: ${extension.repoUrl} - """.trimIndent() - ) - } - } - context.copyToClipboard("Extension Debug information", extDebugInfo) - }, + if (extension is Extension.Installed) { + append("\n\n") + append( + """ + Update available: ${extension.hasUpdate} + Obsolete: ${extension.isObsolete} + Shared: ${extension.isShared} + Repository: ${extension.repoUrl} + """.trimIndent(), + ) + } + } + context.copyToClipboard("Extension Debug information", extDebugInfo) + }, horizontalAlignment = Alignment.CenterHorizontally, ) { ExtensionIcon( - modifier = Modifier - .size(112.dp), + modifier = + Modifier + .size(112.dp), extension = extension, density = DisplayMetrics.DENSITY_XXXHIGH, ) @@ -273,12 +279,13 @@ private fun DetailsHeader( } Row( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = MaterialTheme.padding.extraLarge, - vertical = MaterialTheme.padding.small, - ), + modifier = + Modifier + .fillMaxWidth() + .padding( + horizontal = MaterialTheme.padding.extraLarge, + vertical = MaterialTheme.padding.small, + ), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, ) { @@ -302,10 +309,11 @@ private fun DetailsHeader( InfoText( modifier = Modifier.weight(1f), primaryText = stringResource(MR.strings.ext_nsfw_short), - primaryTextStyle = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.error, - fontWeight = FontWeight.Medium, - ), + primaryTextStyle = + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Medium, + ), secondaryText = stringResource(MR.strings.ext_info_age_rating), onClick = onClickAgeRating, ) @@ -313,12 +321,13 @@ private fun DetailsHeader( } Row( - modifier = Modifier.padding( - start = MaterialTheme.padding.medium, - end = MaterialTheme.padding.medium, - top = MaterialTheme.padding.small, - bottom = MaterialTheme.padding.medium, - ), + modifier = + Modifier.padding( + start = MaterialTheme.padding.medium, + end = MaterialTheme.padding.medium, + top = MaterialTheme.padding.small, + bottom = MaterialTheme.padding.medium, + ), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), ) { OutlinedButton( @@ -353,11 +362,12 @@ private fun InfoText( primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge, onClick: (() -> Unit)? = null, ) { - val clickableModifier = if (onClick != null) { - Modifier.clickable(interactionSource = null, indication = null, onClick = onClick) - } else { - Modifier - } + val clickableModifier = + if (onClick != null) { + Modifier.clickable(interactionSource = null, indication = null, onClick = onClick) + } else { + Modifier + } Column( modifier = modifier.then(clickableModifier), @@ -397,11 +407,12 @@ private fun SourceSwitchPreference( TextPreferenceWidget( modifier = modifier, - title = if (source.labelAsName) { - source.source.toString() - } else { - LocaleHelper.getSourceDisplayName(source.source.lang, context) - }, + title = + if (source.labelAsName) { + source.source.toString() + } else { + LocaleHelper.getSourceDisplayName(source.source.lang, context) + }, widget = { Row( verticalAlignment = Alignment.CenterVertically, @@ -428,9 +439,7 @@ private fun SourceSwitchPreference( } @Composable -private fun NsfwWarningDialog( - onClickConfirm: () -> Unit, -) { +private fun NsfwWarningDialog(onClickConfirm: () -> Unit) { AlertDialog( text = { Text(text = stringResource(MR.strings.ext_nsfw_warning)) 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 5849e8c9e9..0f75ded7f4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -95,21 +95,23 @@ fun ExtensionScreen( when { state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.isEmpty -> { - val msg = if (!searchQuery.isNullOrEmpty()) { - MR.strings.no_results_found - } else { - MR.strings.empty_screen - } + val msg = + if (!searchQuery.isNullOrEmpty()) { + MR.strings.no_results_found + } else { + MR.strings.empty_screen + } EmptyScreen( stringRes = msg, modifier = Modifier.padding(contentPadding), - actions = persistentListOf( - EmptyScreenAction( - stringRes = MR.strings.label_extension_repos, - icon = Icons.Outlined.Settings, - onClick = { navigator.push(ExtensionReposScreen()) }, + actions = + persistentListOf( + EmptyScreenAction( + stringRes = MR.strings.label_extension_repos, + icon = Icons.Outlined.Settings, + onClick = { navigator.push(ExtensionReposScreen()) }, + ), ), - ), ) } else -> { @@ -156,9 +158,10 @@ private fun ExtensionContent( item(key = "extension-permissions-warning") { WarningBanner( textRes = MR.strings.ext_permission_install_apps_warning, - modifier = Modifier.clickable { - context.launchRequestPackageInstallsPermission() - }, + modifier = + Modifier.clickable { + context.launchRequestPackageInstallsPermission() + }, ) } } @@ -176,9 +179,10 @@ private fun ExtensionContent( Button(onClick = { onClickUpdateAll() }) { Text( text = stringResource(MR.strings.ext_update_all), - style = LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.onPrimary, - ), + style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onPrimary, + ), ) } } @@ -218,7 +222,9 @@ private fun ExtensionContent( when (it) { is Extension.Available -> onInstallExtension(it) is Extension.Installed -> onOpenExtension(it) - is Extension.Untrusted -> { trustState = it } + is Extension.Untrusted -> { + trustState = it + } } }, onLongClickItem = onLongClickItem, @@ -240,7 +246,9 @@ private fun ExtensionContent( onOpenExtension(it) } } - is Extension.Untrusted -> { trustState = it } + is Extension.Untrusted -> { + trustState = it + } } }, ) @@ -276,17 +284,19 @@ private fun ExtensionItem( ) { val (extension, installStep) = item BaseBrowseItem( - modifier = modifier - .combinedClickable( - onClick = { onClickItem(extension) }, - onLongClick = { onLongClickItem(extension) }, - ), + modifier = + modifier + .combinedClickable( + onClick = { onClickItem(extension) }, + onLongClick = { onLongClickItem(extension) }, + ), onClickItem = { onClickItem(extension) }, onLongClickItem = { onLongClickItem(extension) }, icon = { Box( - modifier = Modifier - .size(40.dp), + modifier = + Modifier + .size(40.dp), contentAlignment = Alignment.Center, ) { val idle = installStep.isCompleted() @@ -303,9 +313,10 @@ private fun ExtensionItem( ) ExtensionIcon( extension = extension, - modifier = Modifier - .matchParentSize() - .padding(padding), + modifier = + Modifier + .matchParentSize() + .padding(padding), ) } }, @@ -360,12 +371,13 @@ private fun ExtensionItemContent( ) } - val warning = when { - extension is Extension.Untrusted -> MR.strings.ext_untrusted - extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete - extension.isNsfw -> MR.strings.ext_nsfw_short - else -> null - } + val warning = + when { + extension is Extension.Untrusted -> MR.strings.ext_untrusted + extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete + extension.isNsfw -> MR.strings.ext_nsfw_short + else -> null + } if (warning != null) { Text( text = stringResource(warning).uppercase(), @@ -378,12 +390,13 @@ private fun ExtensionItemContent( if (!installStep.isCompleted()) { DotSeparatorNoSpaceText() Text( - text = when (installStep) { - InstallStep.Pending -> stringResource(MR.strings.ext_pending) - InstallStep.Downloading -> stringResource(MR.strings.ext_downloading) - InstallStep.Installing -> stringResource(MR.strings.ext_installing) - else -> error("Must not show non-install process text") - }, + text = + when (installStep) { + InstallStep.Pending -> stringResource(MR.strings.ext_pending) + InstallStep.Downloading -> stringResource(MR.strings.ext_downloading) + InstallStep.Installing -> stringResource(MR.strings.ext_installing) + else -> error("Must not show non-install process text") + }, ) } } @@ -500,9 +513,10 @@ private fun ExtensionHeader( ) { Text( text = text, - modifier = Modifier - .padding(vertical = 8.dp) - .weight(1f), + modifier = + Modifier + .padding(vertical = 8.dp) + .weight(1f), style = MaterialTheme.typography.header, ) action() diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index da4db5e98d..7e4f9104a4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -75,9 +75,10 @@ internal fun GlobalSearchContent( items.forEach { (source, result) -> item(key = source.id) { GlobalSearchResultItem( - title = fromSourceId?.let { - "▶ ${source.name}".takeIf { source.id == fromSourceId } - } ?: source.name, + title = + fromSourceId?.let { + "▶ ${source.name}".takeIf { source.id == fromSourceId } + } ?: source.name, subtitle = LocaleHelper.getLocalizedDisplayName(source.lang), onClick = { onClickSource(source) }, modifier = Modifier.animateItem(), diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index 3f8a67c4cc..51b9fcd1ce 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -53,10 +53,11 @@ fun MigrateSourceScreen( val context = LocalContext.current when { state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) - state.isEmpty -> EmptyScreen( - stringRes = MR.strings.information_empty_library, - modifier = Modifier.padding(contentPadding), - ) + state.isEmpty -> + EmptyScreen( + stringRes = MR.strings.information_empty_library, + modifier = Modifier.padding(contentPadding), + ) else -> MigrateSourceList( list = state.items, @@ -90,9 +91,10 @@ private fun MigrateSourceList( ) { stickyHeader(key = STICKY_HEADER_KEY_PREFIX) { Row( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .padding(start = MaterialTheme.padding.medium), + modifier = + Modifier + .background(MaterialTheme.colorScheme.background) + .padding(start = MaterialTheme.padding.medium), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -103,26 +105,30 @@ private fun MigrateSourceList( IconButton(onClick = onToggleSortingMode) { when (sortingMode) { - SetMigrateSorting.Mode.ALPHABETICAL -> Icon( - Icons.Outlined.SortByAlpha, - contentDescription = stringResource(MR.strings.action_sort_alpha), - ) - SetMigrateSorting.Mode.TOTAL -> Icon( - Icons.Outlined.Numbers, - contentDescription = stringResource(MR.strings.action_sort_count), - ) + SetMigrateSorting.Mode.ALPHABETICAL -> + Icon( + Icons.Outlined.SortByAlpha, + contentDescription = stringResource(MR.strings.action_sort_alpha), + ) + SetMigrateSorting.Mode.TOTAL -> + Icon( + Icons.Outlined.Numbers, + contentDescription = stringResource(MR.strings.action_sort_count), + ) } } IconButton(onClick = onToggleSortingDirection) { when (sortingDirection) { - SetMigrateSorting.Direction.ASCENDING -> Icon( - Icons.Outlined.ArrowUpward, - contentDescription = stringResource(MR.strings.action_asc), - ) - SetMigrateSorting.Direction.DESCENDING -> Icon( - Icons.Outlined.ArrowDownward, - contentDescription = stringResource(MR.strings.action_desc), - ) + SetMigrateSorting.Direction.ASCENDING -> + Icon( + Icons.Outlined.ArrowUpward, + contentDescription = stringResource(MR.strings.action_asc), + ) + SetMigrateSorting.Direction.DESCENDING -> + Icon( + Icons.Outlined.ArrowDownward, + contentDescription = stringResource(MR.strings.action_desc), + ) } } } @@ -165,9 +171,10 @@ private fun MigrateSourceItem( }, content = { _, sourceLangString -> Column( - modifier = Modifier - .padding(horizontal = MaterialTheme.padding.medium) - .weight(1f), + modifier = + Modifier + .padding(horizontal = MaterialTheme.padding.medium) + .weight(1f), ) { Text( text = source.name.ifBlank { source.id.toString() }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt index 56644b3d8c..a9e80e7241 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -48,10 +48,11 @@ fun SourcesScreen( ) { when { state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) - state.isEmpty -> EmptyScreen( - stringRes = MR.strings.source_empty_screen, - modifier = Modifier.padding(contentPadding), - ) + state.isEmpty -> + EmptyScreen( + stringRes = MR.strings.source_empty_screen, + modifier = Modifier.padding(contentPadding), + ) else -> { ScrollbarLazyColumn( contentPadding = contentPadding + topSmallPaddingValues, @@ -78,13 +79,14 @@ fun SourcesScreen( language = model.language, ) } - is SourceUiModel.Item -> SourceItem( - modifier = Modifier.animateItem(), - source = model.source, - onClickItem = onClickItem, - onLongClickItem = onLongClickItem, - onClickPin = onClickPin, - ) + is SourceUiModel.Item -> + SourceItem( + modifier = Modifier.animateItem(), + source = model.source, + onClickItem = onClickItem, + onLongClickItem = onLongClickItem, + onClickPin = onClickPin, + ) } } } @@ -100,8 +102,9 @@ private fun SourceHeader( val context = LocalContext.current Text( text = LocaleHelper.getSourceDisplayName(language, context), - modifier = modifier - .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), + modifier = + modifier + .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), style = MaterialTheme.typography.header, ) } @@ -124,9 +127,10 @@ private fun SourceItem( TextButton(onClick = { onClickItem(source, Listing.Latest) }) { Text( text = stringResource(MR.strings.latest), - style = LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.primary, - ), + style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.primary, + ), ) } } @@ -144,13 +148,14 @@ private fun SourcePinButton( onClick: () -> Unit, ) { val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin - val tint = if (isPinned) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onBackground.copy( - alpha = SecondaryItemAlpha, - ) - } + val tint = + if (isPinned) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onBackground.copy( + alpha = SecondaryItemAlpha, + ) + } val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin IconButton(onClick = onClick) { Icon( @@ -177,18 +182,20 @@ fun SourceOptionsDialog( val textId = if (Pin.Pinned in source.pin) MR.strings.action_unpin else MR.strings.action_pin Text( text = stringResource(textId), - modifier = Modifier - .clickable(onClick = onClickPin) - .fillMaxWidth() - .padding(vertical = 16.dp), + modifier = + Modifier + .clickable(onClick = onClickPin) + .fillMaxWidth() + .padding(vertical = 16.dp), ) if (!source.isLocal()) { Text( text = stringResource(MR.strings.action_disable), - modifier = Modifier - .clickable(onClick = onClickDisable) - .fillMaxWidth() - .padding(vertical = 16.dp), + modifier = + Modifier + .clickable(onClick = onClickDisable) + .fillMaxWidth() + .padding(vertical = 16.dp), ) } } @@ -199,6 +206,11 @@ fun SourceOptionsDialog( } sealed interface SourceUiModel { - data class Item(val source: Source) : SourceUiModel - data class Header(val language: String) : SourceUiModel + data class Item( + val source: Source, + ) : SourceUiModel + + data class Header( + val language: String, + ) : SourceUiModel } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BaseBrowseItem.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BaseBrowseItem.kt index 5748bab398..7a0964497b 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BaseBrowseItem.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BaseBrowseItem.kt @@ -20,12 +20,12 @@ fun BaseBrowseItem( content: @Composable RowScope.() -> Unit = {}, ) { Row( - modifier = modifier - .combinedClickable( - onClick = onClickItem, - onLongClick = onLongClickItem, - ) - .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), + modifier = + modifier + .combinedClickable( + onClick = onClickItem, + onLongClick = onLongClickItem, + ).padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), verticalAlignment = Alignment.CenterVertically, ) { icon() diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt index eebb35253d..0ee69c3973 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt @@ -25,9 +25,10 @@ fun BaseSourceItem( action: @Composable RowScope.(Source) -> Unit = {}, content: @Composable RowScope.(Source, String?) -> Unit = defaultContent, ) { - val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { - showLanguageInContent - } + val sourceLangString = + LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { + showLanguageInContent + } BaseBrowseItem( modifier = modifier, onClickItem = onClickItem, @@ -44,9 +45,10 @@ private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source -> private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString -> Column( - modifier = Modifier - .padding(horizontal = MaterialTheme.padding.medium) - .weight(1f), + modifier = + Modifier + .padding(horizontal = MaterialTheme.padding.medium) + .weight(1f), ) { Text( text = source.name, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt index 950b55192b..64caf9db4a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt @@ -35,9 +35,10 @@ import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.source.model.Source import tachiyomi.source.local.isLocal -private val defaultModifier = Modifier - .height(40.dp) - .aspectRatio(1f) +private val defaultModifier = + Modifier + .height(40.dp) + .aspectRatio(1f) @Composable fun SourceIcon( @@ -92,32 +93,36 @@ fun ExtensionIcon( contentDescription = null, placeholder = ColorPainter(Color(0x1F888888)), error = rememberResourceBitmapPainter(id = R.drawable.cover_error), - modifier = modifier - .clip(MaterialTheme.shapes.extraSmall), + modifier = + modifier + .clip(MaterialTheme.shapes.extraSmall), ) } is Extension.Installed -> { val icon by extension.getIcon(density) when (icon) { Result.Loading -> Box(modifier = modifier) - is Result.Success -> Image( - bitmap = (icon as Result.Success).value, - contentDescription = null, - modifier = modifier, - ) - Result.Error -> Image( - bitmap = ImageBitmap.imageResource(id = R.mipmap.ic_default_source), - contentDescription = null, - modifier = modifier, - ) + is Result.Success -> + Image( + bitmap = (icon as Result.Success).value, + contentDescription = null, + modifier = modifier, + ) + Result.Error -> + Image( + bitmap = ImageBitmap.imageResource(id = R.mipmap.ic_default_source), + contentDescription = null, + modifier = modifier, + ) } } - is Extension.Untrusted -> Image( - imageVector = Icons.Filled.Dangerous, - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error), - modifier = modifier.then(defaultModifier), - ) + is Extension.Untrusted -> + Image( + imageVector = Icons.Filled.Dangerous, + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error), + modifier = modifier.then(defaultModifier), + ) } } @@ -126,23 +131,29 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St val context = LocalContext.current return produceState>(initialValue = Result.Loading, this) { withIOContext { - value = try { - val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo - val appResources = context.packageManager.getResourcesForApplication(appInfo) - Result.Success( - appResources.getDrawableForDensity(appInfo.icon, density, null)!! - .toBitmap() - .asImageBitmap(), - ) - } catch (e: Exception) { - Result.Error - } + value = + try { + val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo + val appResources = context.packageManager.getResourcesForApplication(appInfo) + Result.Success( + appResources + .getDrawableForDensity(appInfo.icon, density, null)!! + .toBitmap() + .asImageBitmap(), + ) + } catch (e: Exception) { + Result.Error + } } } } sealed class Result { data object Loading : Result() + data object Error : Result() - data class Success(val value: T) : Result() + + data class Success( + val value: T, + ) : Result() } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt index 825c1c783a..b7ba5da659 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt @@ -63,13 +63,14 @@ private fun BrowseSourceComfortableGridItem( ) { MangaComfortableGridItem( title = manga.title, - coverData = MangaCover( - mangaId = manga.id, - sourceId = manga.source, - isMangaFavorite = manga.favorite, - url = manga.thumbnailUrl, - lastModified = manga.coverLastModified, - ), + coverData = + MangaCover( + mangaId = manga.id, + sourceId = manga.source, + isMangaFavorite = manga.favorite, + url = manga.thumbnailUrl, + lastModified = manga.coverLastModified, + ), coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, coverBadgeStart = { InLibraryBadge(enabled = manga.favorite) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt index cc2da0b377..d696ec69aa 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt @@ -63,13 +63,14 @@ private fun BrowseSourceCompactGridItem( ) { MangaCompactGridItem( title = manga.title, - coverData = MangaCover( - mangaId = manga.id, - sourceId = manga.source, - isMangaFavorite = manga.favorite, - url = manga.thumbnailUrl, - lastModified = manga.coverLastModified, - ), + coverData = + MangaCover( + mangaId = manga.id, + sourceId = manga.source, + isMangaFavorite = manga.favorite, + url = manga.thumbnailUrl, + lastModified = manga.coverLastModified, + ), coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, coverBadgeStart = { InLibraryBadge(enabled = manga.favorite) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt index 63be2a55d5..7961f715bf 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt @@ -56,13 +56,14 @@ private fun BrowseSourceListItem( ) { MangaListItem( title = manga.title, - coverData = MangaCover( - mangaId = manga.id, - sourceId = manga.source, - isMangaFavorite = manga.favorite, - url = manga.thumbnailUrl, - lastModified = manga.coverLastModified, - ), + coverData = + MangaCover( + mangaId = manga.id, + sourceId = manga.source, + isMangaFavorite = manga.favorite, + url = manga.thumbnailUrl, + lastModified = manga.coverLastModified, + ), coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, badge = { InLibraryBadge(enabled = manga.favorite) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt index fc120ac769..8d96b79d35 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt @@ -12,9 +12,10 @@ import androidx.compose.ui.unit.dp @Composable internal fun BrowseSourceLoadingItem() { Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), horizontalArrangement = Arrangement.Center, ) { CircularProgressIndicator() diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index e1c3618eea..35685787f4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -54,44 +54,46 @@ fun BrowseSourceToolbar( onClickCloseSearch = navigateUp, actions = { AppBarActions( - actions = persistentListOf().builder() - .apply { - 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 }, - ), - ) - if (isLocalSource) { + actions = + persistentListOf() + .builder() + .apply { add( - AppBar.OverflowAction( - title = stringResource(MR.strings.label_help), - onClick = onHelpClick, + 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 }, ), ) - } else { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_open_in_web_view), - onClick = onWebViewClick, - ), - ) - } - if (isConfigurableSource) { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_settings), - onClick = onSettingsClick, - ), - ) - } - } - .build(), + if (isLocalSource) { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.label_help), + onClick = onHelpClick, + ), + ) + } else { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_open_in_web_view), + onClick = onWebViewClick, + ), + ) + } + if (isConfigurableSource) { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_settings), + onClick = onSettingsClick, + ), + ) + } + }.build(), ) DropdownMenu( diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt index 456269b7c6..e6c4924d9d 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt @@ -79,10 +79,11 @@ private fun MangaItem( private fun EmptyResultItem() { Text( text = stringResource(MR.strings.no_results_found), - modifier = Modifier - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ), + modifier = + Modifier + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt index 5de9a2142c..5fba7ab673 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt @@ -37,13 +37,13 @@ fun GlobalSearchResultItem( ) { Column(modifier = modifier) { Row( - modifier = Modifier - .padding( - start = MaterialTheme.padding.medium, - end = MaterialTheme.padding.extraSmall, - ) - .fillMaxWidth() - .clickable(onClick = onClick), + modifier = + Modifier + .padding( + start = MaterialTheme.padding.medium, + end = MaterialTheme.padding.extraSmall, + ).fillMaxWidth() + .clickable(onClick = onClick), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -65,14 +65,16 @@ fun GlobalSearchResultItem( @Composable fun GlobalSearchLoadingResultItem() { Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = MaterialTheme.padding.medium), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = MaterialTheme.padding.medium), ) { CircularProgressIndicator( - modifier = Modifier - .size(16.dp) - .align(Alignment.Center), + modifier = + Modifier + .size(16.dp) + .align(Alignment.Center), strokeWidth = 2.dp, ) } @@ -81,12 +83,12 @@ fun GlobalSearchLoadingResultItem() { @Composable fun GlobalSearchErrorResultItem(message: String?) { Column( - modifier = Modifier - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ) - .fillMaxWidth(), + modifier = + Modifier + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index fa61696ae7..a51ca25c90 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -59,17 +59,19 @@ fun GlobalSearchToolbar( if (progress in 1.. stringResource(MR.strings.label_default) - else -> name - } + get() = + when { + isSystemCategory -> stringResource(MR.strings.label_default) + else -> name + } fun Category.visualName(context: Context): String = when { diff --git a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt index df553a203e..706ac6dca5 100644 --- a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt @@ -76,9 +76,10 @@ fun CategoryScreen( CategoryContent( categories = state.categories, lazyListState = lazyListState, - paddingValues = paddingValues + - topSmallPaddingValues + - PaddingValues(horizontal = MaterialTheme.padding.medium), + paddingValues = + paddingValues + + topSmallPaddingValues + + PaddingValues(horizontal = MaterialTheme.padding.medium), onClickRename = onClickRename, onClickDelete = onClickDelete, onMoveUp = onClickMoveUp, diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt index 7709d960fc..c60602eac8 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt @@ -71,19 +71,21 @@ fun CategoryCreateDialog( }, text = { OutlinedTextField( - modifier = Modifier - .focusRequester(focusRequester), + modifier = + Modifier + .focusRequester(focusRequester), value = name, onValueChange = { name = it }, label = { Text(text = stringResource(MR.strings.name)) }, supportingText = { - val msgRes = if (name.isNotEmpty() && nameAlreadyExists) { - MR.strings.error_category_exists - } else { - MR.strings.information_required_plain - } + val msgRes = + if (name.isNotEmpty() && nameAlreadyExists) { + MR.strings.error_category_exists + } else { + MR.strings.information_required_plain + } Text(text = stringResource(msgRes)) }, isError = name.isNotEmpty() && nameAlreadyExists, @@ -143,11 +145,12 @@ fun CategoryRenameDialog( }, label = { Text(text = stringResource(MR.strings.name)) }, supportingText = { - val msgRes = if (valueHasChanged && nameAlreadyExists) { - MR.strings.error_category_exists - } else { - MR.strings.information_required_plain - } + val msgRes = + if (valueHasChanged && nameAlreadyExists) { + MR.strings.error_category_exists + } else { + MR.strings.information_required_plain + } Text(text = stringResource(msgRes)) }, isError = valueHasChanged && nameAlreadyExists, @@ -300,9 +303,10 @@ fun ChangeCategoryDialog( } } Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onChange(checkbox) }, + modifier = + Modifier + .fillMaxWidth() + .clickable { onChange(checkbox) }, verticalAlignment = Alignment.CenterVertically, ) { when (checkbox) { diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt index 5c387e542c..0b3dd8fae7 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt @@ -39,21 +39,23 @@ fun CategoryListItem( modifier = modifier, ) { Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onRename() } - .padding( - start = MaterialTheme.padding.medium, - top = MaterialTheme.padding.medium, - end = MaterialTheme.padding.medium, - ), + modifier = + Modifier + .fillMaxWidth() + .clickable { onRename() } + .padding( + start = MaterialTheme.padding.medium, + top = MaterialTheme.padding.medium, + end = MaterialTheme.padding.medium, + ), verticalAlignment = Alignment.CenterVertically, ) { Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null) Text( text = category.name, - modifier = Modifier - .padding(start = MaterialTheme.padding.medium), + modifier = + Modifier + .padding(start = MaterialTheme.padding.medium), ) } Row { diff --git a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt index 15d05a6ec6..b53524faa1 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt @@ -89,7 +89,8 @@ fun AdaptiveSheet( } } -private val dialogProperties = DialogProperties( - usePlatformDefaultWidth = false, - decorFitsSystemWindows = true, -) +private val dialogProperties = + DialogProperties( + usePlatformDefaultWidth = false, + decorFitsSystemWindows = true, + ) diff --git a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt index 2315e00505..ea5523cae0 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt @@ -62,7 +62,6 @@ const val SEARCH_DEBOUNCE_MILLIS = 250L @Composable fun AppBar( title: String?, - modifier: Modifier = Modifier, backgroundColor: Color? = null, // Text @@ -76,7 +75,6 @@ fun AppBar( actionModeCounter: Int = 0, onCancelActionMode: () -> Unit = {}, actionModeActions: @Composable RowScope.() -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null, ) { val isActionMode by remember(actionModeCounter) { @@ -112,7 +110,6 @@ fun AppBar( fun AppBar( // Title titleContent: @Composable () -> Unit, - modifier: Modifier = Modifier, backgroundColor: Color? = null, // Up button @@ -123,7 +120,6 @@ fun AppBar( // Action mode isActionMode: Boolean = false, onCancelActionMode: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null, ) { Column( @@ -148,11 +144,13 @@ fun AppBar( }, title = titleContent, actions = actions, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation( - elevation = if (isActionMode) 3.dp else 0.dp, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = + backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation( + elevation = if (isActionMode) 3.dp else 0.dp, + ), ), - ), scrollBehavior = scrollBehavior, ) } @@ -178,18 +176,17 @@ fun AppBarTitle( style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.basicMarquee( - repeatDelayMillis = 2_000, - ), + modifier = + Modifier.basicMarquee( + repeatDelayMillis = 2_000, + ), ) } } } @Composable -fun AppBarActions( - actions: ImmutableList, -) { +fun AppBarActions(actions: ImmutableList) { var showMenu by remember { mutableStateOf(false) } actions.filterIsInstance().map { @@ -294,17 +291,19 @@ fun SearchToolbar( BasicTextField( value = searchQuery, onValueChange = onChangeSearchQuery, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .runOnEnterKeyPressed(action = searchAndClearFocus) - .showSoftKeyboard(remember { searchQuery.isEmpty() }) - .clearFocusOnSoftKeyboardHide(), - textStyle = MaterialTheme.typography.titleMedium.copy( - color = MaterialTheme.colorScheme.onBackground, - fontWeight = FontWeight.Normal, - fontSize = 18.sp, - ), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + .runOnEnterKeyPressed(action = searchAndClearFocus) + .showSoftKeyboard(remember { searchQuery.isEmpty() }) + .clearFocusOnSoftKeyboardHide(), + textStyle = + MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + ), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardActions = KeyboardActions(onSearch = { searchAndClearFocus() }), singleLine = true, @@ -325,10 +324,11 @@ fun SearchToolbar( text = (placeholderText ?: stringResource(MR.strings.action_search_hint)), maxLines = 1, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleMedium.copy( - fontSize = 18.sp, - fontWeight = FontWeight.Normal, - ), + style = + MaterialTheme.typography.titleMedium.copy( + fontSize = 18.sp, + fontWeight = FontWeight.Normal, + ), ) }, container = {}, @@ -399,8 +399,9 @@ fun UpIcon( modifier: Modifier = Modifier, navigationIcon: ImageVector? = null, ) { - val icon = navigationIcon - ?: Icons.AutoMirrored.Outlined.ArrowBack + val icon = + navigationIcon + ?: Icons.AutoMirrored.Outlined.ArrowBack Icon( imageVector = icon, contentDescription = stringResource(MR.strings.action_bar_up_description), diff --git a/app/src/main/java/eu/kanade/presentation/components/Banners.kt b/app/src/main/java/eu/kanade/presentation/components/Banners.kt index 7e702a8109..e6f92bda43 100644 --- a/app/src/main/java/eu/kanade/presentation/components/Banners.kt +++ b/app/src/main/java/eu/kanade/presentation/components/Banners.kt @@ -48,10 +48,11 @@ fun WarningBanner( ) { Text( text = stringResource(textRes), - modifier = modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.error) - .padding(16.dp), + modifier = + modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.error) + .padding(16.dp), color = MaterialTheme.colorScheme.onError, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, @@ -69,45 +70,48 @@ fun AppStateBanners( val mainInsets = WindowInsets.statusBars val mainInsetsTop = mainInsets.getTop(density) SubcomposeLayout(modifier = modifier) { constraints -> - val indexingPlaceable = subcompose(0) { - AnimatedVisibility( - visible = indexing, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - IndexingDownloadBanner( - modifier = Modifier.windowInsetsPadding(mainInsets), - ) - } - }.fastMap { it.measure(constraints) } + val indexingPlaceable = + subcompose(0) { + AnimatedVisibility( + visible = indexing, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + IndexingDownloadBanner( + modifier = Modifier.windowInsetsPadding(mainInsets), + ) + } + }.fastMap { it.measure(constraints) } val indexingHeight = indexingPlaceable.fastMaxBy { it.height }?.height ?: 0 - val downloadedOnlyPlaceable = subcompose(1) { - AnimatedVisibility( - visible = downloadedOnlyMode, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - val top = (mainInsetsTop - indexingHeight).coerceAtLeast(0) - DownloadedOnlyModeBanner( - modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)), - ) - } - }.fastMap { it.measure(constraints) } + val downloadedOnlyPlaceable = + subcompose(1) { + AnimatedVisibility( + visible = downloadedOnlyMode, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + val top = (mainInsetsTop - indexingHeight).coerceAtLeast(0) + DownloadedOnlyModeBanner( + modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)), + ) + } + }.fastMap { it.measure(constraints) } val downloadedOnlyHeight = downloadedOnlyPlaceable.fastMaxBy { it.height }?.height ?: 0 - val incognitoPlaceable = subcompose(2) { - AnimatedVisibility( - visible = incognitoMode, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - val top = (mainInsetsTop - indexingHeight - downloadedOnlyHeight).coerceAtLeast(0) - IncognitoModeBanner( - modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)), - ) - } - }.fastMap { it.measure(constraints) } + val incognitoPlaceable = + subcompose(2) { + AnimatedVisibility( + visible = incognitoMode, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + val top = (mainInsetsTop - indexingHeight - downloadedOnlyHeight).coerceAtLeast(0) + IncognitoModeBanner( + modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)), + ) + } + }.fastMap { it.measure(constraints) } val incognitoHeight = incognitoPlaceable.fastMaxBy { it.height }?.height ?: 0 layout(constraints.maxWidth, indexingHeight + downloadedOnlyHeight + incognitoHeight) { @@ -128,11 +132,12 @@ fun AppStateBanners( private fun DownloadedOnlyModeBanner(modifier: Modifier = Modifier) { Text( text = stringResource(MR.strings.label_downloaded_only), - modifier = Modifier - .background(DownloadedOnlyBannerBackgroundColor) - .fillMaxWidth() - .padding(4.dp) - .then(modifier), + modifier = + Modifier + .background(DownloadedOnlyBannerBackgroundColor) + .fillMaxWidth() + .padding(4.dp) + .then(modifier), color = MaterialTheme.colorScheme.onTertiary, textAlign = TextAlign.Center, style = MaterialTheme.typography.labelMedium, @@ -143,11 +148,12 @@ private fun DownloadedOnlyModeBanner(modifier: Modifier = Modifier) { private fun IncognitoModeBanner(modifier: Modifier = Modifier) { Text( text = stringResource(MR.strings.pref_incognito_mode), - modifier = Modifier - .background(IncognitoModeBannerBackgroundColor) - .fillMaxWidth() - .padding(4.dp) - .then(modifier), + modifier = + Modifier + .background(IncognitoModeBannerBackgroundColor) + .fillMaxWidth() + .padding(4.dp) + .then(modifier), color = MaterialTheme.colorScheme.onPrimary, textAlign = TextAlign.Center, style = MaterialTheme.typography.labelMedium, @@ -158,11 +164,12 @@ private fun IncognitoModeBanner(modifier: Modifier = Modifier) { private fun IndexingDownloadBanner(modifier: Modifier = Modifier) { val density = LocalDensity.current Row( - modifier = Modifier - .background(color = IndexingBannerBackgroundColor) - .fillMaxWidth() - .padding(8.dp) - .then(modifier), + modifier = + Modifier + .background(color = IndexingBannerBackgroundColor) + .fillMaxWidth() + .padding(8.dp) + .then(modifier), horizontalArrangement = Arrangement.Center, ) { var textHeight by remember { mutableStateOf(0.dp) } diff --git a/app/src/main/java/eu/kanade/presentation/components/DateText.kt b/app/src/main/java/eu/kanade/presentation/components/DateText.kt index 4f2d988af7..59dba7fc8d 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DateText.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DateText.kt @@ -14,22 +14,18 @@ import java.time.LocalDate import java.time.ZoneId @Composable -fun relativeDateText( - dateEpochMillis: Long, -): String { - return relativeDateText( - localDate = LocalDate.ofInstant( - Instant.ofEpochMilli(dateEpochMillis), - ZoneId.systemDefault(), - ) - .takeIf { dateEpochMillis > 0L }, +fun relativeDateText(dateEpochMillis: Long): String = + relativeDateText( + localDate = + LocalDate + .ofInstant( + Instant.ofEpochMilli(dateEpochMillis), + ZoneId.systemDefault(), + ).takeIf { dateEpochMillis > 0L }, ) -} @Composable -fun relativeDateText( - localDate: LocalDate?, -): String { +fun relativeDateText(localDate: LocalDate?): String { val context = LocalContext.current val preferences = remember { Injekt.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 a116c3ee1c..5610cc71bd 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt @@ -17,13 +17,14 @@ fun DownloadDropdownMenu( onDownloadClicked: (DownloadAction) -> Unit, modifier: Modifier = Modifier, ) { - val options = persistentListOf( - DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1), - DownloadAction.NEXT_5_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 5, 5), - 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), - ) + val options = + persistentListOf( + DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1), + DownloadAction.NEXT_5_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 5, 5), + 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), + ) DropdownMenu( expanded = expanded, diff --git a/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt b/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt index 5355318a96..ea6cc63132 100644 --- a/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt @@ -31,18 +31,19 @@ private fun WithActionPreview() { Surface { EmptyScreen( stringRes = MR.strings.empty_screen, - actions = persistentListOf( - EmptyScreenAction( - stringRes = MR.strings.action_retry, - icon = Icons.Outlined.Refresh, - onClick = {}, + actions = + persistentListOf( + EmptyScreenAction( + stringRes = MR.strings.action_retry, + icon = Icons.Outlined.Refresh, + onClick = {}, + ), + EmptyScreenAction( + stringRes = MR.strings.getting_started_guide, + icon = Icons.AutoMirrored.Outlined.HelpOutline, + onClick = {}, + ), ), - EmptyScreenAction( - stringRes = MR.strings.getting_started_guide, - icon = Icons.AutoMirrored.Outlined.HelpOutline, - onClick = {}, - ), - ), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt index b651060f71..e9953487d2 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -79,16 +79,14 @@ fun TabbedDialog( modifier = Modifier.animateContentSize(), state = pagerState, verticalAlignment = Alignment.Top, - pageContent = { page -> content(page) } + pageContent = { page -> content(page) }, ) } } } @Composable -private fun MoreMenu( - content: @Composable ColumnScope.(() -> Unit) -> Unit, -) { +private fun MoreMenu(content: @Composable ColumnScope.(() -> Unit) -> Unit) { var expanded by remember { mutableStateOf(false) } Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) { IconButton(onClick = { expanded = true }) { diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index efb1e2e049..faa22701b2 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -63,11 +63,12 @@ fun TabbedScreen( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { contentPadding -> Column( - modifier = Modifier.padding( - top = contentPadding.calculateTopPadding(), - start = contentPadding.calculateStartPadding(LocalLayoutDirection.current), - end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), - ), + modifier = + Modifier.padding( + top = contentPadding.calculateTopPadding(), + start = contentPadding.calculateStartPadding(LocalLayoutDirection.current), + end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), + ), ) { PrimaryTabRow( selectedTabIndex = state.currentPage, diff --git a/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt b/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt index 5be7a2ca55..fceeb34503 100644 --- a/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt @@ -44,16 +44,18 @@ fun CrashScreen( onRejectClick = onRestartClick, ) { Box( - modifier = Modifier - .padding(vertical = MaterialTheme.padding.small) - .clip(MaterialTheme.shapes.small) - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceVariant), + modifier = + Modifier + .padding(vertical = MaterialTheme.padding.small) + .clip(MaterialTheme.shapes.small) + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceVariant), ) { Text( text = exception.toString(), - modifier = Modifier - .padding(all = MaterialTheme.padding.small), + modifier = + Modifier + .padding(all = MaterialTheme.padding.small), color = MaterialTheme.colorScheme.onSurfaceVariant, ) } diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index dba526b54f..080975682b 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -67,11 +67,12 @@ fun HistoryScreen( if (it == null) { LoadingScreen(Modifier.padding(contentPadding)) } else if (it.isEmpty()) { - val msg = if (!state.searchQuery.isNullOrEmpty()) { - MR.strings.no_results_found - } else { - MR.strings.information_no_recent_manga - } + val msg = + if (!state.searchQuery.isNullOrEmpty()) { + MR.strings.no_results_found + } else { + MR.strings.information_no_recent_manga + } EmptyScreen( stringRes = msg, modifier = Modifier.padding(contentPadding), @@ -133,8 +134,13 @@ private fun HistoryScreenContent( } sealed interface HistoryUiModel { - data class Header(val date: LocalDate) : HistoryUiModel - data class Item(val item: HistoryWithRelations) : HistoryUiModel + data class Header( + val date: LocalDate, + ) : HistoryUiModel + + data class Item( + val item: HistoryWithRelations, + ) : HistoryUiModel } @PreviewLightDark diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt index 4ffa15dd1d..9412853481 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt @@ -11,63 +11,71 @@ import java.util.Date import kotlin.random.Random class HistoryScreenModelStateProvider : PreviewParameterProvider { + private val multiPage = + HistoryScreenModel.State( + searchQuery = null, + list = + listOf(HistoryUiModelExamples.headerToday) + .asSequence() + .plus(HistoryUiModelExamples.items().take(3)) + .plus(HistoryUiModelExamples.header { it.minus(1, ChronoUnit.DAYS) }) + .plus(HistoryUiModelExamples.items().take(1)) + .plus(HistoryUiModelExamples.header { it.minus(2, ChronoUnit.DAYS) }) + .plus(HistoryUiModelExamples.items().take(7)) + .toList(), + dialog = null, + ) - private val multiPage = HistoryScreenModel.State( - searchQuery = null, - list = - listOf(HistoryUiModelExamples.headerToday) - .asSequence() - .plus(HistoryUiModelExamples.items().take(3)) - .plus(HistoryUiModelExamples.header { it.minus(1, ChronoUnit.DAYS) }) - .plus(HistoryUiModelExamples.items().take(1)) - .plus(HistoryUiModelExamples.header { it.minus(2, ChronoUnit.DAYS) }) - .plus(HistoryUiModelExamples.items().take(7)) - .toList(), - dialog = null, - ) - - private val shortRecent = HistoryScreenModel.State( - searchQuery = null, - list = listOf( - HistoryUiModelExamples.headerToday, - HistoryUiModelExamples.items().first(), - ), - dialog = null, - ) + private val shortRecent = + HistoryScreenModel.State( + searchQuery = null, + list = + listOf( + HistoryUiModelExamples.headerToday, + HistoryUiModelExamples.items().first(), + ), + dialog = null, + ) - private val shortFuture = HistoryScreenModel.State( - searchQuery = null, - list = listOf( - HistoryUiModelExamples.headerTomorrow, - HistoryUiModelExamples.items().first(), - ), - dialog = null, - ) + private val shortFuture = + HistoryScreenModel.State( + searchQuery = null, + list = + listOf( + HistoryUiModelExamples.headerTomorrow, + HistoryUiModelExamples.items().first(), + ), + dialog = null, + ) - private val empty = HistoryScreenModel.State( - searchQuery = null, - list = listOf(), - dialog = null, - ) + private val empty = + HistoryScreenModel.State( + searchQuery = null, + list = listOf(), + dialog = null, + ) - private val loadingWithSearchQuery = HistoryScreenModel.State( - searchQuery = "Example Search Query", - ) + private val loadingWithSearchQuery = + HistoryScreenModel.State( + searchQuery = "Example Search Query", + ) - private val loading = HistoryScreenModel.State( - searchQuery = null, - list = null, - dialog = null, - ) + private val loading = + HistoryScreenModel.State( + searchQuery = null, + list = null, + dialog = null, + ) - override val values: Sequence = sequenceOf( - multiPage, - shortRecent, - shortFuture, - empty, - loadingWithSearchQuery, - loading, - ) + override val values: Sequence = + sequenceOf( + multiPage, + shortRecent, + shortFuture, + empty, + loadingWithSearchQuery, + loading, + ) private object HistoryUiModelExamples { val headerToday = header() @@ -77,13 +85,14 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider Instant = { it }) = HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now()))) - fun items() = sequence { - var count = 1 - while (true) { - yield(randItem { it.copy(title = "Example Title $count") }) - count += 1 + fun items() = + sequence { + var count = 1 + while (true) { + yield(randItem { it.copy(title = "Example Title $count") }) + count += 1 + } } - } fun randItem(historyBuilder: (HistoryWithRelations) -> HistoryWithRelations = { it }) = HistoryUiModel.Item( @@ -96,13 +105,14 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider -1) { - stringResource( - MR.strings.recent_manga_time, - formatChapterNumber(history.chapterNumber), - readAt, - ) - } else { - readAt - }, + text = + if (history.chapterNumber > -1) { + stringResource( + MR.strings.recent_manga_time, + formatChapterNumber(history.chapterNumber), + readAt, + ) + } else { + readAt + }, modifier = Modifier.padding(top = 4.dp), style = textStyle, ) diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryWithRelationsProvider.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryWithRelationsProvider.kt index 78347ed36c..70bc3070e8 100644 --- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryWithRelationsProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/history/components/HistoryWithRelationsProvider.kt @@ -5,57 +5,62 @@ import tachiyomi.domain.history.model.HistoryWithRelations import java.util.Date internal class HistoryWithRelationsProvider : PreviewParameterProvider { - - private val simple = HistoryWithRelations( - id = 1L, - chapterId = 2L, - mangaId = 3L, - title = "Test Title", - chapterNumber = 10.2, - readAt = Date(1697247357L), - readDuration = 123L, - coverData = tachiyomi.domain.manga.model.MangaCover( + private val simple = + HistoryWithRelations( + id = 1L, + chapterId = 2L, mangaId = 3L, - sourceId = 4L, - isMangaFavorite = false, - url = "https://example.com/cover.png", - lastModified = 5L, - ), - ) + title = "Test Title", + chapterNumber = 10.2, + readAt = Date(1697247357L), + readDuration = 123L, + coverData = + tachiyomi.domain.manga.model.MangaCover( + mangaId = 3L, + sourceId = 4L, + isMangaFavorite = false, + url = "https://example.com/cover.png", + lastModified = 5L, + ), + ) - private val historyWithoutReadAt = HistoryWithRelations( - id = 1L, - chapterId = 2L, - mangaId = 3L, - title = "Test Title", - chapterNumber = 10.2, - readAt = null, - readDuration = 123L, - coverData = tachiyomi.domain.manga.model.MangaCover( + private val historyWithoutReadAt = + HistoryWithRelations( + id = 1L, + chapterId = 2L, mangaId = 3L, - sourceId = 4L, - isMangaFavorite = false, - url = "https://example.com/cover.png", - lastModified = 5L, - ), - ) + title = "Test Title", + chapterNumber = 10.2, + readAt = null, + readDuration = 123L, + coverData = + tachiyomi.domain.manga.model.MangaCover( + mangaId = 3L, + sourceId = 4L, + isMangaFavorite = false, + url = "https://example.com/cover.png", + lastModified = 5L, + ), + ) - private val historyWithNegativeChapterNumber = HistoryWithRelations( - id = 1L, - chapterId = 2L, - mangaId = 3L, - title = "Test Title", - chapterNumber = -2.0, - readAt = Date(1697247357L), - readDuration = 123L, - coverData = tachiyomi.domain.manga.model.MangaCover( + private val historyWithNegativeChapterNumber = + HistoryWithRelations( + id = 1L, + chapterId = 2L, mangaId = 3L, - sourceId = 4L, - isMangaFavorite = false, - url = "https://example.com/cover.png", - lastModified = 5L, - ), - ) + title = "Test Title", + chapterNumber = -2.0, + readAt = Date(1697247357L), + readDuration = 123L, + coverData = + tachiyomi.domain.manga.model.MangaCover( + mangaId = 3L, + sourceId = 4L, + isMangaFavorite = false, + url = "https://example.com/cover.png", + lastModified = 5L, + ), + ) override val values: Sequence get() = sequenceOf(simple, historyWithoutReadAt, historyWithNegativeChapterNumber) diff --git a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt index ad0733606a..adbf30bc07 100644 --- a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt @@ -43,48 +43,52 @@ fun LibrarySettingsDialog( ) { TabbedDialog( onDismissRequest = onDismissRequest, - tabTitles = persistentListOf( - stringResource(MR.strings.action_filter), - stringResource(MR.strings.action_sort), - stringResource(MR.strings.action_display), - ), + tabTitles = + persistentListOf( + stringResource(MR.strings.action_filter), + stringResource(MR.strings.action_sort), + stringResource(MR.strings.action_display), + ), ) { page -> Column( - modifier = Modifier - .padding(vertical = TabbedDialogPaddings.Vertical) - .verticalScroll(rememberScrollState()), + modifier = + Modifier + .padding(vertical = TabbedDialogPaddings.Vertical) + .verticalScroll(rememberScrollState()), ) { when (page) { - 0 -> FilterPage( - screenModel = screenModel, - ) - 1 -> SortPage( - category = category, - screenModel = screenModel, - ) - 2 -> DisplayPage( - screenModel = screenModel, - ) + 0 -> + FilterPage( + screenModel = screenModel, + ) + 1 -> + SortPage( + category = category, + screenModel = screenModel, + ) + 2 -> + DisplayPage( + screenModel = screenModel, + ) } } } } @Composable -private fun ColumnScope.FilterPage( - screenModel: LibrarySettingsScreenModel, -) { +private fun ColumnScope.FilterPage(screenModel: LibrarySettingsScreenModel) { val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState() val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState() val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateMangaRestrictions().collectAsState() TriStateItem( label = stringResource(MR.strings.label_downloaded), - state = if (downloadedOnly) { - TriState.ENABLED_IS - } else { - filterDownloaded - }, + state = + if (downloadedOnly) { + TriState.ENABLED_IS + } else { + filterDownloaded + }, enabled = !downloadedOnly, onClick = { screenModel.toggleFilter(LibraryPreferences::filterDownloaded) }, ) @@ -183,35 +187,37 @@ private fun ColumnScope.SortPage( sortDescending = sortDescending.takeIf { sortingMode == mode }, onClick = { val isTogglingDirection = sortingMode == mode - val direction = when { - isTogglingDirection -> if (sortDescending) { - LibrarySort.Direction.Ascending - } else { - LibrarySort.Direction.Descending + val direction = + when { + isTogglingDirection -> + if (sortDescending) { + LibrarySort.Direction.Ascending + } else { + LibrarySort.Direction.Descending + } + else -> + if (sortDescending) { + LibrarySort.Direction.Descending + } else { + LibrarySort.Direction.Ascending + } } - else -> if (sortDescending) { - LibrarySort.Direction.Descending - } else { - LibrarySort.Direction.Ascending - } - } screenModel.setSort(category, mode, direction) }, ) } } -private val displayModes = listOf( - MR.strings.action_display_grid to LibraryDisplayMode.CompactGrid, - MR.strings.action_display_comfortable_grid to LibraryDisplayMode.ComfortableGrid, - MR.strings.action_display_cover_only_grid to LibraryDisplayMode.CoverOnlyGrid, - MR.strings.action_display_list to LibraryDisplayMode.List, -) +private val displayModes = + listOf( + MR.strings.action_display_grid to LibraryDisplayMode.CompactGrid, + MR.strings.action_display_comfortable_grid to LibraryDisplayMode.ComfortableGrid, + MR.strings.action_display_cover_only_grid to LibraryDisplayMode.CoverOnlyGrid, + MR.strings.action_display_list to LibraryDisplayMode.List, + ) @Composable -private fun ColumnScope.DisplayPage( - screenModel: LibrarySettingsScreenModel, -) { +private fun ColumnScope.DisplayPage(screenModel: LibrarySettingsScreenModel) { val displayMode by screenModel.libraryPreferences.displayMode().collectAsState() SettingsChipRow(MR.strings.action_display_mode) { displayModes.map { (titleRes, mode) -> @@ -225,24 +231,26 @@ private fun ColumnScope.DisplayPage( if (displayMode != LibraryDisplayMode.List) { val configuration = LocalConfiguration.current - val columnPreference = remember { - if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - screenModel.libraryPreferences.landscapeColumns() - } else { - screenModel.libraryPreferences.portraitColumns() + val columnPreference = + remember { + if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + screenModel.libraryPreferences.landscapeColumns() + } else { + screenModel.libraryPreferences.portraitColumns() + } } - } val columns by columnPreference.collectAsState() SliderItem( label = stringResource(MR.strings.pref_library_columns), max = 10, value = columns, - valueText = if (columns > 0) { - stringResource(MR.strings.pref_library_columns_per_row, columns) - } else { - stringResource(MR.strings.label_default) - }, + valueText = + if (columns > 0) { + stringResource(MR.strings.pref_library_columns_per_row, columns) + } else { + stringResource(MR.strings.label_default) + }, onChange = columnPreference::set, ) } 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 b4a4c2cc0f..7b38c02170 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 @@ -88,9 +88,10 @@ fun MangaCompactGridItem( MangaGridCover( cover = { MangaCover.Book( - modifier = Modifier - .fillMaxWidth() - .alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), + modifier = + Modifier + .fillMaxWidth() + .alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), data = coverData, ) }, @@ -107,9 +108,10 @@ fun MangaCompactGridItem( size = ContinueReadingButtonSizeLarge, iconSize = ContinueReadingButtonIconSizeLarge, onClick = onClickContinueReading, - modifier = Modifier - .padding(ContinueReadingButtonGridPadding) - .align(Alignment.BottomEnd), + modifier = + Modifier + .padding(ContinueReadingButtonGridPadding) + .align(Alignment.BottomEnd), ) } }, @@ -126,34 +128,37 @@ private fun BoxScope.CoverTextOverlay( onClickContinueReading: (() -> Unit)? = null, ) { Box( - modifier = Modifier - .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)) - .background( - Brush.verticalGradient( - 0f to Color.Transparent, - 1f to Color(0xAA000000), - ), - ) - .fillMaxHeight(0.33f) - .fillMaxWidth() - .align(Alignment.BottomCenter), + modifier = + Modifier + .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)) + .background( + Brush.verticalGradient( + 0f to Color.Transparent, + 1f to Color(0xAA000000), + ), + ).fillMaxHeight(0.33f) + .fillMaxWidth() + .align(Alignment.BottomCenter), ) Row( modifier = Modifier.align(Alignment.BottomStart), verticalAlignment = Alignment.Bottom, ) { GridItemTitle( - modifier = Modifier - .weight(1f) - .padding(8.dp), + modifier = + Modifier + .weight(1f) + .padding(8.dp), title = title, - style = MaterialTheme.typography.titleSmall.copy( - color = Color.White, - shadow = Shadow( - color = Color.Black, - blurRadius = 4f, + style = + MaterialTheme.typography.titleSmall.copy( + color = Color.White, + shadow = + Shadow( + color = Color.Black, + blurRadius = 4f, + ), ), - ), minLines = 1, ) if (onClickContinueReading != null) { @@ -161,10 +166,11 @@ private fun BoxScope.CoverTextOverlay( size = ContinueReadingButtonSizeSmall, iconSize = ContinueReadingButtonIconSizeSmall, onClick = onClickContinueReading, - modifier = Modifier.padding( - end = ContinueReadingButtonGridPadding, - bottom = ContinueReadingButtonGridPadding, - ), + modifier = + Modifier.padding( + end = ContinueReadingButtonGridPadding, + bottom = ContinueReadingButtonGridPadding, + ), ) } } @@ -195,9 +201,10 @@ fun MangaComfortableGridItem( MangaGridCover( cover = { MangaCover.Book( - modifier = Modifier - .fillMaxWidth() - .alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), + modifier = + Modifier + .fillMaxWidth() + .alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), data = coverData, ) }, @@ -209,9 +216,10 @@ fun MangaComfortableGridItem( size = ContinueReadingButtonSizeLarge, iconSize = ContinueReadingButtonIconSizeLarge, onClick = onClickContinueReading, - modifier = Modifier - .padding(ContinueReadingButtonGridPadding) - .align(Alignment.BottomEnd), + modifier = + Modifier + .padding(ContinueReadingButtonGridPadding) + .align(Alignment.BottomEnd), ) } }, @@ -239,26 +247,29 @@ private fun MangaGridCover( content: @Composable (BoxScope.() -> Unit)? = null, ) { Box( - modifier = modifier - .fillMaxWidth() - .aspectRatio(MangaCover.Book.ratio), + modifier = + modifier + .fillMaxWidth() + .aspectRatio(MangaCover.Book.ratio), ) { cover() content?.invoke(this) if (badgesStart != null) { BadgeGroup( - modifier = Modifier - .padding(4.dp) - .align(Alignment.TopStart), + modifier = + Modifier + .padding(4.dp) + .align(Alignment.TopStart), content = badgesStart, ) } if (badgesEnd != null) { BadgeGroup( - modifier = Modifier - .padding(4.dp) - .align(Alignment.TopEnd), + modifier = + Modifier + .padding(4.dp) + .align(Alignment.TopEnd), content = badgesEnd, ) } @@ -297,20 +308,21 @@ private fun GridItemSelectable( content: @Composable () -> Unit, ) { Box( - modifier = modifier - .clip(MaterialTheme.shapes.small) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - ) - .selectedOutline(isSelected = isSelected, color = MaterialTheme.colorScheme.secondary) - .padding(4.dp), + modifier = + modifier + .clip(MaterialTheme.shapes.small) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ).selectedOutline(isSelected = isSelected, color = MaterialTheme.colorScheme.secondary) + .padding(4.dp), ) { - val contentColor = if (isSelected) { - MaterialTheme.colorScheme.onSecondary - } else { - LocalContentColor.current - } + val contentColor = + if (isSelected) { + MaterialTheme.colorScheme.onSecondary + } else { + LocalContentColor.current + } CompositionLocalProvider(LocalContentColor provides contentColor) { content() } @@ -340,27 +352,29 @@ fun MangaListItem( onClickContinueReading: (() -> Unit)? = null, ) { Row( - modifier = Modifier - .selectedBackground(isSelected) - .height(56.dp) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - ) - .padding(horizontal = 16.dp, vertical = 8.dp), + modifier = + Modifier + .selectedBackground(isSelected) + .height(56.dp) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ).padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { MangaCover.Square( - modifier = Modifier - .fillMaxHeight() - .alpha(coverAlpha), + modifier = + Modifier + .fillMaxHeight() + .alpha(coverAlpha), data = coverData, ) Text( text = title, - modifier = Modifier - .padding(horizontal = 16.dp) - .weight(1f), + modifier = + Modifier + .padding(horizontal = 16.dp) + .weight(1f), maxLines = 2, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium, @@ -371,7 +385,7 @@ fun MangaListItem( size = ContinueReadingButtonSizeSmall, iconSize = ContinueReadingButtonIconSizeSmall, onClick = onClickContinueReading, - modifier = Modifier.padding(start = ContinueReadingButtonListSpacing) + modifier = Modifier.padding(start = ContinueReadingButtonListSpacing), ) } } @@ -388,11 +402,12 @@ private fun ContinueReadingButton( FilledIconButton( onClick = onClick, shape = MaterialTheme.shapes.small, - colors = IconButtonDefaults.filledIconButtonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f), - contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer), - ), - modifier = Modifier.size(size) + colors = + IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f), + contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer), + ), + modifier = Modifier.size(size), ) { Icon( imageVector = Icons.Filled.PlayArrow, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt index 379d4054c2..ecebd450b6 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt @@ -37,13 +37,14 @@ internal fun LibraryComfortableGrid( MangaComfortableGridItem( isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, title = manga.title, - coverData = MangaCover( - mangaId = manga.id, - sourceId = manga.source, - isMangaFavorite = manga.favorite, - url = manga.thumbnailUrl, - lastModified = manga.coverLastModified, - ), + coverData = + MangaCover( + mangaId = manga.id, + sourceId = manga.source, + isMangaFavorite = manga.favorite, + url = manga.thumbnailUrl, + lastModified = manga.coverLastModified, + ), coverBadgeStart = { DownloadsBadge(count = libraryItem.downloadCount) UnreadBadge(count = libraryItem.unreadCount) @@ -56,11 +57,12 @@ internal fun LibraryComfortableGrid( }, onLongClick = { onLongClick(libraryItem.libraryManga) }, onClick = { onClick(libraryItem.libraryManga) }, - onClickContinueReading = if (onClickContinueReading != null && libraryItem.unreadCount > 0) { - { onClickContinueReading(libraryItem.libraryManga) } - } else { - null - }, + onClickContinueReading = + if (onClickContinueReading != null && libraryItem.unreadCount > 0) { + { onClickContinueReading(libraryItem.libraryManga) } + } else { + null + }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt index 56caa99e06..16ba3f2f1c 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt @@ -38,13 +38,14 @@ internal fun LibraryCompactGrid( MangaCompactGridItem( isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, title = manga.title.takeIf { showTitle }, - coverData = MangaCover( - mangaId = manga.id, - sourceId = manga.source, - isMangaFavorite = manga.favorite, - url = manga.thumbnailUrl, - lastModified = manga.coverLastModified, - ), + coverData = + MangaCover( + mangaId = manga.id, + sourceId = manga.source, + isMangaFavorite = manga.favorite, + url = manga.thumbnailUrl, + lastModified = manga.coverLastModified, + ), coverBadgeStart = { DownloadsBadge(count = libraryItem.downloadCount) UnreadBadge(count = libraryItem.unreadCount) @@ -57,11 +58,12 @@ internal fun LibraryCompactGrid( }, onLongClick = { onLongClick(libraryItem.libraryManga) }, onClick = { onClick(libraryItem.libraryManga) }, - onClickContinueReading = if (onClickContinueReading != null && libraryItem.unreadCount > 0) { - { onClickContinueReading(libraryItem.libraryManga) } - } else { - null - }, + onClickContinueReading = + if (onClickContinueReading != null && libraryItem.unreadCount > 0) { + { onClickContinueReading(libraryItem.libraryManga) } + } else { + null + }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt index 61da10345e..3f29dcf83b 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt @@ -47,11 +47,12 @@ fun LibraryContent( getLibraryForPage: (Int) -> List, ) { Column( - modifier = Modifier.padding( - top = contentPadding.calculateTopPadding(), - start = contentPadding.calculateStartPadding(LocalLayoutDirection.current), - end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), - ), + modifier = + Modifier.padding( + top = contentPadding.calculateTopPadding(), + start = contentPadding.calculateStartPadding(LocalLayoutDirection.current), + end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), + ), ) { val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) } val pagerState = rememberPagerState(coercedCurrentPage) { categories.size } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt index cb57bae0ea..caee75f2f9 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt @@ -47,13 +47,14 @@ internal fun LibraryList( MangaListItem( isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, title = manga.title, - coverData = MangaCover( - mangaId = manga.id, - sourceId = manga.source, - isMangaFavorite = manga.favorite, - url = manga.thumbnailUrl, - lastModified = manga.coverLastModified, - ), + coverData = + MangaCover( + mangaId = manga.id, + sourceId = manga.source, + isMangaFavorite = manga.favorite, + url = manga.thumbnailUrl, + lastModified = manga.coverLastModified, + ), badge = { DownloadsBadge(count = libraryItem.downloadCount) UnreadBadge(count = libraryItem.unreadCount) @@ -64,11 +65,12 @@ internal fun LibraryList( }, onLongClick = { onLongClick(libraryItem.libraryManga) }, onClick = { onClick(libraryItem.libraryManga) }, - onClickContinueReading = if (onClickContinueReading != null && libraryItem.unreadCount > 0) { - { onClickContinueReading(libraryItem.libraryManga) } - } else { - null - }, + onClickContinueReading = + if (onClickContinueReading != null && libraryItem.unreadCount > 0) { + { onClickContinueReading(libraryItem.libraryManga) } + } else { + null + }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt index 6487ab39f6..a00b1b4050 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt @@ -123,23 +123,26 @@ private fun LibraryPagerEmptyScreen( contentPadding: PaddingValues, onGlobalSearchClicked: () -> Unit, ) { - val msg = when { - !searchQuery.isNullOrEmpty() -> MR.strings.no_results_found - hasActiveFilters -> MR.strings.error_no_match - else -> MR.strings.information_no_manga_category - } + val msg = + when { + !searchQuery.isNullOrEmpty() -> MR.strings.no_results_found + hasActiveFilters -> MR.strings.error_no_match + else -> MR.strings.information_no_manga_category + } Column( - modifier = Modifier - .padding(contentPadding + PaddingValues(8.dp)) - .fillMaxSize() - .verticalScroll(rememberScrollState()), + modifier = + Modifier + .padding(contentPadding + PaddingValues(8.dp)) + .fillMaxSize() + .verticalScroll(rememberScrollState()), ) { if (!searchQuery.isNullOrEmpty()) { GlobalSearchItem( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally), + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally), searchQuery = searchQuery, onClick = onGlobalSearchClicked, ) diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt index 9845ca8166..5da564ef7e 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt @@ -41,23 +41,25 @@ fun LibraryToolbar( onSearchQueryChange: (String?) -> Unit, scrollBehavior: TopAppBarScrollBehavior?, ) = when { - selectedCount > 0 -> LibrarySelectionToolbar( - selectedCount = selectedCount, - onClickUnselectAll = onClickUnselectAll, - onClickSelectAll = onClickSelectAll, - onClickInvertSelection = onClickInvertSelection, - ) - else -> LibraryRegularToolbar( - title = title, - hasFilters = hasActiveFilters, - searchQuery = searchQuery, - onSearchQueryChange = onSearchQueryChange, - onClickFilter = onClickFilter, - onClickRefresh = onClickRefresh, - onClickGlobalUpdate = onClickGlobalUpdate, - onClickOpenRandomManga = onClickOpenRandomManga, - scrollBehavior = scrollBehavior, - ) + selectedCount > 0 -> + LibrarySelectionToolbar( + selectedCount = selectedCount, + onClickUnselectAll = onClickUnselectAll, + onClickSelectAll = onClickSelectAll, + onClickInvertSelection = onClickInvertSelection, + ) + else -> + LibraryRegularToolbar( + title = title, + hasFilters = hasActiveFilters, + searchQuery = searchQuery, + onSearchQueryChange = onSearchQueryChange, + onClickFilter = onClickFilter, + onClickRefresh = onClickRefresh, + onClickGlobalUpdate = onClickGlobalUpdate, + onClickOpenRandomManga = onClickOpenRandomManga, + scrollBehavior = scrollBehavior, + ) } @Composable diff --git a/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt index 5f98dbb862..1f331474a7 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt @@ -65,11 +65,12 @@ fun ChapterSettingsDialog( TabbedDialog( onDismissRequest = onDismissRequest, - tabTitles = persistentListOf( - stringResource(MR.strings.action_filter), - stringResource(MR.strings.action_sort), - stringResource(MR.strings.action_display), - ), + tabTitles = + persistentListOf( + stringResource(MR.strings.action_filter), + stringResource(MR.strings.action_sort), + stringResource(MR.strings.action_display), + ), tabOverflowMenuContent = { closeMenu -> DropdownMenuItem( text = { Text(stringResource(MR.strings.set_chapter_settings_as_default)) }, @@ -88,16 +89,18 @@ fun ChapterSettingsDialog( }, ) { page -> Column( - modifier = Modifier - .padding(vertical = TabbedDialogPaddings.Vertical) - .verticalScroll(rememberScrollState()), + modifier = + Modifier + .padding(vertical = TabbedDialogPaddings.Vertical) + .verticalScroll(rememberScrollState()), ) { when (page) { 0 -> { FilterPage( downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED, - onDownloadFilterChanged = onDownloadFilterChanged - .takeUnless { manga?.forceDownloaded() == true }, + onDownloadFilterChanged = + onDownloadFilterChanged + .takeUnless { manga?.forceDownloaded() == true }, unreadFilter = manga?.unreadFilter ?: TriState.DISABLED, onUnreadFilterChanged = onUnreadFilterChanged, bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED, @@ -162,21 +165,23 @@ fun ScanlatorFilterItem( onClick: () -> Unit, ) { Row( - modifier = Modifier - .clickable(onClick = onClick) - .fillMaxWidth() - .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp), + modifier = + Modifier + .clickable(onClick = onClick) + .fillMaxWidth() + .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp), ) { Icon( imageVector = Icons.Outlined.PeopleAlt, contentDescription = null, - tint = if (active) { - MaterialTheme.colorScheme.active - } else { - LocalContentColor.current - }, + tint = + if (active) { + MaterialTheme.colorScheme.active + } else { + LocalContentColor.current + }, ) Text( text = stringResource(MR.strings.scanlator), diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index 0707ac2bc6..0a919fa80d 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -45,12 +45,12 @@ fun DuplicateMangaDialog( onDismissRequest = onDismissRequest, ) { Column( - modifier = Modifier - .padding( - vertical = TabbedDialogPaddings.Vertical, - horizontal = TabbedDialogPaddings.Horizontal, - ) - .fillMaxWidth(), + modifier = + Modifier + .padding( + vertical = TabbedDialogPaddings.Vertical, + horizontal = TabbedDialogPaddings.Horizontal, + ).fillMaxWidth(), ) { Text( modifier = Modifier.padding(TitlePadding), @@ -97,18 +97,20 @@ fun DuplicateMangaDialog( ) Row( - modifier = Modifier - .sizeIn(minHeight = minHeight) - .clickable { onDismissRequest.invoke() } - .padding(ButtonPadding) - .fillMaxWidth(), + modifier = + Modifier + .sizeIn(minHeight = minHeight) + .clickable { onDismissRequest.invoke() } + .padding(ButtonPadding) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) { Text( - modifier = Modifier - .padding(vertical = 8.dp), + modifier = + Modifier + .padding(vertical = 8.dp), text = stringResource(MR.strings.action_cancel), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleLarge, 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 91546f57f4..bd4642fe49 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -94,34 +94,27 @@ fun MangaScreen( onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, - // For tags menu onTagSearch: (String) -> Unit, - onFilterButtonClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, - // For cover dialog onCoverClicked: () -> Unit, - // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditFetchIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, - // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List) -> Unit, - // For chapter swipe onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, - // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, @@ -221,35 +214,28 @@ private fun MangaScreenSmallImpl( onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, - // For tags menu onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, - onFilterClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, - // For cover dialog onCoverClicked: () -> Unit, - // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, - // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List) -> Unit, - // For chapter swipe onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, - // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, @@ -257,13 +243,14 @@ private fun MangaScreenSmallImpl( ) { val chapterListState = rememberLazyListState() - val (chapters, listItem, isAnySelected) = remember(state) { - Triple( - first = state.processedChapters, - second = state.chapterListItems, - third = state.isAnySelected, - ) - } + val (chapters, listItem, isAnySelected) = + remember(state) { + Triple( + first = state.processedChapters, + second = state.chapterListItems, + third = state.isAnySelected, + ) + } val internalOnBackPressed = { if (isAnySelected) { @@ -276,9 +263,10 @@ private fun MangaScreenSmallImpl( Scaffold( topBar = { - val selectedChapterCount: Int = remember(chapters) { - chapters.count { it.selected } - } + val selectedChapterCount: Int = + remember(chapters) { + chapters.count { it.selected } + } val isFirstItemVisible by remember { derivedStateOf { chapterListState.firstVisibleItemIndex == 0 } } @@ -311,9 +299,10 @@ private fun MangaScreenSmallImpl( ) }, bottomBar = { - val selectedChapters = remember(chapters) { - chapters.filter { it.selected } - } + val selectedChapters = + remember(chapters) { + chapters.filter { it.selected } + } SharedMangaBottomActionMenu( selected = selectedChapters, onMultiBookmarkClicked = onMultiBookmarkClicked, @@ -326,9 +315,10 @@ private fun MangaScreenSmallImpl( }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { - val isFABVisible = remember(chapters) { - chapters.fastAny { !it.chapter.read } && !isAnySelected - } + val isFABVisible = + remember(chapters) { + chapters.fastAny { !it.chapter.read } && !isAnySelected + } AnimatedVisibility( visible = isFABVisible, enter = fadeIn(), @@ -336,9 +326,10 @@ private fun MangaScreenSmallImpl( ) { ExtendedFloatingActionButton( text = { - val isReading = remember(state.chapters) { - state.chapters.fastAny { it.chapter.read } - } + val isReading = + remember(state.chapters) { + state.chapters.fastAny { it.chapter.read } + } Text( text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start), ) @@ -367,11 +358,12 @@ private fun MangaScreenSmallImpl( LazyColumn( modifier = Modifier.fillMaxHeight(), state = chapterListState, - contentPadding = PaddingValues( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - bottom = contentPadding.calculateBottomPadding(), - ), + contentPadding = + PaddingValues( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + bottom = contentPadding.calculateBottomPadding(), + ), ) { item( key = MangaScreenItem.INFO_BOX, @@ -427,9 +419,10 @@ private fun MangaScreenSmallImpl( key = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER, ) { - val missingChapterCount = remember(chapters) { - chapters.map { it.chapter.chapterNumber }.missingChaptersCount() - } + val missingChapterCount = + remember(chapters) { + chapters.map { it.chapter.chapterNumber }.missingChaptersCount() + } ChapterHeader( enabled = !isAnySelected, chapterCount = chapters.size, @@ -469,35 +462,28 @@ fun MangaScreenLargeImpl( onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: () -> Unit, - // For tags menu onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, - onFilterButtonClicked: () -> Unit, onRefresh: () -> Unit, onContinueReading: () -> Unit, onSearch: (query: String, global: Boolean) -> Unit, - // For cover dialog onCoverClicked: () -> Unit, - // For top action menu onShareClicked: (() -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, - // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List) -> Unit, - // For swipe actions onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, - // Chapter selection onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, @@ -506,13 +492,14 @@ fun MangaScreenLargeImpl( val layoutDirection = LocalLayoutDirection.current val density = LocalDensity.current - val (chapters, listItem, isAnySelected) = remember(state) { - Triple( - first = state.processedChapters, - second = state.chapterListItems, - third = state.isAnySelected, - ) - } + val (chapters, listItem, isAnySelected) = + remember(state) { + Triple( + first = state.processedChapters, + second = state.chapterListItems, + third = state.isAnySelected, + ) + } val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() var topBarHeight by remember { mutableIntStateOf(0) } @@ -530,9 +517,10 @@ fun MangaScreenLargeImpl( Scaffold( topBar = { - val selectedChapterCount = remember(chapters) { - chapters.count { it.selected } - } + val selectedChapterCount = + remember(chapters) { + chapters.count { it.selected } + } MangaToolbar( modifier = Modifier.onSizeChanged { topBarHeight = it.height }, title = state.manga.title, @@ -556,9 +544,10 @@ fun MangaScreenLargeImpl( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomEnd, ) { - val selectedChapters = remember(chapters) { - chapters.filter { it.selected } - } + val selectedChapters = + remember(chapters) { + chapters.filter { it.selected } + } SharedMangaBottomActionMenu( selected = selectedChapters, onMultiBookmarkClicked = onMultiBookmarkClicked, @@ -572,9 +561,10 @@ fun MangaScreenLargeImpl( }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { - val isFABVisible = remember(chapters) { - chapters.fastAny { !it.chapter.read } && !isAnySelected - } + val isFABVisible = + remember(chapters) { + chapters.fastAny { !it.chapter.read } && !isAnySelected + } AnimatedVisibility( visible = isFABVisible, enter = fadeIn(), @@ -582,13 +572,15 @@ fun MangaScreenLargeImpl( ) { ExtendedFloatingActionButton( text = { - val isReading = remember(state.chapters) { - state.chapters.fastAny { it.chapter.read } - } + val isReading = + remember(state.chapters) { + state.chapters.fastAny { it.chapter.read } + } Text( - text = stringResource( - if (isReading) MR.strings.action_resume else MR.strings.action_start, - ), + text = + stringResource( + if (isReading) MR.strings.action_resume else MR.strings.action_start, + ), ) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, @@ -602,22 +594,25 @@ fun MangaScreenLargeImpl( refreshing = state.isRefreshingData, onRefresh = onRefresh, enabled = !isAnySelected, - indicatorPadding = PaddingValues( - start = insetPadding.calculateStartPadding(layoutDirection), - top = with(density) { topBarHeight.toDp() }, - end = insetPadding.calculateEndPadding(layoutDirection), - ), + indicatorPadding = + PaddingValues( + start = insetPadding.calculateStartPadding(layoutDirection), + top = with(density) { topBarHeight.toDp() }, + end = insetPadding.calculateEndPadding(layoutDirection), + ), ) { TwoPanelBox( - modifier = Modifier.padding( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - ), + modifier = + Modifier.padding( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + ), startContent = { Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(bottom = contentPadding.calculateBottomPadding()), + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .padding(bottom = contentPadding.calculateBottomPadding()), ) { MangaInfoBox( isTabletUi = true, @@ -661,18 +656,20 @@ fun MangaScreenLargeImpl( LazyColumn( modifier = Modifier.fillMaxHeight(), state = chapterListState, - contentPadding = PaddingValues( - top = contentPadding.calculateTopPadding(), - bottom = contentPadding.calculateBottomPadding(), - ), + contentPadding = + PaddingValues( + top = contentPadding.calculateTopPadding(), + bottom = contentPadding.calculateBottomPadding(), + ), ) { item( key = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER, ) { - val missingChapterCount = remember(chapters) { - chapters.map { it.chapter.chapterNumber }.missingChaptersCount() - } + val missingChapterCount = + remember(chapters) { + chapters.map { it.chapter.chapterNumber }.missingChaptersCount() + } ChapterHeader( enabled = !isAnySelected, chapterCount = chapters.size, @@ -714,31 +711,38 @@ private fun SharedMangaBottomActionMenu( MangaBottomActionMenu( visible = selected.isNotEmpty(), modifier = modifier.fillMaxWidth(fillFraction), - onBookmarkClicked = { - onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, true) - }.takeIf { selected.fastAny { !it.chapter.bookmark } }, - onRemoveBookmarkClicked = { - onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, false) - }.takeIf { selected.fastAll { it.chapter.bookmark } }, - onMarkAsReadClicked = { - onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, true) - }.takeIf { selected.fastAny { !it.chapter.read } }, - onMarkAsUnreadClicked = { - onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, false) - }.takeIf { selected.fastAny { it.chapter.read || it.chapter.lastPageRead > 0L } }, - onMarkPreviousAsReadClicked = { - onMarkPreviousAsReadClicked(selected[0].chapter) - }.takeIf { selected.size == 1 }, - onDownloadClicked = { - onDownloadChapter!!(selected.toList(), ChapterDownloadAction.START) - }.takeIf { - onDownloadChapter != null && selected.fastAny { it.downloadState != Download.State.DOWNLOADED } - }, - onDeleteClicked = { - onMultiDeleteClicked(selected.fastMap { it.chapter }) - }.takeIf { - selected.fastAny { it.downloadState == Download.State.DOWNLOADED } - }, + onBookmarkClicked = + { + onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, true) + }.takeIf { selected.fastAny { !it.chapter.bookmark } }, + onRemoveBookmarkClicked = + { + onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, false) + }.takeIf { selected.fastAll { it.chapter.bookmark } }, + onMarkAsReadClicked = + { + onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, true) + }.takeIf { selected.fastAny { !it.chapter.read } }, + onMarkAsUnreadClicked = + { + onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, false) + }.takeIf { selected.fastAny { it.chapter.read || it.chapter.lastPageRead > 0L } }, + onMarkPreviousAsReadClicked = + { + onMarkPreviousAsReadClicked(selected[0].chapter) + }.takeIf { selected.size == 1 }, + onDownloadClicked = + { + onDownloadChapter!!(selected.toList(), ChapterDownloadAction.START) + }.takeIf { + onDownloadChapter != null && selected.fastAny { it.downloadState != Download.State.DOWNLOADED } + }, + onDeleteClicked = + { + onMultiDeleteClicked(selected.fastMap { it.chapter }) + }.takeIf { + selected.fastAny { it.downloadState == Download.State.DOWNLOADED } + }, ) } @@ -771,23 +775,25 @@ private fun LazyListScope.sharedChapterItems( } is ChapterList.Item -> { MangaChapterListItem( - title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { - stringResource( - MR.strings.display_mode_chapter, - formatChapterNumber(item.chapter.chapterNumber), - ) - } else { - item.chapter.name - }, - date = relativeDateText(item.chapter.dateUpload), - readProgress = item.chapter.lastPageRead - .takeIf { !item.chapter.read && it > 0L } - ?.let { + title = + if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { stringResource( - MR.strings.chapter_progress, - it + 1, + MR.strings.display_mode_chapter, + formatChapterNumber(item.chapter.chapterNumber), ) + } else { + item.chapter.name }, + date = relativeDateText(item.chapter.dateUpload), + readProgress = + item.chapter.lastPageRead + .takeIf { !item.chapter.read && it > 0L } + ?.let { + stringResource( + MR.strings.chapter_progress, + it + 1, + ) + }, scanlator = item.chapter.scanlator.takeIf { !it.isNullOrBlank() }, read = item.chapter.read, bookmark = item.chapter.bookmark, @@ -809,11 +815,12 @@ private fun LazyListScope.sharedChapterItems( onChapterClicked = onChapterClicked, ) }, - onDownloadClick = if (onDownloadChapter != null) { - { onDownloadChapter(listOf(item), it) } - } else { - null - }, + onDownloadClick = + if (onDownloadChapter != null) { + { onDownloadChapter(listOf(item), it) } + } else { + null + }, onChapterSwipe = { onChapterSwipe(item, it) }, 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 48952d8a8c..a5c27c6d66 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 @@ -28,10 +28,11 @@ fun BaseMangaListItem( content: @Composable RowScope.() -> Unit = { defaultContent(manga) }, ) { Row( - modifier = modifier - .clickable(onClick = onClickItem) - .height(56.dp) - .padding(horizontal = MaterialTheme.padding.medium), + modifier = + modifier + .clickable(onClick = onClickItem) + .height(56.dp) + .padding(horizontal = MaterialTheme.padding.medium), verticalAlignment = Alignment.CenterVertically, ) { cover() @@ -42,9 +43,10 @@ fun BaseMangaListItem( private val defaultCover: @Composable RowScope.(Manga, () -> Unit) -> Unit = { manga, onClick -> MangaCover.Square( - modifier = Modifier - .padding(vertical = MaterialTheme.padding.small) - .fillMaxHeight(), + modifier = + Modifier + .padding(vertical = MaterialTheme.padding.small) + .fillMaxHeight(), data = manga, onClick = onClick, ) @@ -54,8 +56,9 @@ private val defaultContent: @Composable RowScope.(Manga) -> Unit = { Box(modifier = Modifier.weight(1f)) { Text( text = it.title, - modifier = Modifier - .padding(start = MaterialTheme.padding.medium), + modifier = + Modifier + .padding(start = MaterialTheme.padding.medium), overflow = TextOverflow.Ellipsis, maxLines = 1, style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt index 518f09d4bb..f502660d4a 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt @@ -55,28 +55,32 @@ fun ChapterDownloadIndicator( modifier: Modifier = Modifier, ) { when (val downloadState = downloadStateProvider()) { - Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator( - enabled = enabled, - modifier = modifier, - onClick = onClick, - ) - Download.State.QUEUE, Download.State.DOWNLOADING -> DownloadingIndicator( - enabled = enabled, - modifier = modifier, - downloadState = downloadState, - downloadProgressProvider = downloadProgressProvider, - onClick = onClick, - ) - Download.State.DOWNLOADED -> DownloadedIndicator( - enabled = enabled, - modifier = modifier, - onClick = onClick, - ) - Download.State.ERROR -> ErrorIndicator( - enabled = enabled, - modifier = modifier, - onClick = onClick, - ) + Download.State.NOT_DOWNLOADED -> + NotDownloadedIndicator( + enabled = enabled, + modifier = modifier, + onClick = onClick, + ) + Download.State.QUEUE, Download.State.DOWNLOADING -> + DownloadingIndicator( + enabled = enabled, + modifier = modifier, + downloadState = downloadState, + downloadProgressProvider = downloadProgressProvider, + onClick = onClick, + ) + Download.State.DOWNLOADED -> + DownloadedIndicator( + enabled = enabled, + modifier = modifier, + onClick = onClick, + ) + Download.State.ERROR -> + ErrorIndicator( + enabled = enabled, + modifier = modifier, + onClick = onClick, + ) } } @@ -87,15 +91,15 @@ private fun NotDownloadedIndicator( onClick: (ChapterDownloadAction) -> Unit, ) { Box( - modifier = modifier - .size(IconButtonTokens.StateLayerSize) - .commonClickable( - enabled = enabled, - hapticFeedback = LocalHapticFeedback.current, - onLongClick = { onClick(ChapterDownloadAction.START_NOW) }, - onClick = { onClick(ChapterDownloadAction.START) }, - ) - .secondaryItemAlpha(), + modifier = + modifier + .size(IconButtonTokens.StateLayerSize) + .commonClickable( + enabled = enabled, + hapticFeedback = LocalHapticFeedback.current, + onLongClick = { onClick(ChapterDownloadAction.START_NOW) }, + onClick = { onClick(ChapterDownloadAction.START) }, + ).secondaryItemAlpha(), contentAlignment = Alignment.Center, ) { Icon( @@ -117,21 +121,23 @@ private fun DownloadingIndicator( ) { var isMenuExpanded by remember { mutableStateOf(false) } Box( - modifier = modifier - .size(IconButtonTokens.StateLayerSize) - .commonClickable( - enabled = enabled, - hapticFeedback = LocalHapticFeedback.current, - onLongClick = { onClick(ChapterDownloadAction.CANCEL) }, - onClick = { isMenuExpanded = true }, - ), + modifier = + modifier + .size(IconButtonTokens.StateLayerSize) + .commonClickable( + enabled = enabled, + hapticFeedback = LocalHapticFeedback.current, + onLongClick = { onClick(ChapterDownloadAction.CANCEL) }, + onClick = { isMenuExpanded = true }, + ), contentAlignment = Alignment.Center, ) { val arrowColor: Color val strokeColor = MaterialTheme.colorScheme.onSurfaceVariant val downloadProgress = downloadProgressProvider() - val indeterminate = downloadState == Download.State.QUEUE || - (downloadState == Download.State.DOWNLOADING && downloadProgress == 0) + val indeterminate = + downloadState == Download.State.QUEUE || + (downloadState == Download.State.DOWNLOADING && downloadProgress == 0) if (indeterminate) { arrowColor = strokeColor CircularProgressIndicator( @@ -147,11 +153,12 @@ private fun DownloadingIndicator( animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, label = "progress", ) - arrowColor = if (animatedProgress < 0.5f) { - strokeColor - } else { - MaterialTheme.colorScheme.background - } + arrowColor = + if (animatedProgress < 0.5f) { + strokeColor + } else { + MaterialTheme.colorScheme.background + } CircularProgressIndicator( progress = { animatedProgress }, modifier = IndicatorModifier, @@ -195,14 +202,15 @@ private fun DownloadedIndicator( ) { var isMenuExpanded by remember { mutableStateOf(false) } Box( - modifier = modifier - .size(IconButtonTokens.StateLayerSize) - .commonClickable( - enabled = enabled, - hapticFeedback = LocalHapticFeedback.current, - onLongClick = { isMenuExpanded = true }, - onClick = { isMenuExpanded = true }, - ), + modifier = + modifier + .size(IconButtonTokens.StateLayerSize) + .commonClickable( + enabled = enabled, + hapticFeedback = LocalHapticFeedback.current, + onLongClick = { isMenuExpanded = true }, + onClick = { isMenuExpanded = true }, + ), contentAlignment = Alignment.Center, ) { Icon( @@ -230,14 +238,15 @@ private fun ErrorIndicator( onClick: (ChapterDownloadAction) -> Unit, ) { Box( - modifier = modifier - .size(IconButtonTokens.StateLayerSize) - .commonClickable( - enabled = enabled, - hapticFeedback = LocalHapticFeedback.current, - onLongClick = { onClick(ChapterDownloadAction.START) }, - onClick = { onClick(ChapterDownloadAction.START) }, - ), + modifier = + modifier + .size(IconButtonTokens.StateLayerSize) + .commonClickable( + enabled = enabled, + hapticFeedback = LocalHapticFeedback.current, + onLongClick = { onClick(ChapterDownloadAction.START) }, + onClick = { onClick(ChapterDownloadAction.START) }, + ), contentAlignment = Alignment.Center, ) { Icon( @@ -263,10 +272,11 @@ private fun Modifier.commonClickable( onClick = onClick, role = Role.Button, interactionSource = null, - indication = ripple( - bounded = false, - radius = IconButtonTokens.StateLayerSize / 2, - ), + indication = + ripple( + bounded = false, + radius = IconButtonTokens.StateLayerSize / 2, + ), ) private val IndicatorSize = 26.dp @@ -275,8 +285,10 @@ private val IndicatorPadding = 2.dp // To match composable parameter name when used later private val IndicatorStrokeWidth = IndicatorPadding -private val IndicatorModifier = Modifier - .size(IndicatorSize) - .padding(IndicatorPadding) -private val ArrowModifier = Modifier - .size(IndicatorSize - 7.dp) +private val IndicatorModifier = + Modifier + .size(IndicatorSize) + .padding(IndicatorPadding) +private val ArrowModifier = + Modifier + .size(IndicatorSize - 7.dp) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterHeader.kt index 99ad1b37a5..09055cfdf3 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterHeader.kt @@ -26,21 +26,22 @@ fun ChapterHeader( modifier: Modifier = Modifier, ) { Column( - modifier = modifier - .fillMaxWidth() - .clickable( - enabled = enabled, - onClick = onClick, - ) - .padding(horizontal = 16.dp, vertical = 4.dp), + modifier = + modifier + .fillMaxWidth() + .clickable( + enabled = enabled, + onClick = onClick, + ).padding(horizontal = 16.dp, vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { Text( - text = if (chapterCount == null) { - stringResource(MR.strings.chapters) - } else { - pluralStringResource(MR.plurals.manga_num_chapters, count = chapterCount, chapterCount) - }, + text = + if (chapterCount == null) { + stringResource(MR.strings.chapters) + } else { + pluralStringResource(MR.plurals.manga_num_chapters, count = chapterCount, chapterCount) + }, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground, ) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt b/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt index e1f3529999..8d1b60166d 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt @@ -5,9 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @Composable -fun DotSeparatorText( - modifier: Modifier = Modifier, -) { +fun DotSeparatorText(modifier: Modifier = Modifier) { Text( text = " • ", modifier = modifier, @@ -15,9 +13,7 @@ fun DotSeparatorText( } @Composable -fun DotSeparatorNoSpaceText( - modifier: Modifier = Modifier, -) { +fun DotSeparatorNoSpaceText(modifier: Modifier = Modifier) { Text( text = "•", modifier = modifier, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt index 93b8c1843a..c47105e639 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt @@ -90,19 +90,20 @@ fun MangaBottomActionMenu( haptic.performHapticFeedback(HapticFeedbackType.LongPress) (0..<7).forEach { i -> confirm[i] = i == toConfirmIndex } resetJob?.cancel() - resetJob = scope.launch { - delay(1.seconds) - if (isActive) confirm[toConfirmIndex] = false - } + resetJob = + scope.launch { + delay(1.seconds) + if (isActive) confirm[toConfirmIndex] = false + } } Row( - modifier = Modifier - .padding( - WindowInsets.navigationBars - .only(WindowInsetsSides.Bottom) - .asPaddingValues(), - ) - .padding(horizontal = 8.dp, vertical = 12.dp), + modifier = + Modifier + .padding( + WindowInsets.navigationBars + .only(WindowInsetsSides.Bottom) + .asPaddingValues(), + ).padding(horizontal = 8.dp, vertical = 12.dp), ) { if (onBookmarkClicked != null) { Button( @@ -186,15 +187,16 @@ private fun RowScope.Button( label = "weight", ) Column( - modifier = Modifier - .size(48.dp) - .weight(animatedWeight) - .combinedClickable( - interactionSource = null, - indication = ripple(bounded = false), - onLongClick = onLongClick, - onClick = onClick, - ), + modifier = + Modifier + .size(48.dp) + .weight(animatedWeight) + .combinedClickable( + interactionSource = null, + indication = ripple(bounded = false), + onLongClick = onLongClick, + onClick = onClick, + ), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -246,18 +248,19 @@ fun LibraryBottomActionMenu( haptic.performHapticFeedback(HapticFeedbackType.LongPress) (0..<5).forEach { i -> confirm[i] = i == toConfirmIndex } resetJob?.cancel() - resetJob = scope.launch { - delay(1.seconds) - if (isActive) confirm[toConfirmIndex] = false - } + resetJob = + scope.launch { + delay(1.seconds) + if (isActive) confirm[toConfirmIndex] = false + } } Row( - modifier = Modifier - .windowInsetsPadding( - WindowInsets.navigationBars - .only(WindowInsetsSides.Bottom), - ) - .padding(horizontal = 8.dp, vertical = 12.dp), + modifier = + Modifier + .windowInsetsPadding( + WindowInsets.navigationBars + .only(WindowInsetsSides.Bottom), + ).padding(horizontal = 8.dp, vertical = 12.dp), ) { Button( title = stringResource(MR.strings.action_move_category), diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt index 6f323993c5..5bb5165d44 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt @@ -65,22 +65,24 @@ fun MangaChapterListItem( onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, modifier: Modifier = Modifier, ) { - val start = getSwipeAction( - action = chapterSwipeStartAction, - read = read, - bookmark = bookmark, - downloadState = downloadStateProvider(), - background = MaterialTheme.colorScheme.primaryContainer, - onSwipe = { onChapterSwipe(chapterSwipeStartAction) }, - ) - val end = getSwipeAction( - action = chapterSwipeEndAction, - read = read, - bookmark = bookmark, - downloadState = downloadStateProvider(), - background = MaterialTheme.colorScheme.primaryContainer, - onSwipe = { onChapterSwipe(chapterSwipeEndAction) }, - ) + val start = + getSwipeAction( + action = chapterSwipeStartAction, + read = read, + bookmark = bookmark, + downloadState = downloadStateProvider(), + background = MaterialTheme.colorScheme.primaryContainer, + onSwipe = { onChapterSwipe(chapterSwipeStartAction) }, + ) + val end = + getSwipeAction( + action = chapterSwipeEndAction, + read = read, + bookmark = bookmark, + downloadState = downloadStateProvider(), + background = MaterialTheme.colorScheme.primaryContainer, + onSwipe = { onChapterSwipe(chapterSwipeEndAction) }, + ) SwipeableActionsBox( modifier = Modifier.clipToBounds(), @@ -90,13 +92,13 @@ fun MangaChapterListItem( backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest, ) { Row( - modifier = modifier - .selectedBackground(selected) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - ) - .padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), + modifier = + modifier + .selectedBackground(selected) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ).padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), ) { Column( modifier = Modifier.weight(1f), @@ -111,9 +113,10 @@ fun MangaChapterListItem( Icon( imageVector = Icons.Filled.Circle, contentDescription = stringResource(MR.strings.unread), - modifier = Modifier - .height(8.dp) - .padding(end = 4.dp), + modifier = + Modifier + .height(8.dp) + .padding(end = 4.dp), tint = MaterialTheme.colorScheme.primary, ) } @@ -121,8 +124,9 @@ fun MangaChapterListItem( Icon( imageVector = Icons.Filled.Bookmark, contentDescription = stringResource(MR.strings.action_filter_bookmarked), - modifier = Modifier - .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), + modifier = + Modifier + .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), tint = MaterialTheme.colorScheme.primary, ) } @@ -137,11 +141,13 @@ fun MangaChapterListItem( } Row { - val subtitleStyle = MaterialTheme.typography.bodySmall - .merge( - color = LocalContentColor.current - .copy(alpha = if (read) ReadItemAlpha else SecondaryItemAlpha) - ) + val subtitleStyle = + MaterialTheme.typography.bodySmall + .merge( + color = + LocalContentColor.current + .copy(alpha = if (read) ReadItemAlpha else SecondaryItemAlpha), + ) ProvideTextStyle(value = subtitleStyle) { if (date != null) { Text( @@ -189,40 +195,43 @@ private fun getSwipeAction( downloadState: Download.State, background: Color, onSwipe: () -> Unit, -): me.saket.swipe.SwipeAction? { - return when (action) { - LibraryPreferences.ChapterSwipeAction.ToggleRead -> swipeAction( - icon = if (!read) Icons.Outlined.Done else Icons.Outlined.RemoveDone, - background = background, - isUndo = read, - onSwipe = onSwipe, - ) - LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> swipeAction( - icon = if (!bookmark) Icons.Outlined.BookmarkAdd else Icons.Outlined.BookmarkRemove, - background = background, - isUndo = bookmark, - onSwipe = onSwipe, - ) - LibraryPreferences.ChapterSwipeAction.Download -> swipeAction( - icon = when (downloadState) { - Download.State.NOT_DOWNLOADED, Download.State.ERROR -> Icons.Outlined.Download - Download.State.QUEUE, Download.State.DOWNLOADING -> Icons.Outlined.FileDownloadOff - Download.State.DOWNLOADED -> Icons.Outlined.Delete - }, - background = background, - onSwipe = onSwipe, - ) +): me.saket.swipe.SwipeAction? = + when (action) { + LibraryPreferences.ChapterSwipeAction.ToggleRead -> + swipeAction( + icon = if (!read) Icons.Outlined.Done else Icons.Outlined.RemoveDone, + background = background, + isUndo = read, + onSwipe = onSwipe, + ) + LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> + swipeAction( + icon = if (!bookmark) Icons.Outlined.BookmarkAdd else Icons.Outlined.BookmarkRemove, + background = background, + isUndo = bookmark, + onSwipe = onSwipe, + ) + LibraryPreferences.ChapterSwipeAction.Download -> + swipeAction( + icon = + when (downloadState) { + Download.State.NOT_DOWNLOADED, Download.State.ERROR -> Icons.Outlined.Download + Download.State.QUEUE, Download.State.DOWNLOADING -> Icons.Outlined.FileDownloadOff + Download.State.DOWNLOADED -> Icons.Outlined.Delete + }, + background = background, + onSwipe = onSwipe, + ) LibraryPreferences.ChapterSwipeAction.Disabled -> null } -} private fun swipeAction( onSwipe: () -> Unit, icon: ImageVector, background: Color, isUndo: Boolean = false, -): me.saket.swipe.SwipeAction { - return me.saket.swipe.SwipeAction( +): me.saket.swipe.SwipeAction = + me.saket.swipe.SwipeAction( icon = { Icon( modifier = Modifier.padding(16.dp), @@ -235,6 +244,5 @@ private fun swipeAction( onSwipe = onSwipe, isUndo = isUndo, ) -} private val swipeActionThreshold = 56.dp diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt index 4fb1cf87ba..99dfba779e 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt @@ -15,7 +15,9 @@ import coil3.compose.AsyncImage import eu.kanade.presentation.util.rememberResourceBitmapPainter import eu.kanade.tachiyomi.R -enum class MangaCover(val ratio: Float) { +enum class MangaCover( + val ratio: Float, +) { Square(1f / 1f), Book(2f / 3f), ; @@ -33,19 +35,20 @@ enum class MangaCover(val ratio: Float) { placeholder = ColorPainter(CoverPlaceholderColor), error = rememberResourceBitmapPainter(id = R.drawable.cover_error), contentDescription = contentDescription, - modifier = modifier - .aspectRatio(ratio) - .clip(shape) - .then( - if (onClick != null) { - Modifier.clickable( - role = Role.Button, - onClick = onClick, - ) - } else { - Modifier - }, - ), + modifier = + modifier + .aspectRatio(ratio) + .clip(shape) + .then( + if (onClick != null) { + Modifier.clickable( + role = Role.Button, + onClick = onClick, + ) + } else { + Modifier + }, + ), contentScale = ContentScale.Crop, ) } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt index 87bad99c23..5ff2135bfa 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt @@ -66,20 +66,22 @@ fun MangaCoverDialog( ) { Dialog( onDismissRequest = onDismissRequest, - properties = DialogProperties( - usePlatformDefaultWidth = false, - decorFitsSystemWindows = false, // Doesn't work https://issuetracker.google.com/issues/246909281 - ), + properties = + DialogProperties( + usePlatformDefaultWidth = false, + decorFitsSystemWindows = false, // Doesn't work https://issuetracker.google.com/issues/246909281 + ), ) { Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, containerColor = Color.Transparent, bottomBar = { Row( - modifier = Modifier - .fillMaxWidth() - .padding(4.dp) - .navigationBarsPadding(), + modifier = + Modifier + .fillMaxWidth() + .padding(4.dp) + .navigationBarsPadding(), ) { ActionsPill { IconButton(onClick = onDismissRequest) { @@ -92,18 +94,19 @@ fun MangaCoverDialog( Spacer(modifier = Modifier.weight(1f)) ActionsPill { AppBarActions( - actions = persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_share), - icon = Icons.Outlined.Share, - onClick = onShareClick, + actions = + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_share), + icon = Icons.Outlined.Share, + onClick = onShareClick, + ), + AppBar.Action( + title = stringResource(MR.strings.action_save), + icon = Icons.Outlined.Save, + onClick = onSaveClick, + ), ), - AppBar.Action( - title = stringResource(MR.strings.action_save), - icon = Icons.Outlined.Save, - onClick = onSaveClick, - ), - ), ) if (onEditClick != null) { Box { @@ -152,9 +155,10 @@ fun MangaCoverDialog( val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() } Box( - modifier = Modifier - .fillMaxSize() - .clickableNoIndication(onClick = onDismissRequest), + modifier = + Modifier + .fillMaxSize() + .clickableNoIndication(onClick = onDismissRequest), ) { AndroidView( factory = { @@ -165,24 +169,26 @@ fun MangaCoverDialog( } }, update = { view -> - val request = ImageRequest.Builder(view.context) - .data(coverDataProvider()) - .size(Size.ORIGINAL) - .memoryCachePolicy(CachePolicy.DISABLED) - .target { image -> - val drawable = image.asDrawable(view.context.resources) + val request = + ImageRequest + .Builder(view.context) + .data(coverDataProvider()) + .size(Size.ORIGINAL) + .memoryCachePolicy(CachePolicy.DISABLED) + .target { image -> + val drawable = image.asDrawable(view.context.resources) - // Copy bitmap in case it came from memory cache - // Because SSIV needs to thoroughly read the image - val copy = (drawable as? BitmapDrawable)?.let { - BitmapDrawable( - view.context.resources, - it.bitmap.copy(Bitmap.Config.HARDWARE, false), - ) - } ?: drawable - view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500)) - } - .build() + // Copy bitmap in case it came from memory cache + // Because SSIV needs to thoroughly read the image + val copy = + (drawable as? BitmapDrawable)?.let { + BitmapDrawable( + view.context.resources, + it.bitmap.copy(Bitmap.Config.HARDWARE, false), + ) + } ?: drawable + view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500)) + }.build() view.context.imageLoader.enqueue(request) view.updatePadding(top = statusBarPaddingPx, bottom = bottomPaddingPx) @@ -197,9 +203,10 @@ fun MangaCoverDialog( @Composable private fun ActionsPill(content: @Composable () -> Unit) { Row( - modifier = Modifier - .clip(MaterialTheme.shapes.extraLarge) - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.95f)), + modifier = + Modifier + .clip(MaterialTheme.shapes.extraLarge) + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.95f)), ) { content() } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt index a6ef3f9a56..705fce03aa 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt @@ -72,14 +72,15 @@ fun SetIntervalDialog( ) { var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) } - val nextUpdateDays = remember(nextUpdate) { - return@remember if (nextUpdate != null) { - val now = Instant.now() - now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0) - } else { - null + val nextUpdateDays = + remember(nextUpdate) { + return@remember if (nextUpdate != null) { + val now = Instant.now() + now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0) + } else { + null + } } - } AlertDialog( onDismissRequest = onDismissRequest, @@ -114,15 +115,15 @@ fun SetIntervalDialog( contentAlignment = Alignment.Center, ) { val size = DpSize(width = maxWidth / 2, height = 128.dp) - val items = (0..FetchInterval.MAX_INTERVAL) - .map { - if (it == 0) { - stringResource(MR.strings.label_default) - } else { - it.toString() - } - } - .toImmutableList() + val items = + (0..FetchInterval.MAX_INTERVAL) + .map { + if (it == 0) { + stringResource(MR.strings.label_default) + } else { + it.toString() + } + }.toImmutableList() WheelTextPicker( items = items, size = size, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index f1b660626a..e8f0477f2c 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -109,24 +109,25 @@ fun MangaInfoBox( ) { Box(modifier = modifier) { // Backdrop - val backdropGradientColors = listOf( - Color.Transparent, - MaterialTheme.colorScheme.background, - ) + val backdropGradientColors = + listOf( + Color.Transparent, + MaterialTheme.colorScheme.background, + ) AsyncImage( model = coverDataProvider(), contentDescription = null, contentScale = ContentScale.Crop, - modifier = Modifier - .matchParentSize() - .drawWithContent { - drawContent() - drawRect( - brush = Brush.verticalGradient(colors = backdropGradientColors), - ) - } - .blur(4.dp) - .alpha(0.2f), + modifier = + Modifier + .matchParentSize() + .drawWithContent { + drawContent() + drawRect( + brush = Brush.verticalGradient(colors = backdropGradientColors), + ) + }.blur(4.dp) + .alpha(0.2f), ) // Manga & source info @@ -179,47 +180,52 @@ fun MangaActionRow( val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) // TODO: show something better when using custom interval - val nextUpdateDays = remember(nextUpdate) { - return@remember if (nextUpdate != null) { - val now = Instant.now() - now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0) - } else { - null + val nextUpdateDays = + remember(nextUpdate) { + return@remember if (nextUpdate != null) { + val now = Instant.now() + now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0) + } else { + null + } } - } Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) { MangaActionButton( - title = if (favorite) { - stringResource(MR.strings.in_library) - } else { - stringResource(MR.strings.add_to_library) - }, + title = + if (favorite) { + stringResource(MR.strings.in_library) + } else { + stringResource(MR.strings.add_to_library) + }, icon = if (favorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor, onClick = onAddToLibraryClicked, onLongClick = onEditCategory, ) MangaActionButton( - title = when (nextUpdateDays) { - null -> stringResource(MR.strings.not_applicable) - 0 -> stringResource(MR.strings.manga_interval_expected_update_soon) - else -> pluralStringResource( - MR.plurals.day, - count = nextUpdateDays, - nextUpdateDays, - ) - }, + title = + when (nextUpdateDays) { + null -> stringResource(MR.strings.not_applicable) + 0 -> stringResource(MR.strings.manga_interval_expected_update_soon) + else -> + pluralStringResource( + MR.plurals.day, + count = nextUpdateDays, + nextUpdateDays, + ) + }, icon = Icons.Default.HourglassEmpty, color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor, onClick = { onEditIntervalClicked?.invoke() }, ) MangaActionButton( - title = if (trackingCount == 0) { - stringResource(MR.strings.manga_tracking_tab) - } else { - pluralStringResource(MR.plurals.num_trackers, count = trackingCount, trackingCount) - }, + title = + if (trackingCount == 0) { + stringResource(MR.strings.manga_tracking_tab) + } else { + pluralStringResource(MR.plurals.num_trackers, count = trackingCount, trackingCount) + }, icon = if (trackingCount == 0) Icons.Outlined.Sync else Icons.Outlined.Done, color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary, onClick = onTrackingClicked, @@ -246,32 +252,36 @@ fun ExpandableMangaDescription( modifier: Modifier = Modifier, ) { Column(modifier = modifier) { - val (expanded, onExpanded) = rememberSaveable { - mutableStateOf(defaultExpandState) - } + val (expanded, onExpanded) = + rememberSaveable { + mutableStateOf(defaultExpandState) + } val desc = description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder) - val trimmedDescription = remember(desc) { - desc - .replace(whitespaceLineRegex, "\n") - .trimEnd() - } + val trimmedDescription = + remember(desc) { + desc + .replace(whitespaceLineRegex, "\n") + .trimEnd() + } MangaSummary( expandedDescription = desc, shrunkDescription = trimmedDescription, expanded = expanded, - modifier = Modifier - .padding(top = 8.dp) - .padding(horizontal = 16.dp) - .clickableNoIndication { onExpanded(!expanded) }, + modifier = + Modifier + .padding(top = 8.dp) + .padding(horizontal = 16.dp) + .clickableNoIndication { onExpanded(!expanded) }, ) val tags = tagsProvider() if (!tags.isNullOrEmpty()) { Box( - modifier = Modifier - .padding(top = 8.dp) - .padding(vertical = 12.dp) - .animateContentSize(), + modifier = + Modifier + .padding(top = 8.dp) + .padding(vertical = 12.dp) + .animateContentSize(), ) { var showMenu by remember { mutableStateOf(false) } var tagSelected by remember { mutableStateOf("") } @@ -346,9 +356,10 @@ private fun MangaAndSourceTitlesLarge( isStubSource: Boolean, ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { MangaCover.Book( @@ -385,16 +396,18 @@ private fun MangaAndSourceTitlesSmall( isStubSource: Boolean, ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { MangaCover.Book( - modifier = Modifier - .sizeIn(maxWidth = 100.dp) - .align(Alignment.Top), + modifier = + Modifier + .sizeIn(maxWidth = 100.dp) + .align(Alignment.Top), data = coverDataProvider(), contentDescription = stringResource(MR.strings.manga_cover), onClick = onCoverClick, @@ -430,17 +443,18 @@ private fun ColumnScope.MangaContentInfo( Text( text = title.ifBlank { stringResource(MR.strings.unknown_title) }, style = MaterialTheme.typography.titleLarge, - modifier = Modifier.clickableNoIndication( - onLongClick = { - if (title.isNotBlank()) { - context.copyToClipboard( - title, - title, - ) - } - }, - onClick = { if (title.isNotBlank()) doSearch(title, true) }, - ), + modifier = + Modifier.clickableNoIndication( + onLongClick = { + if (title.isNotBlank()) { + context.copyToClipboard( + title, + title, + ) + } + }, + onClick = { if (title.isNotBlank()) doSearch(title, true) }, + ), textAlign = textAlign, ) @@ -457,21 +471,23 @@ private fun ColumnScope.MangaContentInfo( modifier = Modifier.size(16.dp), ) Text( - text = author?.takeIf { it.isNotBlank() } - ?: stringResource(MR.strings.unknown_author), + text = + author?.takeIf { it.isNotBlank() } + ?: stringResource(MR.strings.unknown_author), style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .clickableNoIndication( - onLongClick = { - if (!author.isNullOrBlank()) { - context.copyToClipboard( - author, - author, - ) - } - }, - onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, - ), + modifier = + Modifier + .clickableNoIndication( + onLongClick = { + if (!author.isNullOrBlank()) { + context.copyToClipboard( + author, + author, + ) + } + }, + onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, + ), textAlign = textAlign, ) } @@ -490,11 +506,12 @@ private fun ColumnScope.MangaContentInfo( Text( text = artist, style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .clickableNoIndication( - onLongClick = { context.copyToClipboard(artist, artist) }, - onClick = { doSearch(artist, true) }, - ), + modifier = + Modifier + .clickableNoIndication( + onLongClick = { context.copyToClipboard(artist, artist) }, + onClick = { doSearch(artist, true) }, + ), textAlign = textAlign, ) } @@ -507,31 +524,34 @@ private fun ColumnScope.MangaContentInfo( verticalAlignment = Alignment.CenterVertically, ) { Icon( - imageVector = when (status) { - SManga.ONGOING.toLong() -> Icons.Outlined.Schedule - SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll - SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney - SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done - SManga.CANCELLED.toLong() -> Icons.Outlined.Close - SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause - else -> Icons.Outlined.Block - }, + imageVector = + when (status) { + SManga.ONGOING.toLong() -> Icons.Outlined.Schedule + SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll + SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney + SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done + SManga.CANCELLED.toLong() -> Icons.Outlined.Close + SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause + else -> Icons.Outlined.Block + }, contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), + modifier = + Modifier + .padding(end = 4.dp) + .size(16.dp), ) ProvideTextStyle(MaterialTheme.typography.bodyMedium) { Text( - text = when (status) { - SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing) - SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed) - SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed) - SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished) - SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled) - SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus) - else -> stringResource(MR.strings.unknown) - }, + text = + when (status) { + SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing) + SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed) + SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed) + SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished) + SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled) + SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus) + else -> stringResource(MR.strings.unknown) + }, overflow = TextOverflow.Ellipsis, maxLines = 1, ) @@ -540,20 +560,22 @@ private fun ColumnScope.MangaContentInfo( Icon( imageVector = Icons.Filled.Warning, contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), + modifier = + Modifier + .padding(end = 4.dp) + .size(16.dp), tint = MaterialTheme.colorScheme.error, ) } Text( text = sourceName, - modifier = Modifier.clickableNoIndication { - doSearch( - sourceName, - false, - ) - }, + modifier = + Modifier.clickableNoIndication { + doSearch( + sourceName, + false, + ) + }, overflow = TextOverflow.Ellipsis, maxLines = 1, ) @@ -574,62 +596,72 @@ private fun MangaSummary( ) Layout( modifier = modifier.clipToBounds(), - contents = listOf( - { - Text( - text = "\n\n", // Shows at least 3 lines - style = MaterialTheme.typography.bodyMedium, - ) - }, - { - Text( - text = expandedDescription, - style = MaterialTheme.typography.bodyMedium, - ) - }, - { - SelectionContainer { + contents = + listOf( + { Text( - text = if (expanded) expandedDescription else shrunkDescription, - maxLines = Int.MAX_VALUE, + text = "\n\n", // Shows at least 3 lines style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.secondaryItemAlpha(), ) - } - }, - { - val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background) - Box( - modifier = Modifier.background(Brush.verticalGradient(colors = colors)), - contentAlignment = Alignment.Center, - ) { - val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down) - Icon( - painter = rememberAnimatedVectorPainter(image, !expanded), - contentDescription = stringResource( - if (expanded) MR.strings.manga_info_collapse else MR.strings.manga_info_expand, - ), - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())), + }, + { + Text( + text = expandedDescription, + style = MaterialTheme.typography.bodyMedium, ) - } - }, - ), + }, + { + SelectionContainer { + Text( + text = if (expanded) expandedDescription else shrunkDescription, + maxLines = Int.MAX_VALUE, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.secondaryItemAlpha(), + ) + } + }, + { + val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background) + Box( + modifier = Modifier.background(Brush.verticalGradient(colors = colors)), + contentAlignment = Alignment.Center, + ) { + val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down) + Icon( + painter = rememberAnimatedVectorPainter(image, !expanded), + contentDescription = + stringResource( + if (expanded) MR.strings.manga_info_collapse else MR.strings.manga_info_expand, + ), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())), + ) + } + }, + ), ) { (shrunk, expanded, actual, scrim), constraints -> - val shrunkHeight = shrunk.single() - .measure(constraints) - .height - val expandedHeight = expanded.single() - .measure(constraints) - .height + val shrunkHeight = + shrunk + .single() + .measure(constraints) + .height + val expandedHeight = + expanded + .single() + .measure(constraints) + .height val heightDelta = expandedHeight - shrunkHeight val scrimHeight = 24.dp.roundToPx() - val actualPlaceable = actual.single() - .measure(constraints) - val scrimPlaceable = scrim.single() - .measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight)) + val actualPlaceable = + actual + .single() + .measure(constraints) + val scrimPlaceable = + scrim + .single() + .measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight)) val currentHeight = shrunkHeight + ((heightDelta + scrimHeight) * animProgress).roundToInt() layout(constraints.maxWidth, currentHeight) { diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt index 4415bbf278..df8056e59a 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt @@ -44,12 +44,10 @@ fun MangaToolbar( onClickEditCategory: (() -> Unit)?, onClickRefresh: () -> Unit, onClickMigrate: (() -> Unit)?, - // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, onInvertSelection: () -> Unit, - modifier: Modifier = Modifier, backgroundAlphaProvider: () -> Float = titleAlphaProvider, ) { @@ -100,65 +98,68 @@ fun MangaToolbar( val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current AppBarActions( - actions = persistentListOf().builder() - .apply { - if (onClickDownload != null) { + actions = + persistentListOf() + .builder() + .apply { + if (onClickDownload != null) { + add( + AppBar.Action( + title = stringResource(MR.strings.manga_download), + icon = Icons.Outlined.Download, + onClick = { downloadExpanded = !downloadExpanded }, + ), + ) + } add( AppBar.Action( - title = stringResource(MR.strings.manga_download), - icon = Icons.Outlined.Download, - onClick = { downloadExpanded = !downloadExpanded }, - ), - ) - } - add( - AppBar.Action( - title = stringResource(MR.strings.action_filter), - icon = Icons.Outlined.FilterList, - iconTint = filterTint, - onClick = onClickFilter, - ), - ) - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_webview_refresh), - onClick = onClickRefresh, - ), - ) - if (onClickEditCategory != null) { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_edit_categories), - onClick = onClickEditCategory, - ), - ) - } - if (onClickMigrate != null) { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_migrate), - onClick = onClickMigrate, + title = stringResource(MR.strings.action_filter), + icon = Icons.Outlined.FilterList, + iconTint = filterTint, + onClick = onClickFilter, ), ) - } - if (onClickShare != null) { add( AppBar.OverflowAction( - title = stringResource(MR.strings.action_share), - onClick = onClickShare, + title = stringResource(MR.strings.action_webview_refresh), + onClick = onClickRefresh, ), ) - } - } - .build(), + if (onClickEditCategory != null) { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_edit_categories), + onClick = onClickEditCategory, + ), + ) + } + if (onClickMigrate != null) { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_migrate), + onClick = onClickMigrate, + ), + ) + } + if (onClickShare != null) { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_share), + onClick = onClickShare, + ), + ) + } + }.build(), ) } }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme - .surfaceColorAtElevation(3.dp) - .copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()), - ), + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = + MaterialTheme.colorScheme + .surfaceColorAtElevation(3.dp) + .copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()), + ), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MissingChapterCountListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MissingChapterCountListItem.kt index baca38c34c..64871b4b9a 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MissingChapterCountListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MissingChapterCountListItem.kt @@ -23,12 +23,12 @@ fun MissingChapterCountListItem( modifier: Modifier = Modifier, ) { Row( - modifier = modifier - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ) - .secondaryItemAlpha(), + modifier = + modifier + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ).secondaryItemAlpha(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), ) { diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt index 747ac09e0f..aaaba777e7 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt @@ -39,9 +39,10 @@ fun ScanlatorFilterDialog( onDismissRequest: () -> Unit, onConfirm: (Set) -> Unit, ) { - val sortedAvailableScanlators = remember(availableScanlators) { - availableScanlators.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it }) - } + val sortedAvailableScanlators = + remember(availableScanlators) { + availableScanlators.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it }) + } val mutableExcludedScanlators = remember(excludedScanlators) { excludedScanlators.toMutableStateList() } AlertDialog( onDismissRequest = onDismissRequest, @@ -59,30 +60,32 @@ fun ScanlatorFilterDialog( val isExcluded = mutableExcludedScanlators.contains(scanlator) Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { - if (isExcluded) { - mutableExcludedScanlators.remove(scanlator) - } else { - mutableExcludedScanlators.add(scanlator) - } - } - .minimumInteractiveComponentSize() - .clip(MaterialTheme.shapes.small) - .fillMaxWidth() - .padding(horizontal = MaterialTheme.padding.small), + modifier = + Modifier + .clickable { + if (isExcluded) { + mutableExcludedScanlators.remove(scanlator) + } else { + mutableExcludedScanlators.add(scanlator) + } + }.minimumInteractiveComponentSize() + .clip(MaterialTheme.shapes.small) + .fillMaxWidth() + .padding(horizontal = MaterialTheme.padding.small), ) { Icon( - imageVector = if (isExcluded) { - Icons.Rounded.DisabledByDefault - } else { - Icons.Rounded.CheckBoxOutlineBlank - }, - tint = if (isExcluded) { - MaterialTheme.colorScheme.primary - } else { - LocalContentColor.current - }, + imageVector = + if (isExcluded) { + Icons.Rounded.DisabledByDefault + } else { + Icons.Rounded.CheckBoxOutlineBlank + }, + tint = + if (isExcluded) { + MaterialTheme.colorScheme.primary + } else { + LocalContentColor.current + }, contentDescription = null, ) Text( @@ -98,9 +101,10 @@ fun ScanlatorFilterDialog( if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } }, - properties = DialogProperties( - usePlatformDefaultWidth = true, - ), + properties = + DialogProperties( + usePlatformDefaultWidth = true, + ), confirmButton = { if (sortedAvailableScanlators.isEmpty()) { TextButton(onClick = onDismissRequest) { diff --git a/app/src/main/java/eu/kanade/presentation/more/LogoHeader.kt b/app/src/main/java/eu/kanade/presentation/more/LogoHeader.kt index 881d96b648..99c3644d23 100644 --- a/app/src/main/java/eu/kanade/presentation/more/LogoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/more/LogoHeader.kt @@ -24,9 +24,10 @@ fun LogoHeader() { painter = painterResource(R.drawable.ic_mihon), contentDescription = null, tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier - .padding(vertical = 56.dp) - .size(64.dp), + modifier = + Modifier + .padding(vertical = 56.dp) + .size(64.dp), ) HorizontalDivider() diff --git a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt index fc690139ac..cb4154c800 100644 --- a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt @@ -53,9 +53,10 @@ fun MoreScreen( Scaffold( topBar = { Column( - modifier = Modifier.windowInsetsPadding( - WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - ), + modifier = + Modifier.windowInsetsPadding( + WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + ), ) { if (isFDroid) { // Don't really care about slow updaters now @@ -94,27 +95,28 @@ fun MoreScreen( val downloadQueueState = downloadQueueStateProvider() TextPreferenceWidget( title = stringResource(MR.strings.label_download_queue), - subtitle = when (downloadQueueState) { - DownloadQueueState.Stopped -> null - is DownloadQueueState.Paused -> { - val pending = downloadQueueState.pending - if (pending == 0) { - stringResource(MR.strings.paused) - } else { - "${stringResource(MR.strings.paused)} • ${ - pluralStringResource( - MR.plurals.download_queue_summary, - count = pending, - pending, - ) - }" + subtitle = + when (downloadQueueState) { + DownloadQueueState.Stopped -> null + is DownloadQueueState.Paused -> { + val pending = downloadQueueState.pending + if (pending == 0) { + stringResource(MR.strings.paused) + } else { + "${stringResource(MR.strings.paused)} • ${ + pluralStringResource( + MR.plurals.download_queue_summary, + count = pending, + pending, + ) + }" + } } - } - is DownloadQueueState.Downloading -> { - val pending = downloadQueueState.pending - pluralStringResource(MR.plurals.download_queue_summary, count = pending, pending) - } - }, + is DownloadQueueState.Downloading -> { + val pending = downloadQueueState.pending + pluralStringResource(MR.plurals.download_queue_summary, count = pending, pending) + } + }, icon = Icons.Outlined.GetApp, onPreferenceClick = onClickDownloadQueue, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt index 3d561b0c36..a1a60ce45b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt @@ -43,14 +43,17 @@ fun NewUpdateScreen( onRejectClick = onRejectUpdate, ) { RichText( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = MaterialTheme.padding.large), - style = RichTextStyle( - stringStyle = RichTextStringStyle( - linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = MaterialTheme.padding.large), + style = + RichTextStyle( + stringStyle = + RichTextStringStyle( + linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), + ), ), - ), ) { Markdown(content = changelogInfo) @@ -72,14 +75,15 @@ private fun NewUpdateScreenPreview() { TachiyomiPreviewTheme { NewUpdateScreen( versionName = "v0.99.9", - changelogInfo = """ + changelogInfo = + """ ## Yay Foobar ### More info - Hello - World - """.trimIndent(), + """.trimIndent(), onOpenInBrowser = {}, onRejectUpdate = {}, onAcceptUpdate = {}, diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt index 0af77922a9..7a01ce18ef 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt @@ -21,7 +21,6 @@ import tachiyomi.presentation.core.i18n.stringResource internal class GuidesStep( private val onRestoreBackup: () -> Unit, ) : OnboardingStep { - override val isComplete: Boolean = true @Composable diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt index c5fd8c2faf..d74bd4414b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt @@ -32,14 +32,15 @@ fun OnboardingScreen( val slideDistance = rememberSlideDistance() var currentStep by rememberSaveable { mutableIntStateOf(0) } - val steps = remember { - listOf( - ThemeStep(), - StorageStep(), - PermissionStep(), - GuidesStep(onRestoreBackup = onRestoreBackup), - ) - } + val steps = + remember { + listOf( + ThemeStep(), + StorageStep(), + PermissionStep(), + GuidesStep(onRestoreBackup = onRestoreBackup), + ) + } val isLastStep = currentStep == steps.lastIndex BackHandler(enabled = currentStep != 0, onBack = { currentStep-- }) @@ -48,13 +49,14 @@ fun OnboardingScreen( icon = Icons.Outlined.RocketLaunch, headingText = stringResource(MR.strings.onboarding_heading), subtitleText = stringResource(MR.strings.onboarding_description), - acceptText = stringResource( - if (isLastStep) { - MR.strings.onboarding_action_finish - } else { - MR.strings.onboarding_action_next - }, - ), + acceptText = + stringResource( + if (isLastStep) { + MR.strings.onboarding_action_finish + } else { + MR.strings.onboarding_action_next + }, + ), canAccept = steps[currentStep].isComplete, onAcceptClick = { if (isLastStep) { @@ -65,11 +67,12 @@ fun OnboardingScreen( }, ) { Box( - modifier = Modifier - .padding(vertical = MaterialTheme.padding.small) - .clip(MaterialTheme.shapes.small) - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceVariant), + modifier = + Modifier + .padding(vertical = MaterialTheme.padding.small) + .clip(MaterialTheme.shapes.small) + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceVariant), ) { AnimatedContent( targetState = currentStep, diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingStep.kt index 81b0a9f915..f1d75a6e4e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingStep.kt @@ -3,7 +3,6 @@ package eu.kanade.presentation.more.onboarding import androidx.compose.runtime.Composable internal interface OnboardingStep { - val isComplete: Boolean @Composable diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt index 8b3d9c07bc..7a5a0c7ce4 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt @@ -40,7 +40,6 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.secondaryItemAlpha internal class PermissionStep : OnboardingStep { - private var notificationGranted by mutableStateOf(false) private var batteryGranted by mutableStateOf(false) @@ -54,18 +53,22 @@ internal class PermissionStep : OnboardingStep { val installGranted = rememberRequestPackageInstallsPermissionState() DisposableEffect(lifecycleOwner.lifecycle) { - val observer = object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == - PackageManager.PERMISSION_GRANTED - } else { - true + val observer = + object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + notificationGranted = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED + } else { + true + } + batteryGranted = + context + .getSystemService()!! + .isIgnoringBatteryOptimizations(context.packageName) } - batteryGranted = context.getSystemService()!! - .isIgnoringBatteryOptimizations(context.packageName) } - } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) @@ -83,12 +86,13 @@ internal class PermissionStep : OnboardingStep { ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val permissionRequester = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - onResult = { - // no-op. resulting checks is being done on resume - }, - ) + val permissionRequester = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { + // no-op. resulting checks is being done on resume + }, + ) PermissionItem( title = stringResource(MR.strings.onboarding_permission_notifications), subtitle = stringResource(MR.strings.onboarding_permission_notifications_description), @@ -103,9 +107,10 @@ internal class PermissionStep : OnboardingStep { granted = batteryGranted, onButtonClick = { @SuppressLint("BatteryLife") - val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { - data = Uri.parse("package:${context.packageName}") - } + val intent = + Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${context.packageName}") + } context.startActivity(intent) }, ) @@ -120,9 +125,10 @@ internal class PermissionStep : OnboardingStep { Text( text = text, style = MaterialTheme.typography.titleLarge, - modifier = modifier - .padding(horizontal = 16.dp) - .secondaryItemAlpha(), + modifier = + modifier + .padding(horizontal = 16.dp) + .secondaryItemAlpha(), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt index 4b64097462..8e070120f3 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt @@ -29,7 +29,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get internal class StorageStep : OnboardingStep { - private val storagePref = Injekt.get().baseStorageDirectory() private var _isComplete by mutableStateOf(false) @@ -84,7 +83,8 @@ internal class StorageStep : OnboardingStep { } LaunchedEffect(Unit) { - storagePref.changes() + storagePref + .changes() .collectLatest { _isComplete = storagePref.isSet() } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/ThemeStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/ThemeStep.kt index dfd7517dcc..73458aa900 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/ThemeStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/ThemeStep.kt @@ -12,7 +12,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get internal class ThemeStep : OnboardingStep { - override val isComplete: Boolean = true private val uiPreferences: UiPreferences = Injekt.get() diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt index d2ed81c580..fc65abbdb5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt @@ -28,7 +28,6 @@ sealed class Preference { override val icon: ImageVector? = null, override val enabled: Boolean = true, override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, - val onClick: (() -> Unit)? = null, ) : PreferenceItem() @@ -71,15 +70,17 @@ sealed class Preference { override val icon: ImageVector? = null, override val enabled: Boolean = true, override val onValueChanged: suspend (newValue: T) -> Boolean = { true }, - val entries: ImmutableMap, ) : PreferenceItem() { internal fun internalSet(newValue: Any) = pref.set(newValue as T) + internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T) @Composable - internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap) = - subtitleProvider(value as T, entries as ImmutableMap) + internal fun internalSubtitleProvider( + value: Any?, + entries: ImmutableMap, + ) = subtitleProvider(value as T, entries as ImmutableMap) } /** @@ -94,7 +95,6 @@ sealed class Preference { override val icon: ImageVector? = null, override val enabled: Boolean = true, override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, - val entries: ImmutableMap, ) : PreferenceItem() @@ -110,17 +110,18 @@ sealed class Preference { value: Set, entries: ImmutableMap, ) -> String? = { v, e -> - val combined = remember(v) { - v.map { e[it] } - .takeIf { it.isNotEmpty() } - ?.joinToString() - } ?: stringResource(MR.strings.none) + val combined = + remember(v) { + v + .map { e[it] } + .takeIf { it.isNotEmpty() } + ?.joinToString() + } ?: stringResource(MR.strings.none) subtitle?.format(combined) }, override val icon: ImageVector? = null, override val enabled: Boolean = true, override val onValueChanged: suspend (newValue: Set) -> Boolean = { true }, - val entries: ImmutableMap, ) : PreferenceItem>() @@ -174,7 +175,6 @@ sealed class Preference { data class PreferenceGroup( override val title: String, override val enabled: Boolean = true, - val preferenceItems: ImmutableList>, ) : Preference() } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt index b22e69323a..8f9fa07eb0 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt @@ -156,7 +156,8 @@ internal fun PreferenceItem( ) } is Preference.PreferenceItem.TrackerPreference -> { - val uName by Injekt.get() + val uName by Injekt + .get() .trackUsername(item.tracker) .collectAsState() item.tracker.run { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScreen.kt index 26b4086b86..5aac260315 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScreen.kt @@ -71,19 +71,20 @@ fun PreferenceScreen( } // Create Preference Item - is Preference.PreferenceItem<*> -> item { - PreferenceItem( - item = preference, - highlightKey = highlightKey, - ) - } + is Preference.PreferenceItem<*> -> + item { + PreferenceItem( + item = preference, + highlightKey = highlightKey, + ) + } } } } } -private fun List.findHighlightedIndex(highlightKey: String): Int { - return flatMap { +private fun List.findHighlightedIndex(highlightKey: String): Int = + flatMap { if (it is Preference.PreferenceGroup) { buildList { add(null) // Header @@ -94,4 +95,3 @@ private fun List.findHighlightedIndex(highlightKey: String): Int { listOf(it.title) } }.indexOfFirst { it == highlightKey } -} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt index 4c44abce3d..d7e23e4b3c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt @@ -20,28 +20,32 @@ fun getCategoriesLabel( ): String { val context = LocalContext.current - val includedCategories = included - .mapNotNull { id -> allCategories.find { it.id == id.toLong() } } - .sortedBy { it.order } - val excludedCategories = excluded - .mapNotNull { id -> allCategories.find { it.id == id.toLong() } } - .sortedBy { it.order } + val includedCategories = + included + .mapNotNull { id -> allCategories.find { it.id == id.toLong() } } + .sortedBy { it.order } + val excludedCategories = + excluded + .mapNotNull { id -> allCategories.find { it.id == id.toLong() } } + .sortedBy { it.order } val allExcluded = excludedCategories.size == allCategories.size - val includedItemsText = when { - // Some selected, but not all - includedCategories.isNotEmpty() && includedCategories.size != allCategories.size -> - includedCategories.joinToString { it.visualName(context) } - // All explicitly selected - includedCategories.size == allCategories.size -> stringResource(MR.strings.all) - allExcluded -> stringResource(MR.strings.none) - else -> stringResource(MR.strings.all) - } - val excludedItemsText = when { - excludedCategories.isEmpty() -> stringResource(MR.strings.none) - allExcluded -> stringResource(MR.strings.all) - else -> excludedCategories.joinToString { it.visualName(context) } - } + val includedItemsText = + when { + // Some selected, but not all + includedCategories.isNotEmpty() && includedCategories.size != allCategories.size -> + includedCategories.joinToString { it.visualName(context) } + // All explicitly selected + includedCategories.size == allCategories.size -> stringResource(MR.strings.all) + allExcluded -> stringResource(MR.strings.none) + else -> stringResource(MR.strings.all) + } + val excludedItemsText = + when { + excludedCategories.isEmpty() -> stringResource(MR.strings.none) + allExcluded -> stringResource(MR.strings.all) + else -> excludedCategories.joinToString { it.visualName(context) } + } return stringResource(MR.strings.include, includedItemsText) + "\n" + stringResource(MR.strings.exclude, excludedItemsText) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SearchableSettings.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SearchableSettings.kt index 5652ace76c..4ac426a97c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SearchableSettings.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SearchableSettings.kt @@ -10,7 +10,6 @@ import eu.kanade.presentation.more.settings.PreferenceScaffold import eu.kanade.presentation.util.LocalBackPress interface SearchableSettings : Screen { - @Composable @ReadOnlyComposable fun getTitleRes(): StringResource 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 646c8c0425..80a56bb622 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 @@ -71,7 +71,6 @@ import uy.kohesive.injekt.api.get import java.io.File object SettingsAdvancedScreen : SearchableSettings { - @ReadOnlyComposable @Composable override fun getTitleRes() = MR.strings.pref_category_advanced @@ -115,9 +114,10 @@ object SettingsAdvancedScreen : SearchableSettings { Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.pref_manage_notifications), onClick = { - val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { - putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) - } + val intent = + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } context.startActivity(intent) }, ), @@ -137,35 +137,37 @@ object SettingsAdvancedScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.label_background_activity), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_disable_battery_optimization), - subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary), - onClick = { - val packageName: String = context.packageName - if (!context.powerManager.isIgnoringBatteryOptimizations(packageName)) { - try { - @SuppressLint("BatteryLife") - val intent = Intent().apply { - action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - data = "package:$packageName".toUri() - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + preferenceItems = + persistentListOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_disable_battery_optimization), + subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary), + onClick = { + val packageName: String = context.packageName + if (!context.powerManager.isIgnoringBatteryOptimizations(packageName)) { + try { + @SuppressLint("BatteryLife") + val intent = + Intent().apply { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = "package:$packageName".toUri() + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + context.toast(MR.strings.battery_optimization_setting_activity_not_found) } - context.startActivity(intent) - } catch (e: ActivityNotFoundException) { - context.toast(MR.strings.battery_optimization_setting_activity_not_found) + } else { + context.toast(MR.strings.battery_optimization_disabled) } - } else { - context.toast(MR.strings.battery_optimization_disabled) - } - }, - ), - Preference.PreferenceItem.TextPreference( - title = "Don't kill my app!", - subtitle = stringResource(MR.strings.about_dont_kill_my_app), - onClick = { uriHandler.openUri("https://dontkillmyapp.com/") }, + }, + ), + Preference.PreferenceItem.TextPreference( + title = "Don't kill my app!", + subtitle = stringResource(MR.strings.about_dont_kill_my_app), + onClick = { uriHandler.openUri("https://dontkillmyapp.com/") }, + ), ), - ), ) } @@ -176,28 +178,27 @@ object SettingsAdvancedScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.label_data), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_invalidate_download_cache), - subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary), - onClick = { - Injekt.get().invalidateCache() - context.toast(MR.strings.download_cache_invalidated) - }, - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_clear_database), - subtitle = stringResource(MR.strings.pref_clear_database_summary), - onClick = { navigator.push(ClearDatabaseScreen()) }, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_invalidate_download_cache), + subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary), + onClick = { + Injekt.get().invalidateCache() + context.toast(MR.strings.download_cache_invalidated) + }, + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_clear_database), + subtitle = stringResource(MR.strings.pref_clear_database_summary), + onClick = { navigator.push(ClearDatabaseScreen()) }, + ), ), - ), ) } @Composable - private fun getNetworkGroup( - networkPreferences: NetworkPreferences, - ): Preference.PreferenceGroup { + private fun getNetworkGroup(networkPreferences: NetworkPreferences): Preference.PreferenceGroup { val context = LocalContext.current val networkHelper = remember { Injekt.get() } @@ -206,80 +207,82 @@ object SettingsAdvancedScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.label_network), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_clear_cookies), - onClick = { - networkHelper.cookieJar.removeAll() - context.toast(MR.strings.cookies_cleared) - }, - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_clear_webview_data), - onClick = { - try { - WebView(context).run { - setDefaultSettings() - clearCache(true) - clearFormData() - clearHistory() - clearSslPreferences() + preferenceItems = + persistentListOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_clear_cookies), + onClick = { + networkHelper.cookieJar.removeAll() + context.toast(MR.strings.cookies_cleared) + }, + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_clear_webview_data), + onClick = { + try { + WebView(context).run { + setDefaultSettings() + clearCache(true) + clearFormData() + clearHistory() + clearSslPreferences() + } + WebStorage.getInstance().deleteAllData() + context.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() } + context.toast(MR.strings.webview_data_deleted) + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) + context.toast(MR.strings.cache_delete_error) } - WebStorage.getInstance().deleteAllData() - context.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() } - context.toast(MR.strings.webview_data_deleted) - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) - context.toast(MR.strings.cache_delete_error) - } - }, - ), - Preference.PreferenceItem.ListPreference( - pref = networkPreferences.dohProvider(), - title = stringResource(MR.strings.pref_dns_over_https), - entries = persistentMapOf( - -1 to stringResource(MR.strings.disabled), - PREF_DOH_CLOUDFLARE to "Cloudflare", - PREF_DOH_GOOGLE to "Google", - PREF_DOH_ADGUARD to "AdGuard", - PREF_DOH_QUAD9 to "Quad9", - PREF_DOH_ALIDNS to "AliDNS", - PREF_DOH_DNSPOD to "DNSPod", - PREF_DOH_360 to "360", - PREF_DOH_QUAD101 to "Quad 101", - PREF_DOH_MULLVAD to "Mullvad", - PREF_DOH_CONTROLD to "Control D", - PREF_DOH_NJALLA to "Njalla", - PREF_DOH_SHECAN to "Shecan", + }, + ), + Preference.PreferenceItem.ListPreference( + pref = networkPreferences.dohProvider(), + title = stringResource(MR.strings.pref_dns_over_https), + entries = + persistentMapOf( + -1 to stringResource(MR.strings.disabled), + PREF_DOH_CLOUDFLARE to "Cloudflare", + PREF_DOH_GOOGLE to "Google", + PREF_DOH_ADGUARD to "AdGuard", + PREF_DOH_QUAD9 to "Quad9", + PREF_DOH_ALIDNS to "AliDNS", + PREF_DOH_DNSPOD to "DNSPod", + PREF_DOH_360 to "360", + PREF_DOH_QUAD101 to "Quad 101", + PREF_DOH_MULLVAD to "Mullvad", + PREF_DOH_CONTROLD to "Control D", + PREF_DOH_NJALLA to "Njalla", + PREF_DOH_SHECAN to "Shecan", + ), + onValueChanged = { + context.toast(MR.strings.requires_app_restart) + true + }, + ), + Preference.PreferenceItem.EditTextPreference( + pref = userAgentPref, + title = stringResource(MR.strings.pref_user_agent_string), + onValueChanged = { + try { + // OkHttp checks for valid values internally + Headers.Builder().add("User-Agent", it) + } catch (_: IllegalArgumentException) { + context.toast(MR.strings.error_user_agent_string_invalid) + return@EditTextPreference false + } + true + }, + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_reset_user_agent_string), + enabled = remember(userAgent) { userAgent != userAgentPref.defaultValue() }, + onClick = { + userAgentPref.delete() + context.toast(MR.strings.requires_app_restart) + }, ), - onValueChanged = { - context.toast(MR.strings.requires_app_restart) - true - }, - ), - Preference.PreferenceItem.EditTextPreference( - pref = userAgentPref, - title = stringResource(MR.strings.pref_user_agent_string), - onValueChanged = { - try { - // OkHttp checks for valid values internally - Headers.Builder().add("User-Agent", it) - } catch (_: IllegalArgumentException) { - context.toast(MR.strings.error_user_agent_string_invalid) - return@EditTextPreference false - } - true - }, - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_reset_user_agent_string), - enabled = remember(userAgent) { userAgent != userAgentPref.defaultValue() }, - onClick = { - userAgentPref.delete() - context.toast(MR.strings.requires_app_restart) - }, ), - ), ) } @@ -290,64 +293,64 @@ object SettingsAdvancedScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.label_library), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_refresh_library_covers), - onClick = { MetadataUpdateJob.startNow(context) }, - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_reset_viewer_flags), - subtitle = stringResource(MR.strings.pref_reset_viewer_flags_summary), - onClick = { - scope.launchNonCancellable { - val success = Injekt.get().await() - withUIContext { - val message = if (success) { - MR.strings.pref_reset_viewer_flags_success - } else { - MR.strings.pref_reset_viewer_flags_error + preferenceItems = + persistentListOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_refresh_library_covers), + onClick = { MetadataUpdateJob.startNow(context) }, + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_reset_viewer_flags), + subtitle = stringResource(MR.strings.pref_reset_viewer_flags_summary), + onClick = { + scope.launchNonCancellable { + val success = Injekt.get().await() + withUIContext { + val message = + if (success) { + MR.strings.pref_reset_viewer_flags_success + } else { + MR.strings.pref_reset_viewer_flags_error + } + context.toast(message) } - context.toast(message) } - } - }, + }, + ), ), - ), ) } @Composable - private fun getReaderGroup( - basePreferences: BasePreferences, - ): Preference.PreferenceGroup { + private fun getReaderGroup(basePreferences: BasePreferences): Preference.PreferenceGroup { val context = LocalContext.current - val chooseColorProfile = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocument(), - ) { uri -> - uri?.let { - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - context.contentResolver.takePersistableUriPermission(uri, flags) - basePreferences.displayProfile().set(uri.toString()) + val chooseColorProfile = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + ) { uri -> + uri?.let { + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + context.contentResolver.takePersistableUriPermission(uri, flags) + basePreferences.displayProfile().set(uri.toString()) + } } - } return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_category_reader), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_display_profile), - subtitle = basePreferences.displayProfile().get(), - onClick = { - chooseColorProfile.launch(arrayOf("*/*")) - }, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_display_profile), + subtitle = basePreferences.displayProfile().get(), + onClick = { + chooseColorProfile.launch(arrayOf("*/*")) + }, + ), ), - ) ) } @Composable - private fun getExtensionsGroup( - basePreferences: BasePreferences, - ): Preference.PreferenceGroup { + private fun getExtensionsGroup(basePreferences: BasePreferences): Preference.PreferenceGroup { val context = LocalContext.current val uriHandler = LocalUriHandler.current val extensionInstallerPref = basePreferences.extensionInstaller() @@ -379,40 +382,41 @@ object SettingsAdvancedScreen : SearchableSettings { } return Preference.PreferenceGroup( title = stringResource(MR.strings.label_extensions), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - pref = extensionInstallerPref, - title = stringResource(MR.strings.ext_installer_pref), - entries = extensionInstallerPref.entries - .filter { - // TODO: allow private option in stable versions once URL handling is more fleshed out - if (isPreviewBuildType || isDevFlavor) { - true + preferenceItems = + persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = extensionInstallerPref, + title = stringResource(MR.strings.ext_installer_pref), + entries = + extensionInstallerPref.entries + .filter { + // TODO: allow private option in stable versions once URL handling is more fleshed out + if (isPreviewBuildType || isDevFlavor) { + true + } else { + it != BasePreferences.ExtensionInstaller.PRIVATE + } + }.associateWith { stringResource(it.titleRes) } + .toImmutableMap(), + onValueChanged = { + if (it == BasePreferences.ExtensionInstaller.SHIZUKU && + !context.isShizukuInstalled + ) { + shizukuMissing = true + false } else { - it != BasePreferences.ExtensionInstaller.PRIVATE + true } - } - .associateWith { stringResource(it.titleRes) } - .toImmutableMap(), - onValueChanged = { - if (it == BasePreferences.ExtensionInstaller.SHIZUKU && - !context.isShizukuInstalled - ) { - shizukuMissing = true - false - } else { - true - } - }, - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.ext_revoke_trust), - onClick = { - trustExtension.revokeAll() - context.toast(MR.strings.requires_app_restart) - }, + }, + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.ext_revoke_trust), + onClick = { + trustExtension.revokeAll() + context.toast(MR.strings.requires_app_restart) + }, + ), ), - ), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt index 10ad5e7bde..9f4c9cc5ae 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt @@ -29,7 +29,6 @@ import uy.kohesive.injekt.api.get import java.time.LocalDate object SettingsAppearanceScreen : SearchableSettings { - @ReadOnlyComposable @Composable override fun getTitleRes() = MR.strings.pref_category_appearance @@ -45,9 +44,7 @@ object SettingsAppearanceScreen : SearchableSettings { } @Composable - private fun getThemeGroup( - uiPreferences: UiPreferences, - ): Preference.PreferenceGroup { + private fun getThemeGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup { val context = LocalContext.current val themeModePref = uiPreferences.themeMode() @@ -61,100 +58,104 @@ object SettingsAppearanceScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_category_theme), - preferenceItems = persistentListOf( - Preference.PreferenceItem.CustomPreference( - title = stringResource(MR.strings.pref_app_theme), - ) { - Column { - AppThemeModePreferenceWidget( - value = themeMode, - onItemClick = { - themeModePref.set(it) - setAppCompatDelegateThemeMode(it) - }, - ) - - AppThemePreferenceWidget( - value = appTheme, - amoled = amoled, - onItemClick = { appThemePref.set(it) }, - ) - } - }, - Preference.PreferenceItem.SwitchPreference( - pref = amoledPref, - title = stringResource(MR.strings.pref_dark_theme_pure_black), - enabled = themeMode != ThemeMode.LIGHT, - onValueChanged = { - (context as? Activity)?.let { ActivityCompat.recreate(it) } - true + preferenceItems = + persistentListOf( + Preference.PreferenceItem.CustomPreference( + title = stringResource(MR.strings.pref_app_theme), + ) { + Column { + AppThemeModePreferenceWidget( + value = themeMode, + onItemClick = { + themeModePref.set(it) + setAppCompatDelegateThemeMode(it) + }, + ) + + AppThemePreferenceWidget( + value = appTheme, + amoled = amoled, + onItemClick = { appThemePref.set(it) }, + ) + } }, + Preference.PreferenceItem.SwitchPreference( + pref = amoledPref, + title = stringResource(MR.strings.pref_dark_theme_pure_black), + enabled = themeMode != ThemeMode.LIGHT, + onValueChanged = { + (context as? Activity)?.let { ActivityCompat.recreate(it) } + true + }, + ), ), - ), ) } @Composable - private fun getDisplayGroup( - uiPreferences: UiPreferences, - ): Preference.PreferenceGroup { + private fun getDisplayGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow val now = remember { LocalDate.now() } val dateFormat by uiPreferences.dateFormat().collectAsState() - val formattedNow = remember(dateFormat) { - UiPreferences.dateFormat(dateFormat).format(now) - } + val formattedNow = + remember(dateFormat) { + UiPreferences.dateFormat(dateFormat).format(now) + } return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_category_display), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_app_language), - onClick = { navigator.push(AppLanguageScreen()) }, - ), - Preference.PreferenceItem.ListPreference( - pref = uiPreferences.tabletUiMode(), - title = stringResource(MR.strings.pref_tablet_ui_mode), - entries = TabletUiMode.entries - .associateWith { stringResource(it.titleRes) } - .toImmutableMap(), - onValueChanged = { - context.toast(MR.strings.requires_app_restart) - true - }, - ), - Preference.PreferenceItem.ListPreference( - pref = uiPreferences.dateFormat(), - title = stringResource(MR.strings.pref_date_format), - entries = DateFormats - .associateWith { - val formattedDate = UiPreferences.dateFormat(it).format(now) - "${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)" - } - .toImmutableMap(), - ), - Preference.PreferenceItem.SwitchPreference( - pref = uiPreferences.relativeTime(), - title = stringResource(MR.strings.pref_relative_format), - subtitle = stringResource( - MR.strings.pref_relative_format_summary, - stringResource(MR.strings.relative_time_today), - formattedNow, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_app_language), + onClick = { navigator.push(AppLanguageScreen()) }, + ), + Preference.PreferenceItem.ListPreference( + pref = uiPreferences.tabletUiMode(), + title = stringResource(MR.strings.pref_tablet_ui_mode), + entries = + TabletUiMode.entries + .associateWith { stringResource(it.titleRes) } + .toImmutableMap(), + onValueChanged = { + context.toast(MR.strings.requires_app_restart) + true + }, + ), + Preference.PreferenceItem.ListPreference( + pref = uiPreferences.dateFormat(), + title = stringResource(MR.strings.pref_date_format), + entries = + DateFormats + .associateWith { + val formattedDate = UiPreferences.dateFormat(it).format(now) + "${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)" + }.toImmutableMap(), + ), + Preference.PreferenceItem.SwitchPreference( + pref = uiPreferences.relativeTime(), + title = stringResource(MR.strings.pref_relative_format), + subtitle = + stringResource( + MR.strings.pref_relative_format_summary, + stringResource(MR.strings.relative_time_today), + formattedNow, + ), ), ), - ), ) } } -private val DateFormats = listOf( - "", // Default - "MM/dd/yy", - "dd/MM/yy", - "yyyy-MM-dd", - "dd MMM yyyy", - "MMM dd, yyyy", -) +private val DateFormats = + listOf( + "", // Default + "MM/dd/yy", + "dd/MM/yy", + "yyyy-MM-dd", + "dd MMM yyyy", + "MMM dd, yyyy", + ) 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 b47dbe502d..53761f830a 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 @@ -23,7 +23,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object SettingsBrowseScreen : SearchableSettings { - @ReadOnlyComposable @Composable override fun getTitleRes() = MR.strings.browse @@ -41,35 +40,37 @@ object SettingsBrowseScreen : SearchableSettings { return listOf( Preference.PreferenceGroup( title = stringResource(MR.strings.label_sources), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = sourcePreferences.hideInLibraryItems(), - 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), - onClick = { - navigator.push(ExtensionReposScreen()) - }, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = sourcePreferences.hideInLibraryItems(), + 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), + onClick = { + navigator.push(ExtensionReposScreen()) + }, + ), ), - ), ), Preference.PreferenceGroup( title = stringResource(MR.strings.pref_category_nsfw_content), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = sourcePreferences.showNsfwSource(), - title = stringResource(MR.strings.pref_show_nsfw_source), - subtitle = stringResource(MR.strings.requires_app_restart), - onValueChanged = { - (context as FragmentActivity).authenticate( - title = context.stringResource(MR.strings.pref_category_nsfw_content), - ) - }, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = sourcePreferences.showNsfwSource(), + title = stringResource(MR.strings.pref_show_nsfw_source), + subtitle = stringResource(MR.strings.requires_app_restart), + onValueChanged = { + (context as FragmentActivity).authenticate( + title = context.stringResource(MR.strings.pref_category_nsfw_content), + ) + }, + ), + Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.parental_controls_info)), ), - Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.parental_controls_info)), - ), ), ) } 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 ecfd2ec756..acaac24b4d 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 @@ -65,7 +65,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object SettingsDataScreen : SearchableSettings { - val restorePreferenceKeyString = MR.strings.label_backup const val HELP_URL = "https://mihon.app/docs/faq/storage" @@ -92,7 +91,6 @@ object SettingsDataScreen : SearchableSettings { return persistentListOf( getStorageLocationPref(storagePreferences = storagePreferences), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)), - getBackupAndRestoreGroup(backupPreferences = backupPreferences), getDataGroup(), ) @@ -108,8 +106,9 @@ object SettingsDataScreen : SearchableSettings { contract = ActivityResultContracts.OpenDocumentTree(), ) { uri -> if (uri != null) { - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION + val flags = + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION // For some reason InkBook devices do not implement the SAF properly. Persistable URI grants do not // work. However, simply retrieving the URI and using it works fine for these devices. Access is not @@ -131,9 +130,7 @@ object SettingsDataScreen : SearchableSettings { } @Composable - fun storageLocationText( - storageDirPref: tachiyomi.core.common.preference.Preference, - ): String { + fun storageLocationText(storageDirPref: tachiyomi.core.common.preference.Preference): String { val context = LocalContext.current val storageDir by storageDirPref.collectAsState() @@ -174,91 +171,97 @@ object SettingsDataScreen : SearchableSettings { val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState() - val chooseBackup = rememberLauncherForActivityResult( - object : ActivityResultContracts.GetContent() { - override fun createIntent(context: Context, input: String): Intent { - val intent = super.createIntent(context, input) - return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup)) + val chooseBackup = + rememberLauncherForActivityResult( + object : ActivityResultContracts.GetContent() { + override fun createIntent( + context: Context, + input: String, + ): Intent { + val intent = super.createIntent(context, input) + return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup)) + } + }, + ) { + if (it == null) { + context.toast(MR.strings.file_null_uri_error) + return@rememberLauncherForActivityResult } - }, - ) { - if (it == null) { - context.toast(MR.strings.file_null_uri_error) - return@rememberLauncherForActivityResult - } - navigator.push(RestoreBackupScreen(it.toString())) - } + navigator.push(RestoreBackupScreen(it.toString())) + } return Preference.PreferenceGroup( title = stringResource(MR.strings.label_backup), - preferenceItems = persistentListOf( - // Manual actions - Preference.PreferenceItem.CustomPreference( - title = stringResource(restorePreferenceKeyString), - ) { - BasePreferenceWidget( - subcomponent = { - MultiChoiceSegmentedButtonRow( - modifier = Modifier - .fillMaxWidth() - .height(intrinsicSize = IntrinsicSize.Min) - .padding(horizontal = PrefsHorizontalPadding), - ) { - SegmentedButton( - modifier = Modifier.fillMaxHeight(), - checked = false, - onCheckedChange = { navigator.push(CreateBackupScreen()) }, - shape = SegmentedButtonDefaults.itemShape(0, 2), + preferenceItems = + persistentListOf( + // Manual actions + Preference.PreferenceItem.CustomPreference( + title = stringResource(restorePreferenceKeyString), + ) { + BasePreferenceWidget( + subcomponent = { + MultiChoiceSegmentedButtonRow( + modifier = + Modifier + .fillMaxWidth() + .height(intrinsicSize = IntrinsicSize.Min) + .padding(horizontal = PrefsHorizontalPadding), ) { - Text(stringResource(MR.strings.pref_create_backup)) - } - SegmentedButton( - modifier = Modifier.fillMaxHeight(), - checked = false, - onCheckedChange = { - if (!BackupRestoreJob.isRunning(context)) { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - context.toast(MR.strings.restore_miui_warning) - } + SegmentedButton( + modifier = Modifier.fillMaxHeight(), + checked = false, + onCheckedChange = { navigator.push(CreateBackupScreen()) }, + shape = SegmentedButtonDefaults.itemShape(0, 2), + ) { + Text(stringResource(MR.strings.pref_create_backup)) + } + SegmentedButton( + modifier = Modifier.fillMaxHeight(), + checked = false, + onCheckedChange = { + if (!BackupRestoreJob.isRunning(context)) { + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { + context.toast(MR.strings.restore_miui_warning) + } - // no need to catch because it's wrapped with a chooser - chooseBackup.launch("*/*") - } else { - context.toast(MR.strings.restore_in_progress) - } - }, - shape = SegmentedButtonDefaults.itemShape(1, 2), - ) { - Text(stringResource(MR.strings.pref_restore_backup)) + // no need to catch because it's wrapped with a chooser + chooseBackup.launch("*/*") + } else { + context.toast(MR.strings.restore_in_progress) + } + }, + shape = SegmentedButtonDefaults.itemShape(1, 2), + ) { + Text(stringResource(MR.strings.pref_restore_backup)) + } } - } + }, + ) + }, + // Automatic backups + Preference.PreferenceItem.ListPreference( + pref = backupPreferences.backupInterval(), + title = stringResource(MR.strings.pref_backup_interval), + entries = + persistentMapOf( + 0 to stringResource(MR.strings.off), + 6 to stringResource(MR.strings.update_6hour), + 12 to stringResource(MR.strings.update_12hour), + 24 to stringResource(MR.strings.update_24hour), + 48 to stringResource(MR.strings.update_48hour), + 168 to stringResource(MR.strings.update_weekly), + ), + onValueChanged = { + BackupCreateJob.setupTask(context, it) + true }, - ) - }, - - // Automatic backups - Preference.PreferenceItem.ListPreference( - pref = backupPreferences.backupInterval(), - title = stringResource(MR.strings.pref_backup_interval), - entries = persistentMapOf( - 0 to stringResource(MR.strings.off), - 6 to stringResource(MR.strings.update_6hour), - 12 to stringResource(MR.strings.update_12hour), - 24 to stringResource(MR.strings.update_24hour), - 48 to stringResource(MR.strings.update_48hour), - 168 to stringResource(MR.strings.update_weekly), ), - onValueChanged = { - BackupCreateJob.setupTask(context, it) - true - }, - ), - Preference.PreferenceItem.InfoPreference( - stringResource(MR.strings.backup_info) + "\n\n" + - stringResource(MR.strings.last_auto_backup_info, relativeTimeSpanString(lastAutoBackup)), + Preference.PreferenceItem.InfoPreference( + stringResource(MR.strings.backup_info) + "\n\n" + + stringResource(MR.strings.last_auto_backup_info, relativeTimeSpanString(lastAutoBackup)), + ), ), - ), ) } @@ -274,42 +277,42 @@ object SettingsDataScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_storage_usage), - preferenceItems = persistentListOf( - Preference.PreferenceItem.CustomPreference( - title = stringResource(MR.strings.pref_storage_usage), - ) { - BasePreferenceWidget( - subcomponent = { - StorageInfo( - modifier = Modifier.padding(horizontal = PrefsHorizontalPadding), - ) - }, - ) - }, - - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_clear_chapter_cache), - subtitle = stringResource(MR.strings.used_cache, cacheReadableSize), - onClick = { - scope.launchNonCancellable { - try { - val deletedFiles = chapterCache.clear() - withUIContext { - context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles)) - cacheReadableSizeSema++ + preferenceItems = + persistentListOf( + Preference.PreferenceItem.CustomPreference( + title = stringResource(MR.strings.pref_storage_usage), + ) { + BasePreferenceWidget( + subcomponent = { + StorageInfo( + modifier = Modifier.padding(horizontal = PrefsHorizontalPadding), + ) + }, + ) + }, + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_clear_chapter_cache), + subtitle = stringResource(MR.strings.used_cache, cacheReadableSize), + onClick = { + scope.launchNonCancellable { + try { + val deletedFiles = chapterCache.clear() + withUIContext { + context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles)) + cacheReadableSizeSema++ + } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) + withUIContext { context.toast(MR.strings.cache_delete_error) } } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) - withUIContext { context.toast(MR.strings.cache_delete_error) } } - } - }, - ), - Preference.PreferenceItem.SwitchPreference( - pref = libraryPreferences.autoClearChapterCache(), - title = stringResource(MR.strings.pref_auto_clear_chapter_cache), + }, + ), + Preference.PreferenceItem.SwitchPreference( + pref = libraryPreferences.autoClearChapterCache(), + title = stringResource(MR.strings.pref_auto_clear_chapter_cache), + ), ), - ), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt index 072013415b..a49c2053bb 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt @@ -27,7 +27,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object SettingsDownloadScreen : SearchableSettings { - @ReadOnlyComposable @Composable override fun getTitleRes() = MR.strings.pref_category_downloads @@ -68,51 +67,52 @@ object SettingsDownloadScreen : SearchableSettings { private fun getDeleteChaptersGroup( downloadPreferences: DownloadPreferences, categories: List, - ): Preference.PreferenceGroup { - return Preference.PreferenceGroup( + ): Preference.PreferenceGroup = + Preference.PreferenceGroup( title = stringResource(MR.strings.pref_category_delete_chapters), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = downloadPreferences.removeAfterMarkedAsRead(), - title = stringResource(MR.strings.pref_remove_after_marked_as_read), - ), - Preference.PreferenceItem.ListPreference( - pref = downloadPreferences.removeAfterReadSlots(), - title = stringResource(MR.strings.pref_remove_after_read), - entries = persistentMapOf( - -1 to stringResource(MR.strings.disabled), - 0 to stringResource(MR.strings.last_read_chapter), - 1 to stringResource(MR.strings.second_to_last), - 2 to stringResource(MR.strings.third_to_last), - 3 to stringResource(MR.strings.fourth_to_last), - 4 to stringResource(MR.strings.fifth_to_last), + preferenceItems = + persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = downloadPreferences.removeAfterMarkedAsRead(), + title = stringResource(MR.strings.pref_remove_after_marked_as_read), + ), + Preference.PreferenceItem.ListPreference( + pref = downloadPreferences.removeAfterReadSlots(), + title = stringResource(MR.strings.pref_remove_after_read), + entries = + persistentMapOf( + -1 to stringResource(MR.strings.disabled), + 0 to stringResource(MR.strings.last_read_chapter), + 1 to stringResource(MR.strings.second_to_last), + 2 to stringResource(MR.strings.third_to_last), + 3 to stringResource(MR.strings.fourth_to_last), + 4 to stringResource(MR.strings.fifth_to_last), + ), + ), + Preference.PreferenceItem.SwitchPreference( + pref = downloadPreferences.removeBookmarkedChapters(), + title = stringResource(MR.strings.pref_remove_bookmarked_chapters), + ), + getExcludedCategoriesPreference( + downloadPreferences = downloadPreferences, + categories = { categories }, ), ), - Preference.PreferenceItem.SwitchPreference( - pref = downloadPreferences.removeBookmarkedChapters(), - title = stringResource(MR.strings.pref_remove_bookmarked_chapters), - ), - getExcludedCategoriesPreference( - downloadPreferences = downloadPreferences, - categories = { categories }, - ), - ), ) - } @Composable private fun getExcludedCategoriesPreference( downloadPreferences: DownloadPreferences, categories: () -> List, - ): Preference.PreferenceItem.MultiSelectListPreference { - return Preference.PreferenceItem.MultiSelectListPreference( + ): Preference.PreferenceItem.MultiSelectListPreference = + Preference.PreferenceItem.MultiSelectListPreference( pref = downloadPreferences.removeExcludeCategories(), title = stringResource(MR.strings.pref_remove_exclude_categories), - entries = categories() - .associate { it.id.toString() to it.visualName } - .toImmutableMap(), + entries = + categories() + .associate { it.id.toString() to it.visualName } + .toImmutableMap(), ) - } @Composable private fun getAutoDownloadGroup( @@ -147,47 +147,47 @@ object SettingsDownloadScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_category_auto_download), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = downloadNewChaptersPref, - title = stringResource(MR.strings.pref_download_new), - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.categories), - subtitle = getCategoriesLabel( - allCategories = allCategories, - included = included, - excluded = excluded, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = downloadNewChaptersPref, + title = stringResource(MR.strings.pref_download_new), + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.categories), + subtitle = + getCategoriesLabel( + allCategories = allCategories, + included = included, + excluded = excluded, + ), + onClick = { showDialog = true }, + enabled = downloadNewChapters, ), - onClick = { showDialog = true }, - enabled = downloadNewChapters, ), - ), ) } @Composable - private fun getDownloadAheadGroup( - downloadPreferences: DownloadPreferences, - ): Preference.PreferenceGroup { - return Preference.PreferenceGroup( + private fun getDownloadAheadGroup(downloadPreferences: DownloadPreferences): Preference.PreferenceGroup = + Preference.PreferenceGroup( title = stringResource(MR.strings.download_ahead), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - pref = downloadPreferences.autoDownloadWhileReading(), - title = stringResource(MR.strings.auto_download_while_reading), - entries = listOf(0, 2, 3, 5, 10) - .associateWith { - if (it == 0) { - stringResource(MR.strings.disabled) - } else { - pluralStringResource(MR.plurals.next_unread_chapters, count = it, it) - } - } - .toImmutableMap(), + preferenceItems = + persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = downloadPreferences.autoDownloadWhileReading(), + title = stringResource(MR.strings.auto_download_while_reading), + entries = + listOf(0, 2, 3, 5, 10) + .associateWith { + if (it == 0) { + stringResource(MR.strings.disabled) + } else { + pluralStringResource(MR.plurals.next_unread_chapters, count = it, it) + } + }.toImmutableMap(), + ), + Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)), ), - Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)), - ), ) - } } 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 346a60b862..9c14feb325 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 @@ -44,7 +44,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object SettingsLibraryScreen : SearchableSettings { - @Composable @ReadOnlyComposable override fun getTitleRes() = MR.strings.pref_category_library @@ -75,42 +74,46 @@ object SettingsLibraryScreen : SearchableSettings { val selectedCategory = allCategories.find { it.id == defaultCategory.toLong() } // For default category - val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) + - allCategories.fastMap { it.id.toInt() } - val labels = listOf(stringResource(MR.strings.default_category_summary)) + - allCategories.fastMap { it.visualName } + val ids = + listOf(libraryPreferences.defaultCategory().defaultValue()) + + allCategories.fastMap { it.id.toInt() } + val labels = + listOf(stringResource(MR.strings.default_category_summary)) + + allCategories.fastMap { it.visualName } return Preference.PreferenceGroup( title = stringResource(MR.strings.categories), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.action_edit_categories), - subtitle = pluralStringResource( - MR.plurals.num_categories, - count = userCategoriesCount, - userCategoriesCount, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.action_edit_categories), + subtitle = + pluralStringResource( + MR.plurals.num_categories, + count = userCategoriesCount, + userCategoriesCount, + ), + onClick = { navigator.push(CategoryScreen()) }, ), - onClick = { navigator.push(CategoryScreen()) }, - ), - Preference.PreferenceItem.ListPreference( - pref = libraryPreferences.defaultCategory(), - title = stringResource(MR.strings.default_category), - subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary), - entries = ids.zip(labels).toMap().toImmutableMap(), - ), - Preference.PreferenceItem.SwitchPreference( - pref = libraryPreferences.categorizedDisplaySettings(), - title = stringResource(MR.strings.categorized_display_settings), - onValueChanged = { - if (!it) { - scope.launch { - Injekt.get().await() + Preference.PreferenceItem.ListPreference( + pref = libraryPreferences.defaultCategory(), + title = stringResource(MR.strings.default_category), + subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary), + entries = ids.zip(labels).toMap().toImmutableMap(), + ), + Preference.PreferenceItem.SwitchPreference( + pref = libraryPreferences.categorizedDisplaySettings(), + title = stringResource(MR.strings.categorized_display_settings), + onValueChanged = { + if (!it) { + scope.launch { + Injekt.get().await() + } } - } - true - }, + true + }, + ), ), - ), ) } @@ -149,107 +152,113 @@ object SettingsLibraryScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_category_library_update), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - pref = autoUpdateIntervalPref, - title = stringResource(MR.strings.pref_library_update_interval), - entries = persistentMapOf( - 0 to stringResource(MR.strings.update_never), - 12 to stringResource(MR.strings.update_12hour), - 24 to stringResource(MR.strings.update_24hour), - 48 to stringResource(MR.strings.update_48hour), - 72 to stringResource(MR.strings.update_72hour), - 168 to stringResource(MR.strings.update_weekly), + preferenceItems = + persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = autoUpdateIntervalPref, + title = stringResource(MR.strings.pref_library_update_interval), + entries = + persistentMapOf( + 0 to stringResource(MR.strings.update_never), + 12 to stringResource(MR.strings.update_12hour), + 24 to stringResource(MR.strings.update_24hour), + 48 to stringResource(MR.strings.update_48hour), + 72 to stringResource(MR.strings.update_72hour), + 168 to stringResource(MR.strings.update_weekly), + ), + onValueChanged = { + LibraryUpdateJob.setupTask(context, it) + true + }, ), - onValueChanged = { - LibraryUpdateJob.setupTask(context, it) - true - }, - ), - Preference.PreferenceItem.MultiSelectListPreference( - pref = libraryPreferences.autoUpdateDeviceRestrictions(), - enabled = autoUpdateInterval > 0, - title = stringResource(MR.strings.pref_library_update_restriction), - subtitle = stringResource(MR.strings.restrictions), - entries = persistentMapOf( - DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi), - DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered), - DEVICE_CHARGING to stringResource(MR.strings.charging), + Preference.PreferenceItem.MultiSelectListPreference( + pref = libraryPreferences.autoUpdateDeviceRestrictions(), + enabled = autoUpdateInterval > 0, + title = stringResource(MR.strings.pref_library_update_restriction), + subtitle = stringResource(MR.strings.restrictions), + entries = + persistentMapOf( + DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi), + DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered), + DEVICE_CHARGING to stringResource(MR.strings.charging), + ), + onValueChanged = { + // Post to event looper to allow the preference to be updated. + ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) } + true + }, ), - onValueChanged = { - // Post to event looper to allow the preference to be updated. - ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) } - true - }, - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.categories), - subtitle = getCategoriesLabel( - allCategories = allCategories, - included = included, - excluded = excluded, + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.categories), + subtitle = + getCategoriesLabel( + allCategories = allCategories, + included = included, + excluded = excluded, + ), + onClick = { showCategoriesDialog = true }, ), - onClick = { showCategoriesDialog = true }, - ), - Preference.PreferenceItem.SwitchPreference( - pref = libraryPreferences.autoUpdateMetadata(), - title = stringResource(MR.strings.pref_library_update_refresh_metadata), - subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary), - ), - Preference.PreferenceItem.MultiSelectListPreference( - pref = libraryPreferences.autoUpdateMangaRestrictions(), - title = stringResource(MR.strings.pref_library_update_smart_update), - entries = persistentMapOf( - MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read), - MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started), - MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed), - MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period), + Preference.PreferenceItem.SwitchPreference( + pref = libraryPreferences.autoUpdateMetadata(), + title = stringResource(MR.strings.pref_library_update_refresh_metadata), + subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary), + ), + Preference.PreferenceItem.MultiSelectListPreference( + pref = libraryPreferences.autoUpdateMangaRestrictions(), + title = stringResource(MR.strings.pref_library_update_smart_update), + entries = + persistentMapOf( + MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read), + MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started), + MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed), + MANGA_OUTSIDE_RELEASE_PERIOD to + stringResource(MR.strings.pref_update_only_in_release_period), + ), + ), + Preference.PreferenceItem.SwitchPreference( + pref = libraryPreferences.newShowUpdatesCount(), + title = stringResource(MR.strings.pref_library_update_show_tab_badge), ), ), - Preference.PreferenceItem.SwitchPreference( - pref = libraryPreferences.newShowUpdatesCount(), - title = stringResource(MR.strings.pref_library_update_show_tab_badge), - ), - ), ) } @Composable - private fun getChapterSwipeActionsGroup( - libraryPreferences: LibraryPreferences, - ): Preference.PreferenceGroup { - return Preference.PreferenceGroup( + private fun getChapterSwipeActionsGroup(libraryPreferences: LibraryPreferences): Preference.PreferenceGroup = + Preference.PreferenceGroup( title = stringResource(MR.strings.pref_chapter_swipe), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - pref = libraryPreferences.swipeToStartAction(), - title = stringResource(MR.strings.pref_chapter_swipe_start), - entries = persistentMapOf( - LibraryPreferences.ChapterSwipeAction.Disabled to - stringResource(MR.strings.disabled), - LibraryPreferences.ChapterSwipeAction.ToggleBookmark to - stringResource(MR.strings.action_bookmark), - LibraryPreferences.ChapterSwipeAction.ToggleRead to - stringResource(MR.strings.action_mark_as_read), - LibraryPreferences.ChapterSwipeAction.Download to - stringResource(MR.strings.action_download), + preferenceItems = + persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = libraryPreferences.swipeToStartAction(), + title = stringResource(MR.strings.pref_chapter_swipe_start), + entries = + persistentMapOf( + LibraryPreferences.ChapterSwipeAction.Disabled to + stringResource(MR.strings.disabled), + LibraryPreferences.ChapterSwipeAction.ToggleBookmark to + stringResource(MR.strings.action_bookmark), + LibraryPreferences.ChapterSwipeAction.ToggleRead to + stringResource(MR.strings.action_mark_as_read), + LibraryPreferences.ChapterSwipeAction.Download to + stringResource(MR.strings.action_download), + ), ), - ), - Preference.PreferenceItem.ListPreference( - pref = libraryPreferences.swipeToEndAction(), - title = stringResource(MR.strings.pref_chapter_swipe_end), - entries = persistentMapOf( - LibraryPreferences.ChapterSwipeAction.Disabled to - stringResource(MR.strings.disabled), - LibraryPreferences.ChapterSwipeAction.ToggleBookmark to - stringResource(MR.strings.action_bookmark), - LibraryPreferences.ChapterSwipeAction.ToggleRead to - stringResource(MR.strings.action_mark_as_read), - LibraryPreferences.ChapterSwipeAction.Download to - stringResource(MR.strings.action_download), + Preference.PreferenceItem.ListPreference( + pref = libraryPreferences.swipeToEndAction(), + title = stringResource(MR.strings.pref_chapter_swipe_end), + entries = + persistentMapOf( + LibraryPreferences.ChapterSwipeAction.Disabled to + stringResource(MR.strings.disabled), + LibraryPreferences.ChapterSwipeAction.ToggleBookmark to + stringResource(MR.strings.action_bookmark), + LibraryPreferences.ChapterSwipeAction.ToggleRead to + stringResource(MR.strings.action_mark_as_read), + LibraryPreferences.ChapterSwipeAction.Download to + stringResource(MR.strings.action_download), + ), ), ), - ), ) - } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt index cfb010e599..1d43fc911e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt @@ -51,7 +51,6 @@ import tachiyomi.presentation.core.i18n.stringResource import cafe.adriel.voyager.core.screen.Screen as VoyagerScreen object SettingsMainScreen : Screen() { - @Composable override fun Content() { Content(twoPane = false) @@ -64,11 +63,12 @@ object SettingsMainScreen : Screen() { return remember(surface, dark) { val arr = FloatArray(3) ColorUtils.colorToHSL(surface.toArgb(), arr) - arr[2] = if (dark) { - arr[2] - 0.05f - } else { - arr[2] + 0.02f - }.coerceIn(0f, 1f) + arr[2] = + if (dark) { + arr[2] - 0.05f + } else { + arr[2] + 0.02f + }.coerceIn(0f, 1f) Color.hsl(arr[0], arr[1], arr[2]) } } @@ -103,20 +103,22 @@ object SettingsMainScreen : Screen() { containerColor = containerColor, content = { contentPadding -> val state = rememberLazyListState() - val indexSelected = if (twoPane) { - items.indexOfFirst { it.screen::class == navigator.items.first()::class } - .also { - LaunchedEffect(Unit) { - state.animateScrollToItem(it) - if (it > 0) { - // Lift scroll - topBarState.contentOffset = topBarState.heightOffsetLimit + val indexSelected = + if (twoPane) { + items + .indexOfFirst { it.screen::class == navigator.items.first()::class } + .also { + LaunchedEffect(Unit) { + state.animateScrollToItem(it) + if (it > 0) { + // Lift scroll + topBarState.contentOffset = topBarState.heightOffsetLimit + } } } - } - } else { - null - } + } else { + null + } LazyColumn( state = state, @@ -130,16 +132,17 @@ object SettingsMainScreen : Screen() { var modifier: Modifier = Modifier var contentColor = LocalContentColor.current if (twoPane) { - modifier = Modifier - .padding(horizontal = 8.dp) - .clip(RoundedCornerShape(24.dp)) - .then( - if (selected) { - Modifier.background(MaterialTheme.colorScheme.surfaceVariant) - } else { - Modifier - }, - ) + modifier = + Modifier + .padding(horizontal = 8.dp) + .clip(RoundedCornerShape(24.dp)) + .then( + if (selected) { + Modifier.background(MaterialTheme.colorScheme.surfaceVariant) + } else { + Modifier + }, + ) if (selected) { contentColor = MaterialTheme.colorScheme.onSurfaceVariant } @@ -159,7 +162,10 @@ object SettingsMainScreen : Screen() { ) } - private fun Navigator.navigate(screen: VoyagerScreen, twoPane: Boolean) { + private fun Navigator.navigate( + screen: VoyagerScreen, + twoPane: Boolean, + ) { if (twoPane) replaceAll(screen) else push(screen) } @@ -171,68 +177,69 @@ object SettingsMainScreen : Screen() { val screen: VoyagerScreen, ) - private val items = listOf( - Item( - titleRes = MR.strings.pref_category_appearance, - subtitleRes = MR.strings.pref_appearance_summary, - icon = Icons.Outlined.Palette, - screen = SettingsAppearanceScreen, - ), - Item( - titleRes = MR.strings.pref_category_library, - subtitleRes = MR.strings.pref_library_summary, - icon = Icons.Outlined.CollectionsBookmark, - screen = SettingsLibraryScreen, - ), - Item( - titleRes = MR.strings.pref_category_reader, - subtitleRes = MR.strings.pref_reader_summary, - icon = Icons.AutoMirrored.Outlined.ChromeReaderMode, - screen = SettingsReaderScreen, - ), - Item( - titleRes = MR.strings.pref_category_downloads, - subtitleRes = MR.strings.pref_downloads_summary, - icon = Icons.Outlined.GetApp, - screen = SettingsDownloadScreen, - ), - Item( - titleRes = MR.strings.pref_category_tracking, - subtitleRes = MR.strings.pref_tracking_summary, - icon = Icons.Outlined.Sync, - screen = SettingsTrackingScreen, - ), - Item( - titleRes = MR.strings.browse, - subtitleRes = MR.strings.pref_browse_summary, - icon = Icons.Outlined.Explore, - screen = SettingsBrowseScreen, - ), - Item( - titleRes = MR.strings.label_data_storage, - subtitleRes = MR.strings.pref_backup_summary, - icon = Icons.Outlined.Storage, - screen = SettingsDataScreen, - ), - Item( - titleRes = MR.strings.pref_category_security, - subtitleRes = MR.strings.pref_security_summary, - icon = Icons.Outlined.Security, - screen = SettingsSecurityScreen, - ), - Item( - titleRes = MR.strings.pref_category_advanced, - subtitleRes = MR.strings.pref_advanced_summary, - icon = Icons.Outlined.Code, - screen = SettingsAdvancedScreen, - ), - Item( - titleRes = MR.strings.pref_category_about, - formatSubtitle = { - "${stringResource(MR.strings.app_name)} ${AboutScreen.getVersionName(withBuildDate = false)}" - }, - icon = Icons.Outlined.Info, - screen = AboutScreen, - ), - ) + private val items = + listOf( + Item( + titleRes = MR.strings.pref_category_appearance, + subtitleRes = MR.strings.pref_appearance_summary, + icon = Icons.Outlined.Palette, + screen = SettingsAppearanceScreen, + ), + Item( + titleRes = MR.strings.pref_category_library, + subtitleRes = MR.strings.pref_library_summary, + icon = Icons.Outlined.CollectionsBookmark, + screen = SettingsLibraryScreen, + ), + Item( + titleRes = MR.strings.pref_category_reader, + subtitleRes = MR.strings.pref_reader_summary, + icon = Icons.AutoMirrored.Outlined.ChromeReaderMode, + screen = SettingsReaderScreen, + ), + Item( + titleRes = MR.strings.pref_category_downloads, + subtitleRes = MR.strings.pref_downloads_summary, + icon = Icons.Outlined.GetApp, + screen = SettingsDownloadScreen, + ), + Item( + titleRes = MR.strings.pref_category_tracking, + subtitleRes = MR.strings.pref_tracking_summary, + icon = Icons.Outlined.Sync, + screen = SettingsTrackingScreen, + ), + Item( + titleRes = MR.strings.browse, + subtitleRes = MR.strings.pref_browse_summary, + icon = Icons.Outlined.Explore, + screen = SettingsBrowseScreen, + ), + Item( + titleRes = MR.strings.label_data_storage, + subtitleRes = MR.strings.pref_backup_summary, + icon = Icons.Outlined.Storage, + screen = SettingsDataScreen, + ), + Item( + titleRes = MR.strings.pref_category_security, + subtitleRes = MR.strings.pref_security_summary, + icon = Icons.Outlined.Security, + screen = SettingsSecurityScreen, + ), + Item( + titleRes = MR.strings.pref_category_advanced, + subtitleRes = MR.strings.pref_advanced_summary, + icon = Icons.Outlined.Code, + screen = SettingsAdvancedScreen, + ), + Item( + titleRes = MR.strings.pref_category_about, + formatSubtitle = { + "${stringResource(MR.strings.app_name)} ${AboutScreen.getVersionName(withBuildDate = false)}" + }, + icon = Icons.Outlined.Info, + screen = AboutScreen, + ), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt index 7b5aa15f30..69130d7c42 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt @@ -22,7 +22,6 @@ import uy.kohesive.injekt.api.get import java.text.NumberFormat object SettingsReaderScreen : SearchableSettings { - @ReadOnlyComposable @Composable override fun getTitleRes() = MR.strings.pref_category_reader @@ -35,18 +34,21 @@ object SettingsReaderScreen : SearchableSettings { Preference.PreferenceItem.ListPreference( pref = readerPref.defaultReadingMode(), title = stringResource(MR.strings.pref_viewer_type), - entries = ReadingMode.entries.drop(1) - .associate { it.flagValue to stringResource(it.stringRes) } - .toImmutableMap(), + entries = + ReadingMode.entries + .drop(1) + .associate { it.flagValue to stringResource(it.stringRes) } + .toImmutableMap(), ), Preference.PreferenceItem.ListPreference( pref = readerPref.doubleTapAnimSpeed(), title = stringResource(MR.strings.pref_double_tap_anim_speed), - entries = persistentMapOf( - 1 to stringResource(MR.strings.double_tap_anim_speed_0), - 500 to stringResource(MR.strings.double_tap_anim_speed_normal), - 250 to stringResource(MR.strings.double_tap_anim_speed_fast), - ), + entries = + persistentMapOf( + 1 to stringResource(MR.strings.double_tap_anim_speed_0), + 500 to stringResource(MR.strings.double_tap_anim_speed_normal), + 250 to stringResource(MR.strings.double_tap_anim_speed_fast), + ), ), Preference.PreferenceItem.SwitchPreference( pref = readerPref.showReadingMode(), @@ -78,44 +80,50 @@ object SettingsReaderScreen : SearchableSettings { val fullscreen by fullscreenPref.collectAsState() return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_category_display), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - pref = readerPreferences.defaultOrientationType(), - title = stringResource(MR.strings.pref_rotation_type), - entries = ReaderOrientation.entries.drop(1) - .associate { it.flagValue to stringResource(it.stringRes) } - .toImmutableMap(), - ), - Preference.PreferenceItem.ListPreference( - pref = readerPreferences.readerTheme(), - title = stringResource(MR.strings.pref_reader_theme), - entries = persistentMapOf( - 1 to stringResource(MR.strings.black_background), - 2 to stringResource(MR.strings.gray_background), - 0 to stringResource(MR.strings.white_background), - 3 to stringResource(MR.strings.automatic_background), + preferenceItems = + persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = readerPreferences.defaultOrientationType(), + title = stringResource(MR.strings.pref_rotation_type), + entries = + ReaderOrientation.entries + .drop(1) + .associate { it.flagValue to stringResource(it.stringRes) } + .toImmutableMap(), + ), + Preference.PreferenceItem.ListPreference( + pref = readerPreferences.readerTheme(), + title = stringResource(MR.strings.pref_reader_theme), + entries = + persistentMapOf( + 1 to stringResource(MR.strings.black_background), + 2 to stringResource(MR.strings.gray_background), + 0 to stringResource(MR.strings.white_background), + 3 to stringResource(MR.strings.automatic_background), + ), + ), + Preference.PreferenceItem.SwitchPreference( + pref = fullscreenPref, + title = stringResource(MR.strings.pref_fullscreen), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.cutoutShort(), + title = stringResource(MR.strings.pref_cutout_short), + enabled = + fullscreen && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + LocalView.current.rootWindowInsets?.displayCutout != null, + // has cutout + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.keepScreenOn(), + title = stringResource(MR.strings.pref_keep_screen_on), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.showPageNumber(), + title = stringResource(MR.strings.pref_show_page_number), ), ), - Preference.PreferenceItem.SwitchPreference( - pref = fullscreenPref, - title = stringResource(MR.strings.pref_fullscreen), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.cutoutShort(), - title = stringResource(MR.strings.pref_cutout_short), - enabled = fullscreen && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && - LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.keepScreenOn(), - title = stringResource(MR.strings.pref_keep_screen_on), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.showPageNumber(), - title = stringResource(MR.strings.pref_show_page_number), - ), - ), ) } @@ -133,75 +141,77 @@ object SettingsReaderScreen : SearchableSettings { return Preference.PreferenceGroup( title = "E-Ink", - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.flashOnPageChange(), - title = stringResource(MR.strings.pref_flash_page), - subtitle = stringResource(MR.strings.pref_flash_page_summ), - ), - Preference.PreferenceItem.SliderPreference( - value = flashMillis / ReaderPreferences.MILLI_CONVERSION, - min = 1, - max = 15, - title = stringResource(MR.strings.pref_flash_duration), - subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), - onValueChanged = { - flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) - true - }, - enabled = flashPageState, - ), - Preference.PreferenceItem.SliderPreference( - value = flashInterval, - min = 1, - max = 10, - title = stringResource(MR.strings.pref_flash_page_interval), - subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), - onValueChanged = { - flashIntervalPref.set(it) - true - }, - enabled = flashPageState, - ), - Preference.PreferenceItem.ListPreference( - pref = flashColorPref, - title = stringResource(MR.strings.pref_flash_with), - entries = persistentMapOf( - ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black), - ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white), - ReaderPreferences.FlashColor.WHITE_BLACK - to stringResource(MR.strings.pref_flash_style_white_black), - ), - enabled = flashPageState, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.flashOnPageChange(), + title = stringResource(MR.strings.pref_flash_page), + subtitle = stringResource(MR.strings.pref_flash_page_summ), + ), + Preference.PreferenceItem.SliderPreference( + value = flashMillis / ReaderPreferences.MILLI_CONVERSION, + min = 1, + max = 15, + title = stringResource(MR.strings.pref_flash_duration), + subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), + onValueChanged = { + flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) + true + }, + enabled = flashPageState, + ), + Preference.PreferenceItem.SliderPreference( + value = flashInterval, + min = 1, + max = 10, + title = stringResource(MR.strings.pref_flash_page_interval), + subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), + onValueChanged = { + flashIntervalPref.set(it) + true + }, + enabled = flashPageState, + ), + Preference.PreferenceItem.ListPreference( + pref = flashColorPref, + title = stringResource(MR.strings.pref_flash_with), + entries = + persistentMapOf( + ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black), + ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white), + ReaderPreferences.FlashColor.WHITE_BLACK + to stringResource(MR.strings.pref_flash_style_white_black), + ), + enabled = flashPageState, + ), ), - ), ) } @Composable - private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup { - return Preference.PreferenceGroup( + private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup = + Preference.PreferenceGroup( title = stringResource(MR.strings.pref_category_reading), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.skipRead(), - title = stringResource(MR.strings.pref_skip_read_chapters), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.skipFiltered(), - title = stringResource(MR.strings.pref_skip_filtered_chapters), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.skipDupe(), - title = stringResource(MR.strings.pref_skip_dupe_chapters), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.alwaysShowChapterTransition(), - title = stringResource(MR.strings.pref_always_show_chapter_transition), + preferenceItems = + persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.skipRead(), + title = stringResource(MR.strings.pref_skip_read_chapters), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.skipFiltered(), + title = stringResource(MR.strings.pref_skip_filtered_chapters), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.skipDupe(), + title = stringResource(MR.strings.pref_skip_dupe_chapters), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.alwaysShowChapterTransition(), + title = stringResource(MR.strings.pref_always_show_chapter_transition), + ), ), - ), ) - } @Composable private fun getPagedGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup { @@ -217,86 +227,90 @@ object SettingsReaderScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.pager_viewer), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - pref = navModePref, - title = stringResource(MR.strings.pref_viewer_nav), - entries = ReaderPreferences.TapZones - .mapIndexed { index, it -> index to stringResource(it) } - .toMap() - .toImmutableMap(), - ), - Preference.PreferenceItem.ListPreference( - pref = readerPreferences.pagerNavInverted(), - title = stringResource(MR.strings.pref_read_with_tapping_inverted), - entries = persistentListOf( - ReaderPreferences.TappingInvertMode.NONE, - ReaderPreferences.TappingInvertMode.HORIZONTAL, - ReaderPreferences.TappingInvertMode.VERTICAL, - ReaderPreferences.TappingInvertMode.BOTH, - ) - .associateWith { stringResource(it.titleRes) } - .toImmutableMap(), - enabled = navMode != 5, - ), - Preference.PreferenceItem.ListPreference( - pref = imageScaleTypePref, - title = stringResource(MR.strings.pref_image_scale_type), - entries = ReaderPreferences.ImageScaleType - .mapIndexed { index, it -> index + 1 to stringResource(it) } - .toMap() - .toImmutableMap(), - ), - Preference.PreferenceItem.ListPreference( - pref = readerPreferences.zoomStart(), - title = stringResource(MR.strings.pref_zoom_start), - entries = ReaderPreferences.ZoomStart - .mapIndexed { index, it -> index + 1 to stringResource(it) } - .toMap() - .toImmutableMap(), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.cropBorders(), - title = stringResource(MR.strings.pref_crop_borders), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.landscapeZoom(), - title = stringResource(MR.strings.pref_landscape_zoom), - enabled = imageScaleType == 1, - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.navigateToPan(), - title = stringResource(MR.strings.pref_navigate_pan), - enabled = navMode != 5, - ), - Preference.PreferenceItem.SwitchPreference( - pref = dualPageSplitPref, - title = stringResource(MR.strings.pref_dual_page_split), - onValueChanged = { - rotateToFitPref.set(false) - true - }, - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.dualPageInvertPaged(), - title = stringResource(MR.strings.pref_dual_page_invert), - subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), - enabled = dualPageSplit, - ), - Preference.PreferenceItem.SwitchPreference( - pref = rotateToFitPref, - title = stringResource(MR.strings.pref_page_rotate), - onValueChanged = { - dualPageSplitPref.set(false) - true - }, - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.dualPageRotateToFitInvert(), - title = stringResource(MR.strings.pref_page_rotate_invert), - enabled = rotateToFit, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = navModePref, + title = stringResource(MR.strings.pref_viewer_nav), + entries = + ReaderPreferences.TapZones + .mapIndexed { index, it -> index to stringResource(it) } + .toMap() + .toImmutableMap(), + ), + Preference.PreferenceItem.ListPreference( + pref = readerPreferences.pagerNavInverted(), + title = stringResource(MR.strings.pref_read_with_tapping_inverted), + entries = + persistentListOf( + ReaderPreferences.TappingInvertMode.NONE, + ReaderPreferences.TappingInvertMode.HORIZONTAL, + ReaderPreferences.TappingInvertMode.VERTICAL, + ReaderPreferences.TappingInvertMode.BOTH, + ).associateWith { stringResource(it.titleRes) } + .toImmutableMap(), + enabled = navMode != 5, + ), + Preference.PreferenceItem.ListPreference( + pref = imageScaleTypePref, + title = stringResource(MR.strings.pref_image_scale_type), + entries = + ReaderPreferences.ImageScaleType + .mapIndexed { index, it -> index + 1 to stringResource(it) } + .toMap() + .toImmutableMap(), + ), + Preference.PreferenceItem.ListPreference( + pref = readerPreferences.zoomStart(), + title = stringResource(MR.strings.pref_zoom_start), + entries = + ReaderPreferences.ZoomStart + .mapIndexed { index, it -> index + 1 to stringResource(it) } + .toMap() + .toImmutableMap(), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.cropBorders(), + title = stringResource(MR.strings.pref_crop_borders), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.landscapeZoom(), + title = stringResource(MR.strings.pref_landscape_zoom), + enabled = imageScaleType == 1, + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.navigateToPan(), + title = stringResource(MR.strings.pref_navigate_pan), + enabled = navMode != 5, + ), + Preference.PreferenceItem.SwitchPreference( + pref = dualPageSplitPref, + title = stringResource(MR.strings.pref_dual_page_split), + onValueChanged = { + rotateToFitPref.set(false) + true + }, + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.dualPageInvertPaged(), + title = stringResource(MR.strings.pref_dual_page_invert), + subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), + enabled = dualPageSplit, + ), + Preference.PreferenceItem.SwitchPreference( + pref = rotateToFitPref, + title = stringResource(MR.strings.pref_page_rotate), + onValueChanged = { + dualPageSplitPref.set(false) + true + }, + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.dualPageRotateToFitInvert(), + title = stringResource(MR.strings.pref_page_rotate_invert), + enabled = rotateToFit, + ), ), - ), ) } @@ -316,89 +330,95 @@ object SettingsReaderScreen : SearchableSettings { return Preference.PreferenceGroup( title = stringResource(MR.strings.webtoon_viewer), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - pref = navModePref, - title = stringResource(MR.strings.pref_viewer_nav), - entries = ReaderPreferences.TapZones - .mapIndexed { index, it -> index to stringResource(it) } - .toMap() - .toImmutableMap(), - ), - Preference.PreferenceItem.ListPreference( - pref = readerPreferences.webtoonNavInverted(), - title = stringResource(MR.strings.pref_read_with_tapping_inverted), - entries = persistentListOf( - ReaderPreferences.TappingInvertMode.NONE, - ReaderPreferences.TappingInvertMode.HORIZONTAL, - ReaderPreferences.TappingInvertMode.VERTICAL, - ReaderPreferences.TappingInvertMode.BOTH, - ) - .associateWith { stringResource(it.titleRes) } - .toImmutableMap(), - enabled = navMode != 5, - ), - Preference.PreferenceItem.SliderPreference( - value = webtoonSidePadding, - title = stringResource(MR.strings.pref_webtoon_side_padding), - subtitle = numberFormat.format(webtoonSidePadding / 100f), - min = ReaderPreferences.WEBTOON_PADDING_MIN, - max = ReaderPreferences.WEBTOON_PADDING_MAX, - onValueChanged = { - webtoonSidePaddingPref.set(it) - true - }, - ), - Preference.PreferenceItem.ListPreference( - pref = readerPreferences.readerHideThreshold(), - title = stringResource(MR.strings.pref_hide_threshold), - entries = persistentMapOf( - ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest), - ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high), - ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low), - ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest), + preferenceItems = + persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = navModePref, + title = stringResource(MR.strings.pref_viewer_nav), + entries = + ReaderPreferences.TapZones + .mapIndexed { index, it -> index to stringResource(it) } + .toMap() + .toImmutableMap(), + ), + Preference.PreferenceItem.ListPreference( + pref = readerPreferences.webtoonNavInverted(), + title = stringResource(MR.strings.pref_read_with_tapping_inverted), + entries = + persistentListOf( + ReaderPreferences.TappingInvertMode.NONE, + ReaderPreferences.TappingInvertMode.HORIZONTAL, + ReaderPreferences.TappingInvertMode.VERTICAL, + ReaderPreferences.TappingInvertMode.BOTH, + ).associateWith { stringResource(it.titleRes) } + .toImmutableMap(), + enabled = navMode != 5, + ), + Preference.PreferenceItem.SliderPreference( + value = webtoonSidePadding, + title = stringResource(MR.strings.pref_webtoon_side_padding), + subtitle = numberFormat.format(webtoonSidePadding / 100f), + min = ReaderPreferences.WEBTOON_PADDING_MIN, + max = ReaderPreferences.WEBTOON_PADDING_MAX, + onValueChanged = { + webtoonSidePaddingPref.set(it) + true + }, + ), + Preference.PreferenceItem.ListPreference( + pref = readerPreferences.readerHideThreshold(), + title = stringResource(MR.strings.pref_hide_threshold), + entries = + persistentMapOf( + ReaderPreferences.ReaderHideThreshold.HIGHEST to + stringResource( + MR.strings.pref_highest, + ), + ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high), + ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low), + ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest), + ), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.cropBordersWebtoon(), + title = stringResource(MR.strings.pref_crop_borders), + ), + Preference.PreferenceItem.SwitchPreference( + pref = dualPageSplitPref, + title = stringResource(MR.strings.pref_dual_page_split), + onValueChanged = { + rotateToFitPref.set(false) + true + }, + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.dualPageInvertWebtoon(), + title = stringResource(MR.strings.pref_dual_page_invert), + subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), + enabled = dualPageSplit, + ), + Preference.PreferenceItem.SwitchPreference( + pref = rotateToFitPref, + title = stringResource(MR.strings.pref_page_rotate), + onValueChanged = { + dualPageSplitPref.set(false) + true + }, + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.dualPageRotateToFitInvertWebtoon(), + title = stringResource(MR.strings.pref_page_rotate_invert), + enabled = rotateToFit, + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.webtoonDoubleTapZoomEnabled(), + title = stringResource(MR.strings.pref_double_tap_zoom), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.webtoonDisableZoomOut(), + title = stringResource(MR.strings.pref_webtoon_disable_zoom_out), ), ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.cropBordersWebtoon(), - title = stringResource(MR.strings.pref_crop_borders), - ), - Preference.PreferenceItem.SwitchPreference( - pref = dualPageSplitPref, - title = stringResource(MR.strings.pref_dual_page_split), - onValueChanged = { - rotateToFitPref.set(false) - true - }, - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.dualPageInvertWebtoon(), - title = stringResource(MR.strings.pref_dual_page_invert), - subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), - enabled = dualPageSplit, - ), - Preference.PreferenceItem.SwitchPreference( - pref = rotateToFitPref, - title = stringResource(MR.strings.pref_page_rotate), - onValueChanged = { - dualPageSplitPref.set(false) - true - }, - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.dualPageRotateToFitInvertWebtoon(), - title = stringResource(MR.strings.pref_page_rotate_invert), - enabled = rotateToFit, - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.webtoonDoubleTapZoomEnabled(), - title = stringResource(MR.strings.pref_double_tap_zoom), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.webtoonDisableZoomOut(), - title = stringResource(MR.strings.pref_webtoon_disable_zoom_out), - ), - ), ) } @@ -408,35 +428,36 @@ object SettingsReaderScreen : SearchableSettings { val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState() return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_reader_navigation), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = readWithVolumeKeysPref, - title = stringResource(MR.strings.pref_read_with_volume_keys), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.readWithVolumeKeysInverted(), - title = stringResource(MR.strings.pref_read_with_volume_keys_inverted), - enabled = readWithVolumeKeys, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = readWithVolumeKeysPref, + title = stringResource(MR.strings.pref_read_with_volume_keys), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.readWithVolumeKeysInverted(), + title = stringResource(MR.strings.pref_read_with_volume_keys_inverted), + enabled = readWithVolumeKeys, + ), ), - ), ) } @Composable - private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup { - return Preference.PreferenceGroup( + private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup = + Preference.PreferenceGroup( title = stringResource(MR.strings.pref_reader_actions), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.readWithLongTap(), - title = stringResource(MR.strings.pref_read_with_long_tap), - ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.folderPerManga(), - title = stringResource(MR.strings.pref_create_folder_per_manga), - subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary), + preferenceItems = + persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.readWithLongTap(), + title = stringResource(MR.strings.pref_read_with_long_tap), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.folderPerManga(), + title = stringResource(MR.strings.pref_create_folder_per_manga), + subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary), + ), ), - ), ) - } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt index a173effc0b..f141d5ec05 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt @@ -60,7 +60,6 @@ import tachiyomi.presentation.core.util.runOnEnterKeyPressed import cafe.adriel.voyager.core.screen.Screen as VoyagerScreen class SettingsSearchScreen : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -105,12 +104,14 @@ class SettingsSearchScreen : Screen() { BasicTextField( value = textFieldValue, onValueChange = { textFieldValue = it }, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .runOnEnterKeyPressed(action = focusManager::clearFocus), - textStyle = MaterialTheme.typography.bodyLarge - .copy(color = MaterialTheme.colorScheme.onSurface), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + .runOnEnterKeyPressed(action = focusManager::clearFocus), + textStyle = + MaterialTheme.typography.bodyLarge + .copy(color = MaterialTheme.colorScheme.onSurface), singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }), @@ -169,50 +170,54 @@ private fun SearchResult( val index = getIndex() val result by produceState?>(initialValue = null, searchKey) { - value = index.asSequence() - .flatMap { settingsData -> - settingsData.contents.asSequence() - // Only search from enabled prefs and one with valid title - .filter { it.enabled && it.title.isNotBlank() } - // Flatten items contained inside *enabled* PreferenceGroup - .flatMap { p -> - when (p) { - is Preference.PreferenceGroup -> { - if (p.enabled) { - p.preferenceItems.asSequence() - .filter { it.enabled && it.title.isNotBlank() } - .map { p.title to it } - } else { - emptySequence() + value = + index + .asSequence() + .flatMap { settingsData -> + settingsData.contents + .asSequence() + // Only search from enabled prefs and one with valid title + .filter { it.enabled && it.title.isNotBlank() } + // Flatten items contained inside *enabled* PreferenceGroup + .flatMap { p -> + when (p) { + is Preference.PreferenceGroup -> { + if (p.enabled) { + p.preferenceItems + .asSequence() + .filter { it.enabled && it.title.isNotBlank() } + .map { p.title to it } + } else { + emptySequence() + } } + is Preference.PreferenceItem<*> -> sequenceOf(null to p) } - is Preference.PreferenceItem<*> -> sequenceOf(null to p) } - } - // Don't show info preference - .filterNot { it.second is Preference.PreferenceItem.InfoPreference } - // Filter by search query - .filter { (_, p) -> - val inTitle = p.title.contains(searchKey, true) - val inSummary = p.subtitle?.contains(searchKey, true) ?: false - inTitle || inSummary - } - // Map result data - .map { (categoryTitle, p) -> - SearchResultItem( - route = settingsData.route, - title = p.title, - breadcrumbs = getLocalizedBreadcrumb( - path = settingsData.title, - node = categoryTitle, - isLtr = isLtr, - ), - highlightKey = p.title, - ) - } - } - .take(10) // Just take top 10 result for quicker result - .toList() + // Don't show info preference + .filterNot { it.second is Preference.PreferenceItem.InfoPreference } + // Filter by search query + .filter { (_, p) -> + val inTitle = p.title.contains(searchKey, true) + val inSummary = p.subtitle?.contains(searchKey, true) ?: false + inTitle || inSummary + } + // Map result data + .map { (categoryTitle, p) -> + SearchResultItem( + route = settingsData.route, + title = p.title, + breadcrumbs = + getLocalizedBreadcrumb( + path = settingsData.title, + node = categoryTitle, + isLtr = isLtr, + ), + highlightKey = p.title, + ) + } + }.take(10) // Just take top 10 result for quicker result + .toList() } Crossfade( @@ -236,10 +241,11 @@ private fun SearchResult( key = { i -> i.hashCode() }, ) { item -> Column( - modifier = Modifier - .fillMaxWidth() - .clickable { onItemClick(item) } - .padding(horizontal = 24.dp, vertical = 14.dp), + modifier = + Modifier + .fillMaxWidth() + .clickable { onItemClick(item) } + .padding(horizontal = 24.dp, vertical = 14.dp), ) { Text( text = item.title, @@ -265,17 +271,22 @@ private fun SearchResult( @Composable @NonRestartableComposable -private fun getIndex() = settingScreens - .map { screen -> - SettingsData( - title = stringResource(screen.getTitleRes()), - route = screen, - contents = screen.getPreferences(), - ) - } +private fun getIndex() = + settingScreens + .map { screen -> + SettingsData( + title = stringResource(screen.getTitleRes()), + route = screen, + contents = screen.getPreferences(), + ) + } -private fun getLocalizedBreadcrumb(path: String, node: String?, isLtr: Boolean): String { - return if (node == null) { +private fun getLocalizedBreadcrumb( + path: String, + node: String?, + isLtr: Boolean, +): String = + if (node == null) { path } else { if (isLtr) { @@ -286,19 +297,19 @@ private fun getLocalizedBreadcrumb(path: String, node: String?, isLtr: Boolean): "$node < $path" } } -} -private val settingScreens = listOf( - SettingsAppearanceScreen, - SettingsLibraryScreen, - SettingsReaderScreen, - SettingsDownloadScreen, - SettingsTrackingScreen, - SettingsBrowseScreen, - SettingsDataScreen, - SettingsSecurityScreen, - SettingsAdvancedScreen, -) +private val settingScreens = + listOf( + SettingsAppearanceScreen, + SettingsLibraryScreen, + SettingsReaderScreen, + SettingsDownloadScreen, + SettingsTrackingScreen, + SettingsBrowseScreen, + SettingsDataScreen, + SettingsSecurityScreen, + SettingsAdvancedScreen, + ) private data class SettingsData( val title: String, 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 1d05100253..ea01095068 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 @@ -21,7 +21,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object SettingsSecurityScreen : SearchableSettings { - @ReadOnlyComposable @Composable override fun getTitleRes() = MR.strings.pref_category_security @@ -50,15 +49,15 @@ object SettingsSecurityScreen : SearchableSettings { pref = securityPreferences.lockAppAfter(), title = stringResource(MR.strings.lock_when_idle), enabled = authSupported && useAuth, - entries = LockAfterValues - .associateWith { - when (it) { - -1 -> stringResource(MR.strings.lock_never) - 0 -> stringResource(MR.strings.lock_always) - else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it) - } - } - .toImmutableMap(), + entries = + LockAfterValues + .associateWith { + when (it) { + -1 -> stringResource(MR.strings.lock_never) + 0 -> stringResource(MR.strings.lock_always) + else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it) + } + }.toImmutableMap(), onValueChanged = { (context as FragmentActivity).authenticate( title = context.stringResource(MR.strings.lock_when_idle), @@ -72,20 +71,22 @@ object SettingsSecurityScreen : SearchableSettings { Preference.PreferenceItem.ListPreference( pref = securityPreferences.secureScreen(), title = stringResource(MR.strings.secure_screen), - entries = SecurityPreferences.SecureScreenMode.entries - .associateWith { stringResource(it.titleRes) } - .toImmutableMap(), + entries = + SecurityPreferences.SecureScreenMode.entries + .associateWith { stringResource(it.titleRes) } + .toImmutableMap(), ), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)), ) } } -private val LockAfterValues = persistentListOf( - 0, // Always - 1, - 2, - 5, - 10, - -1, // Never -) +private val LockAfterValues = + persistentListOf( + 0, // Always + 1, + 2, + 5, + 10, + -1, // Never + ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index 021f0ceb20..114e1274a2 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -63,7 +63,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object SettingsTrackingScreen : SearchableSettings { - @ReadOnlyComposable @Composable override fun getTitleRes() = MR.strings.pref_category_tracking @@ -105,18 +104,20 @@ object SettingsTrackingScreen : SearchableSettings { } } - val enhancedTrackers = trackerManager.trackers - .filter { it is EnhancedTracker } - .partition { service -> - val acceptedSources = (service as EnhancedTracker).getAcceptedSources() - sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources } - } + val enhancedTrackers = + trackerManager.trackers + .filter { it is EnhancedTracker } + .partition { service -> + val acceptedSources = (service as EnhancedTracker).getAcceptedSources() + sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources } + } var enhancedTrackerInfo = stringResource(MR.strings.enhanced_tracking_info) if (enhancedTrackers.second.isNotEmpty()) { - val missingSourcesInfo = stringResource( - MR.strings.enhanced_services_not_installed, - enhancedTrackers.second.joinToString { it.name }, - ) + val missingSourcesInfo = + stringResource( + MR.strings.enhanced_services_not_installed, + enhancedTrackers.second.joinToString { it.name }, + ) enhancedTrackerInfo += "\n\n$missingSourcesInfo" } @@ -127,58 +128,60 @@ object SettingsTrackingScreen : SearchableSettings { ), Preference.PreferenceGroup( title = stringResource(MR.strings.services), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TrackerPreference( - title = trackerManager.myAnimeList.name, - tracker = trackerManager.myAnimeList, - login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) }, - logout = { dialog = LogoutDialog(trackerManager.myAnimeList) }, - ), - Preference.PreferenceItem.TrackerPreference( - title = trackerManager.aniList.name, - tracker = trackerManager.aniList, - login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) }, - logout = { dialog = LogoutDialog(trackerManager.aniList) }, - ), - Preference.PreferenceItem.TrackerPreference( - title = trackerManager.kitsu.name, - tracker = trackerManager.kitsu, - login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) }, - logout = { dialog = LogoutDialog(trackerManager.kitsu) }, - ), - Preference.PreferenceItem.TrackerPreference( - title = trackerManager.mangaUpdates.name, - tracker = trackerManager.mangaUpdates, - login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) }, - logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) }, - ), - Preference.PreferenceItem.TrackerPreference( - title = trackerManager.shikimori.name, - tracker = trackerManager.shikimori, - login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) }, - logout = { dialog = LogoutDialog(trackerManager.shikimori) }, - ), - Preference.PreferenceItem.TrackerPreference( - title = trackerManager.bangumi.name, - tracker = trackerManager.bangumi, - login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) }, - logout = { dialog = LogoutDialog(trackerManager.bangumi) }, + preferenceItems = + persistentListOf( + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.myAnimeList.name, + tracker = trackerManager.myAnimeList, + login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) }, + logout = { dialog = LogoutDialog(trackerManager.myAnimeList) }, + ), + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.aniList.name, + tracker = trackerManager.aniList, + login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) }, + logout = { dialog = LogoutDialog(trackerManager.aniList) }, + ), + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.kitsu.name, + tracker = trackerManager.kitsu, + login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) }, + logout = { dialog = LogoutDialog(trackerManager.kitsu) }, + ), + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.mangaUpdates.name, + tracker = trackerManager.mangaUpdates, + login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) }, + logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) }, + ), + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.shikimori.name, + tracker = trackerManager.shikimori, + login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) }, + logout = { dialog = LogoutDialog(trackerManager.shikimori) }, + ), + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.bangumi.name, + tracker = trackerManager.bangumi, + login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) }, + logout = { dialog = LogoutDialog(trackerManager.bangumi) }, + ), + Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.tracking_info)), ), - Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.tracking_info)), - ), ), Preference.PreferenceGroup( title = stringResource(MR.strings.enhanced_services), - preferenceItems = ( - enhancedTrackers.first - .map { service -> - Preference.PreferenceItem.TrackerPreference( - title = service.name, - tracker = service, - login = { (service as EnhancedTracker).loginNoop() }, - logout = service::logout, - ) - } + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo)) + preferenceItems = + ( + enhancedTrackers.first + .map { service -> + Preference.PreferenceItem.TrackerPreference( + title = service.name, + tracker = service, + login = { (service as EnhancedTracker).loginNoop() }, + logout = service::logout, + ) + } + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo)) ).toImmutableList(), ), ) @@ -235,24 +238,27 @@ object SettingsTrackingScreen : SearchableSettings { trailingIcon = { IconButton(onClick = { hidePassword = !hidePassword }) { Icon( - imageVector = if (hidePassword) { - Icons.Filled.Visibility - } else { - Icons.Filled.VisibilityOff - }, + imageVector = + if (hidePassword) { + Icons.Filled.Visibility + } else { + Icons.Filled.VisibilityOff + }, contentDescription = null, ) } }, - visualTransformation = if (hidePassword) { - PasswordVisualTransformation() - } else { - VisualTransformation.None - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, - ), + visualTransformation = + if (hidePassword) { + PasswordVisualTransformation() + } else { + VisualTransformation.None + }, + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), singleLine = true, isError = inputError && !processing, ) @@ -265,12 +271,13 @@ object SettingsTrackingScreen : SearchableSettings { onClick = { scope.launchIO { processing = true - val result = checkLogin( - context = context, - tracker = tracker, - username = username.text, - password = password.text, - ) + val result = + checkLogin( + context = context, + tracker = tracker, + username = username.text, + password = password.text, + ) inputError = !result if (result) onDismissRequest() processing = false @@ -289,8 +296,8 @@ object SettingsTrackingScreen : SearchableSettings { tracker: Tracker, username: String, password: String, - ): Boolean { - return try { + ): Boolean = + try { tracker.login(username, password) withUIContext { context.toast(MR.strings.login_success) } true @@ -299,7 +306,6 @@ object SettingsTrackingScreen : SearchableSettings { withUIContext { context.toast(e.message.toString()) } false } - } @Composable private fun TrackingLogoutDialog( @@ -331,10 +337,11 @@ object SettingsTrackingScreen : SearchableSettings { onDismissRequest() context.toast(MR.strings.logout_success) }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError, - ), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), ) { Text(text = stringResource(MR.strings.logout)) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt index e91a0576c7..eeec356d1e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt @@ -60,7 +60,6 @@ import java.time.LocalDateTime import java.time.ZoneId object AboutScreen : Screen() { - @Composable override fun Content() { val scope = rememberCoroutineScope() @@ -117,12 +116,13 @@ object AboutScreen : Screen() { checkVersion( context = context, onAvailableUpdate = { result -> - val updateScreen = NewUpdateScreen( - versionName = result.release.version, - changelogInfo = result.release.info, - releaseLink = result.release.releaseLink, - downloadLink = result.release.getDownloadLink(), - ) + val updateScreen = + NewUpdateScreen( + versionName = result.release.version, + changelogInfo = result.release.info, + releaseLink = result.release.releaseLink, + downloadLink = result.release.getDownloadLink(), + ) navigator.push(updateScreen) }, onFinish = { @@ -161,9 +161,10 @@ object AboutScreen : Screen() { item { Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), horizontalArrangement = Arrangement.Center, ) { LinkIcon( @@ -234,8 +235,8 @@ object AboutScreen : Screen() { } } - fun getVersionName(withBuildDate: Boolean): String { - return when { + fun getVersionName(withBuildDate: Boolean): String = + when { BuildConfig.DEBUG -> { "Debug ${BuildConfig.COMMIT_SHA}".let { if (withBuildDate) { @@ -264,15 +265,14 @@ object AboutScreen : Screen() { } } } - } - internal fun getFormattedBuildTime(): String { - return try { - LocalDateTime.ofInstant( - Instant.parse(BuildConfig.BUILD_TIME), - ZoneId.systemDefault(), - ) - .toDateTimestampString( + internal fun getFormattedBuildTime(): String = + try { + LocalDateTime + .ofInstant( + Instant.parse(BuildConfig.BUILD_TIME), + ZoneId.systemDefault(), + ).toDateTimestampString( UiPreferences.dateFormat( Injekt.get().dateFormat().get(), ), @@ -280,5 +280,4 @@ object AboutScreen : Screen() { } catch (e: Exception) { BuildConfig.BUILD_TIME } - } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLibraryLicenseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLibraryLicenseScreen.kt index 725ed64078..a2fd1a83e2 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLibraryLicenseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLibraryLicenseScreen.kt @@ -28,7 +28,6 @@ class OpenSourceLibraryLicenseScreen( private val website: String?, private val license: String, ) : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -57,10 +56,11 @@ class OpenSourceLibraryLicenseScreen( }, ) { contentPadding -> Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(contentPadding) - .padding(16.dp), + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .padding(contentPadding) + .padding(16.dp), ) { HtmlLicenseText(html = license) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt index 3c2309b322..30f65cff7a 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt @@ -14,7 +14,6 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource class OpenSourceLicensesScreen : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -28,16 +27,21 @@ class OpenSourceLicensesScreen : Screen() { }, ) { contentPadding -> LibrariesContainer( - modifier = Modifier - .fillMaxSize(), + modifier = + Modifier + .fillMaxSize(), contentPadding = contentPadding, onLibraryClick = { navigator.push( OpenSourceLibraryLicenseScreen( name = it.name, website = it.website, - license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(), - ) + license = + it.licenses + .firstOrNull() + ?.htmlReadyLicenseContent + .orEmpty(), + ), ) }, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt index eaaec95855..825812aeff 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt @@ -55,7 +55,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class ClearDatabaseScreen : Screen() { - @Composable override fun Content() { val context = LocalContext.current @@ -103,18 +102,19 @@ class ClearDatabaseScreen : Screen() { actions = { if (s.items.isNotEmpty()) { AppBarActions( - actions = persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_select_all), - icon = Icons.Outlined.SelectAll, - onClick = model::selectAll, - ), - AppBar.Action( - title = stringResource(MR.strings.action_select_inverse), - icon = Icons.Outlined.FlipToBack, - onClick = model::invertSelection, + actions = + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_select_all), + icon = Icons.Outlined.SelectAll, + onClick = model::selectAll, + ), + AppBar.Action( + title = stringResource(MR.strings.action_select_inverse), + icon = Icons.Outlined.FlipToBack, + onClick = model::invertSelection, + ), ), - ), ) } }, @@ -157,18 +157,20 @@ class ClearDatabaseScreen : Screen() { onClickSelect: () -> Unit, ) { Row( - modifier = Modifier - .selectedBackground(isSelected) - .clickable(onClick = onClickSelect) - .padding(horizontal = 8.dp) - .height(56.dp), + modifier = + Modifier + .selectedBackground(isSelected) + .clickable(onClick = onClickSelect) + .padding(horizontal = 8.dp) + .height(56.dp), verticalAlignment = Alignment.CenterVertically, ) { SourceIcon(source = source) Column( - modifier = Modifier - .padding(start = 8.dp) - .weight(1f), + modifier = + Modifier + .padding(start = 8.dp) + .weight(1f), ) { Text( text = source.visualName, @@ -190,7 +192,8 @@ private class ClearDatabaseScreenModel : StateScreenModel mutableState.update { old -> val items = list.sortedBy { it.name } @@ -203,51 +206,59 @@ private class ClearDatabaseScreenModel : StateScreenModel - if (state !is State.Ready) return@update state - val mutableList = state.selection.toMutableList() - if (mutableList.contains(source.id)) { - mutableList.remove(source.id) - } else { - mutableList.add(source.id) + fun toggleSelection(source: Source) = + mutableState.update { state -> + if (state !is State.Ready) return@update state + val mutableList = state.selection.toMutableList() + if (mutableList.contains(source.id)) { + mutableList.remove(source.id) + } else { + mutableList.add(source.id) + } + state.copy(selection = mutableList) } - state.copy(selection = mutableList) - } - fun clearSelection() = mutableState.update { state -> - if (state !is State.Ready) return@update state - state.copy(selection = emptyList()) - } + fun clearSelection() = + mutableState.update { state -> + if (state !is State.Ready) return@update state + state.copy(selection = emptyList()) + } - fun selectAll() = mutableState.update { state -> - if (state !is State.Ready) return@update state - state.copy(selection = state.items.fastMap { it.id }) - } + fun selectAll() = + mutableState.update { state -> + if (state !is State.Ready) return@update state + state.copy(selection = state.items.fastMap { it.id }) + } - fun invertSelection() = mutableState.update { state -> - if (state !is State.Ready) return@update state - state.copy( - selection = state.items - .fastMap { it.id } - .filterNot { it in state.selection }, - ) - } + fun invertSelection() = + mutableState.update { state -> + if (state !is State.Ready) return@update state + state.copy( + selection = + state.items + .fastMap { it.id } + .filterNot { it in state.selection }, + ) + } - fun showConfirmation() = mutableState.update { state -> - if (state !is State.Ready) return@update state - state.copy(showConfirmation = true) - } + fun showConfirmation() = + mutableState.update { state -> + if (state !is State.Ready) return@update state + state.copy(showConfirmation = true) + } - fun hideConfirmation() = mutableState.update { state -> - if (state !is State.Ready) return@update state - state.copy(showConfirmation = false) - } + fun hideConfirmation() = + mutableState.update { state -> + if (state !is State.Ready) return@update state + state.copy(showConfirmation = false) + } sealed interface State { @Immutable diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppLanguageScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppLanguageScreen.kt index b59b26acac..1c14040d60 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppLanguageScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppLanguageScreen.kt @@ -36,7 +36,6 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource class AppLanguageScreen : Screen() { - @Composable override fun Content() { val context = LocalContext.current @@ -48,11 +47,12 @@ class AppLanguageScreen : Screen() { } LaunchedEffect(currentLanguage) { - val locale = if (currentLanguage.isEmpty()) { - LocaleListCompat.getEmptyLocaleList() - } else { - LocaleListCompat.forLanguageTags(currentLanguage) - } + val locale = + if (currentLanguage.isEmpty()) { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(currentLanguage) + } AppCompatDelegate.setApplicationLocales(locale) } @@ -70,9 +70,10 @@ class AppLanguageScreen : Screen() { ) { items(langs) { ListItem( - modifier = Modifier.clickable { - currentLanguage = it.langTag - }, + modifier = + Modifier.clickable { + currentLanguage = it.langTag + }, headlineContent = { Text(it.displayName) }, supportingContent = { it.localizedDisplayName?.let { 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/ExtensionReposScreen.kt index 03c9acd7c7..50eb8e7008 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/ExtensionReposScreen.kt @@ -22,7 +22,6 @@ import tachiyomi.presentation.core.screens.LoadingScreen class ExtensionReposScreen( private val url: String? = null, ) : Screen() { - @Composable override fun Content() { val context = LocalContext.current 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 index 4131bc6fd3..14288ca4fa 100644 --- 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 @@ -28,13 +28,13 @@ class ExtensionReposScreenModel( private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(), private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(), ) : StateScreenModel(RepoScreenState.Loading) { - private val _events: Channel = Channel(Int.MAX_VALUE) val events = _events.receiveAsFlow() init { screenModelScope.launchIO { - getExtensionRepo.subscribeAll() + getExtensionRepo + .subscribeAll() .collectLatest { repos -> mutableState.update { RepoScreenState.Success( @@ -116,19 +116,29 @@ class ExtensionReposScreenModel( } sealed class RepoEvent { - sealed class LocalizedMessage(val stringRes: StringResource) : 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 Delete( + val repo: String, + ) : RepoDialog() + + data class Conflict( + val oldRepo: ExtensionRepo, + val newRepo: ExtensionRepo, + ) : RepoDialog() } sealed class RepoScreenState { - @Immutable data object Loading : RepoScreenState() @@ -138,7 +148,6 @@ sealed class RepoScreenState { val oldRepos: ImmutableSet? = null, val dialog: RepoDialog? = null, ) : RepoScreenState() { - val isEmpty: Boolean get() = repos.isEmpty() } 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/ExtensionReposContent.kt index e6b2a227f6..2cee42f3f1 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/ExtensionReposContent.kt @@ -69,13 +69,14 @@ private fun ExtensionRepoListItem( modifier = modifier, ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding( - start = MaterialTheme.padding.medium, - top = MaterialTheme.padding.medium, - end = MaterialTheme.padding.medium, - ), + modifier = + Modifier + .fillMaxWidth() + .padding( + start = MaterialTheme.padding.medium, + top = MaterialTheme.padding.medium, + end = MaterialTheme.padding.medium, + ), verticalAlignment = Alignment.CenterVertically, ) { Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null) 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 index 239f917d0b..96ba185e17 100644 --- 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 @@ -60,19 +60,21 @@ fun ExtensionRepoCreateDialog( Text(text = stringResource(MR.strings.action_add_repo_message)) OutlinedTextField( - modifier = Modifier - .focusRequester(focusRequester), + modifier = + Modifier + .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 - } + 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, 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/ExtensionReposScreen.kt index b07ba4101e..a0c41ca79c 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/ExtensionReposScreen.kt @@ -68,8 +68,9 @@ fun ExtensionReposScreen( ExtensionReposContent( repos = state.repos, lazyListState = lazyListState, - paddingValues = paddingValues + topSmallPaddingValues + - PaddingValues(horizontal = MaterialTheme.padding.medium), + paddingValues = + paddingValues + topSmallPaddingValues + + PaddingValues(horizontal = MaterialTheme.padding.medium), onOpenWebsite = onOpenWebsite, onClickDelete = onClickDelete, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index a45fd374e5..68cd748305 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -34,7 +34,6 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource class CreateBackupScreen : Screen() { - @Composable override fun Content() { val context = LocalContext.current @@ -42,19 +41,20 @@ class CreateBackupScreen : Screen() { val model = rememberScreenModel { CreateBackupScreenModel() } val state by model.state.collectAsState() - val chooseBackupDir = rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument("application/*"), - ) { - if (it != null) { - context.contentResolver.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION, - ) - model.createBackup(context, it) - navigator.pop() + val chooseBackupDir = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/*"), + ) { + if (it != null) { + context.contentResolver.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + ) + model.createBackup(context, it) + navigator.pop() + } } - } Scaffold( topBar = { @@ -122,8 +122,10 @@ class CreateBackupScreen : Screen() { } private class CreateBackupScreenModel : StateScreenModel(State()) { - - fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) { + fun toggle( + setter: (BackupOptions, Boolean) -> BackupOptions, + enabled: Boolean, + ) { mutableState.update { it.copy( options = setter(it.options, enabled), @@ -131,7 +133,10 @@ private class CreateBackupScreenModel : StateScreenModel { - appendLine(stringResource(MR.strings.backup_restore_content_full)) - if (error.sources.isNotEmpty()) { - appendLine() - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - appendLine(stringResource(MR.strings.backup_restore_missing_sources)) + val msg = + buildAnnotatedString { + when (error) { + is MissingRestoreComponents -> { + appendLine(stringResource(MR.strings.backup_restore_content_full)) + if (error.sources.isNotEmpty()) { + appendLine() + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.backup_restore_missing_sources)) + } + error.sources.joinTo( + this, + separator = "\n- ", + prefix = "- ", + ) } - error.sources.joinTo( - this, - separator = "\n- ", - prefix = "- ", - ) - } - if (error.trackers.isNotEmpty()) { - appendLine() - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - appendLine(stringResource(MR.strings.backup_restore_missing_trackers)) + if (error.trackers.isNotEmpty()) { + appendLine() + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.backup_restore_missing_trackers)) + } + error.trackers.joinTo( + this, + separator = "\n- ", + prefix = "- ", + ) } - error.trackers.joinTo( - this, - separator = "\n- ", - prefix = "- ", - ) } - } - is InvalidRestore -> { - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - appendLine(stringResource(MR.strings.invalid_backup_file)) - } - appendLine(error.uri.toString()) + is InvalidRestore -> { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.invalid_backup_file)) + } + appendLine(error.uri.toString()) - appendLine() + appendLine() - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - appendLine(stringResource(MR.strings.invalid_backup_file_error)) + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.invalid_backup_file_error)) + } + appendLine(error.message) } - appendLine(error.message) - } - else -> { - appendLine(error.toString()) + else -> { + appendLine(error.toString()) + } } } - } SelectionContainer { Text(text = msg) @@ -168,12 +166,14 @@ private class RestoreBackupScreenModel( private val context: Context, private val uri: String, ) : StateScreenModel(State()) { - init { validate(uri.toUri()) } - fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { + fun toggle( + setter: (RestoreOptions, Boolean) -> RestoreOptions, + enabled: Boolean, + ) { mutableState.update { it.copy( options = setter(it.options, enabled), @@ -190,15 +190,16 @@ private class RestoreBackupScreenModel( } private fun validate(uri: Uri) { - val results = try { - BackupFileValidator(context).validate(uri) - } catch (e: Exception) { - setError( - error = InvalidRestore(uri, e.message.toString()), - canRestore = false, - ) - return - } + val results = + try { + BackupFileValidator(context).validate(uri) + } catch (e: Exception) { + setError( + error = InvalidRestore(uri, e.message.toString()), + canRestore = false, + ) + return + } if (results.missingSources.isNotEmpty() || results.missingTrackers.isNotEmpty()) { setError( @@ -211,7 +212,10 @@ private class RestoreBackupScreenModel( setError(error = null, canRestore = true) } - private fun setError(error: Any?, canRestore: Boolean) { + private fun setError( + error: Any?, + canRestore: Boolean, + ) { mutableState.update { it.copy( error = error, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt index 5fed6c6ef0..80d4713db3 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt @@ -23,9 +23,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha import java.io.File @Composable -fun StorageInfo( - modifier: Modifier = Modifier, -) { +fun StorageInfo(modifier: Modifier = Modifier) { val context = LocalContext.current val storages = remember { DiskUtil.getExternalStorages(context) } @@ -40,9 +38,7 @@ fun StorageInfo( } @Composable -private fun StorageInfo( - file: File, -) { +private fun StorageInfo(file: File) { val context = LocalContext.current val available = remember(file) { DiskUtil.getAvailableStorageSpace(file) } @@ -59,10 +55,11 @@ private fun StorageInfo( ) LinearProgressIndicator( - modifier = Modifier - .clip(MaterialTheme.shapes.small) - .fillMaxWidth() - .height(12.dp), + modifier = + Modifier + .clip(MaterialTheme.shapes.small) + .fillMaxWidth() + .height(12.dp), progress = { (1 - (available / total.toFloat())) }, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/BackupSchemaScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/BackupSchemaScreen.kt index d5652b16ac..b7437b80e6 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/BackupSchemaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/BackupSchemaScreen.kt @@ -26,7 +26,6 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource class BackupSchemaScreen : Screen() { - companion object { const val title = "Backup file schema" } @@ -62,10 +61,11 @@ class BackupSchemaScreen : Screen() { ) { contentPadding -> Text( text = schema, - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(contentPadding) - .padding(16.dp), + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .padding(contentPadding) + .padding(16.dp), fontFamily = FontFamily.Monospace, ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt index 0db4bd3c7c..390421a77f 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.guava.await import tachiyomi.i18n.MR class DebugInfoScreen : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -46,50 +45,49 @@ class DebugInfoScreen : Screen() { } @Composable - private fun getAppInfoGroup(): Preference.PreferenceGroup { - return Preference.PreferenceGroup( + private fun getAppInfoGroup(): Preference.PreferenceGroup = + Preference.PreferenceGroup( title = "App info", - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = "Version", - subtitle = AboutScreen.getVersionName(false), - ), - Preference.PreferenceItem.TextPreference( - title = "Build time", - subtitle = AboutScreen.getFormattedBuildTime(), - ), - getProfileVerifierPreference(), - Preference.PreferenceItem.TextPreference( - title = "WebView version", - subtitle = getWebViewVersion(), + preferenceItems = + persistentListOf( + Preference.PreferenceItem.TextPreference( + title = "Version", + subtitle = AboutScreen.getVersionName(false), + ), + Preference.PreferenceItem.TextPreference( + title = "Build time", + subtitle = AboutScreen.getFormattedBuildTime(), + ), + getProfileVerifierPreference(), + Preference.PreferenceItem.TextPreference( + title = "WebView version", + subtitle = getWebViewVersion(), + ), ), - ), ) - } @Composable @ReadOnlyComposable - private fun getWebViewVersion(): String { - return WebViewUtil.getVersion(LocalContext.current) - } + private fun getWebViewVersion(): String = WebViewUtil.getVersion(LocalContext.current) @Composable private fun getProfileVerifierPreference(): Preference.PreferenceItem.TextPreference { val status by produceState(initialValue = "-") { val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode - value = when (result) { - ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed" - ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled" - ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING -> - "Compiled non-matching" - ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ, - ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE, - ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST, - -> "Error $result" - ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION -> "Not supported" - ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION -> "Pending compilation" - else -> "Unknown code $result" - } + value = + when (result) { + ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed" + ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled" + ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING -> + "Compiled non-matching" + ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ, + ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE, + ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST, + -> "Error $result" + ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION -> "Not supported" + ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION -> "Pending compilation" + else -> "Unknown code $result" + } } return Preference.PreferenceItem.TextPreference( title = "Profile compilation status", @@ -98,45 +96,47 @@ class DebugInfoScreen : Screen() { } private fun getDeviceInfoGroup(): Preference.PreferenceGroup { - val items = persistentListOf>().mutate { - it.add( - Preference.PreferenceItem.TextPreference( - title = "Model", - subtitle = "${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})", - ), - ) - - if (DeviceUtil.oneUiVersion != null) { + val items = + persistentListOf>().mutate { it.add( Preference.PreferenceItem.TextPreference( - title = "OneUI version", - subtitle = "${DeviceUtil.oneUiVersion}", + title = "Model", + subtitle = "${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})", ), ) - } else if (DeviceUtil.miuiMajorVersion != null) { + + if (DeviceUtil.oneUiVersion != null) { + it.add( + Preference.PreferenceItem.TextPreference( + title = "OneUI version", + subtitle = "${DeviceUtil.oneUiVersion}", + ), + ) + } else if (DeviceUtil.miuiMajorVersion != null) { + it.add( + Preference.PreferenceItem.TextPreference( + title = "MIUI version", + subtitle = "${DeviceUtil.miuiMajorVersion}", + ), + ) + } + + val androidVersion = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Build.VERSION.RELEASE_OR_PREVIEW_DISPLAY + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Build.VERSION.RELEASE_OR_CODENAME + } else { + Build.VERSION.RELEASE + } it.add( Preference.PreferenceItem.TextPreference( - title = "MIUI version", - subtitle = "${DeviceUtil.miuiMajorVersion}", + title = "Android version", + subtitle = "$androidVersion (${Build.DISPLAY})", ), ) } - val androidVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Build.VERSION.RELEASE_OR_PREVIEW_DISPLAY - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Build.VERSION.RELEASE_OR_CODENAME - } else { - Build.VERSION.RELEASE - } - it.add( - Preference.PreferenceItem.TextPreference( - title = "Android version", - subtitle = "$androidVersion (${Build.DISPLAY})", - ), - ) - } - return Preference.PreferenceGroup( title = "Device info", preferenceItems = items, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt index 4a57103d85..de7e9242cb 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt @@ -47,7 +47,6 @@ import java.time.LocalDateTime import java.time.ZoneId class WorkerInfoScreen : Screen() { - companion object { const val title = "Worker info" } @@ -118,53 +117,59 @@ class WorkerInfoScreen : Screen() { ) } - private class Model(context: Context) : ScreenModel { + private class Model( + context: Context, + ) : ScreenModel { private val workManager = context.workManager - val finished = workManager - .getWorkInfosFlow( - WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED), - ) - .map(::constructString) - .stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") + val finished = + workManager + .getWorkInfosFlow( + WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED), + ).map(::constructString) + .stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") - val running = workManager - .getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING)) - .map(::constructString) - .stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") + val running = + workManager + .getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING)) + .map(::constructString) + .stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") - val enqueued = workManager - .getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED)) - .map(::constructString) - .stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") + val enqueued = + workManager + .getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED)) + .map(::constructString) + .stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") - private fun constructString(list: List) = buildString { - if (list.isEmpty()) { - appendLine("-") - } else { - list.fastForEach { workInfo -> - appendLine("Id: ${workInfo.id}") - appendLine("Tags:") - workInfo.tags.forEach { - appendLine(" - $it") + private fun constructString(list: List) = + buildString { + if (list.isEmpty()) { + appendLine("-") + } else { + list.fastForEach { workInfo -> + appendLine("Id: ${workInfo.id}") + appendLine("Tags:") + workInfo.tags.forEach { + appendLine(" - $it") + } + appendLine("State: ${workInfo.state}") + if (workInfo.state == WorkInfo.State.ENQUEUED) { + val timestamp = + LocalDateTime + .ofInstant( + Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis), + ZoneId.systemDefault(), + ).toDateTimestampString( + UiPreferences.dateFormat( + Injekt.get().dateFormat().get(), + ), + ) + appendLine("Next scheduled run: $timestamp") + appendLine("Attempt #${workInfo.runAttemptCount + 1}") + } + appendLine() } - appendLine("State: ${workInfo.state}") - if (workInfo.state == WorkInfo.State.ENQUEUED) { - val timestamp = LocalDateTime.ofInstant( - Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis), - ZoneId.systemDefault(), - ) - .toDateTimestampString( - UiPreferences.dateFormat( - Injekt.get().dateFormat().get(), - ), - ) - appendLine("Next scheduled run: $timestamp",) - appendLine("Attempt #${workInfo.runAttemptCount + 1}") - } - appendLine() } } - } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemeModePreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemeModePreferenceWidget.kt index 6ba423e32a..f2db676f7f 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemeModePreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemeModePreferenceWidget.kt @@ -12,11 +12,12 @@ import eu.kanade.domain.ui.model.ThemeMode import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -private val options = mapOf( - ThemeMode.SYSTEM to MR.strings.theme_system, - ThemeMode.LIGHT to MR.strings.theme_light, - ThemeMode.DARK to MR.strings.theme_dark, -) +private val options = + mapOf( + ThemeMode.SYSTEM to MR.strings.theme_system, + ThemeMode.LIGHT to MR.strings.theme_light, + ThemeMode.DARK to MR.strings.theme_dark, + ) @Composable internal fun AppThemeModePreferenceWidget( @@ -26,18 +27,20 @@ internal fun AppThemeModePreferenceWidget( BasePreferenceWidget( subcomponent = { MultiChoiceSegmentedButtonRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PrefsHorizontalPadding), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = PrefsHorizontalPadding), ) { options.onEachIndexed { index, (mode, labelRes) -> SegmentedButton( checked = mode == value, onCheckedChange = { onItemClick(mode) }, - shape = SegmentedButtonDefaults.itemShape( - index, - options.size, - ), + shape = + SegmentedButtonDefaults.itemShape( + index, + options.size, + ), ) { Text(stringResource(labelRes)) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt index 5e3f76efe6..bb0f87e427 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt @@ -80,10 +80,11 @@ private fun AppThemesList( onItemClick: (AppTheme) -> Unit, ) { val context = LocalContext.current - val appThemes = remember { - AppTheme.entries - .filterNot { it.titleRes == null || (it == AppTheme.MONET && !DeviceUtil.isDynamicColorAvailable) } - } + val appThemes = + remember { + AppTheme.entries + .filterNot { it.titleRes == null || (it == AppTheme.MONET && !DeviceUtil.isDynamicColorAvailable) } + } LazyRow( contentPadding = PaddingValues(horizontal = PrefsHorizontalPadding), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), @@ -93,9 +94,10 @@ private fun AppThemesList( key = { it.name }, ) { appTheme -> Column( - modifier = Modifier - .width(114.dp) - .padding(top = 8.dp), + modifier = + Modifier + .width(114.dp) + .padding(top = 8.dp), ) { TachiyomiTheme( appTheme = appTheme, @@ -114,9 +116,10 @@ private fun AppThemesList( Text( text = stringResource(appTheme.titleRes!!), - modifier = Modifier - .fillMaxWidth() - .secondaryItemAlpha(), + modifier = + Modifier + .fillMaxWidth() + .secondaryItemAlpha(), textAlign = TextAlign.Center, maxLines = 2, minLines = 2, @@ -133,40 +136,43 @@ fun AppThemePreviewItem( onClick: () -> Unit, ) { Column( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(9f / 16f) - .border( - width = 4.dp, - color = if (selected) { - MaterialTheme.colorScheme.primary - } else { - DividerDefaults.color - }, - shape = RoundedCornerShape(17.dp), - ) - .padding(4.dp) - .clip(RoundedCornerShape(13.dp)) - .background(MaterialTheme.colorScheme.background) - .clickable(onClick = onClick), + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(9f / 16f) + .border( + width = 4.dp, + color = + if (selected) { + MaterialTheme.colorScheme.primary + } else { + DividerDefaults.color + }, + shape = RoundedCornerShape(17.dp), + ).padding(4.dp) + .clip(RoundedCornerShape(13.dp)) + .background(MaterialTheme.colorScheme.background) + .clickable(onClick = onClick), ) { // App Bar Row( - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - .padding(8.dp), + modifier = + Modifier + .fillMaxWidth() + .height(40.dp) + .padding(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Box( - modifier = Modifier - .fillMaxHeight(0.8f) - .weight(0.7f) - .padding(end = 4.dp) - .background( - color = MaterialTheme.colorScheme.onSurface, - shape = MaterialTheme.shapes.small, - ), + modifier = + Modifier + .fillMaxHeight(0.8f) + .weight(0.7f) + .padding(end = 4.dp) + .background( + color = MaterialTheme.colorScheme.onSurface, + shape = MaterialTheme.shapes.small, + ), ) Box( @@ -185,71 +191,78 @@ fun AppThemePreviewItem( // Cover Box( - modifier = Modifier - .padding(start = 8.dp, top = 2.dp) - .background( - color = DividerDefaults.color, - shape = MaterialTheme.shapes.small, - ) - .fillMaxWidth(0.5f) - .aspectRatio(MangaCover.Book.ratio), + modifier = + Modifier + .padding(start = 8.dp, top = 2.dp) + .background( + color = DividerDefaults.color, + shape = MaterialTheme.shapes.small, + ).fillMaxWidth(0.5f) + .aspectRatio(MangaCover.Book.ratio), ) { Row( - modifier = Modifier - .padding(4.dp) - .size(width = 24.dp, height = 16.dp) - .clip(RoundedCornerShape(5.dp)), + modifier = + Modifier + .padding(4.dp) + .size(width = 24.dp, height = 16.dp) + .clip(RoundedCornerShape(5.dp)), ) { Box( - modifier = Modifier - .fillMaxHeight() - .width(12.dp) - .background(MaterialTheme.colorScheme.tertiary), + modifier = + Modifier + .fillMaxHeight() + .width(12.dp) + .background(MaterialTheme.colorScheme.tertiary), ) Box( - modifier = Modifier - .fillMaxHeight() - .width(12.dp) - .background(MaterialTheme.colorScheme.secondary), + modifier = + Modifier + .fillMaxHeight() + .width(12.dp) + .background(MaterialTheme.colorScheme.secondary), ) } } // Bottom bar Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f), + modifier = + Modifier + .fillMaxWidth() + .weight(1f), contentAlignment = Alignment.BottomCenter, ) { Surface( color = MaterialTheme.colorScheme.surfaceContainer, ) { Row( - modifier = Modifier - .height(32.dp) - .fillMaxWidth() - .padding(horizontal = 8.dp), + modifier = + Modifier + .height(32.dp) + .fillMaxWidth() + .padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Box( - modifier = Modifier - .size(17.dp) - .background( - color = MaterialTheme.colorScheme.primary, - shape = CircleShape, - ), + modifier = + Modifier + .size(17.dp) + .background( + color = MaterialTheme.colorScheme.primary, + shape = CircleShape, + ), ) Box( - modifier = Modifier - .padding(start = 8.dp) - .alpha(0.6f) - .height(17.dp) - .weight(1f) - .background( - color = MaterialTheme.colorScheme.onSurface, - shape = MaterialTheme.shapes.small, - ), + modifier = + Modifier + .padding(start = 8.dp) + .alpha(0.6f) + .height(17.dp) + .weight(1f) + .background( + color = MaterialTheme.colorScheme.onSurface, + shape = MaterialTheme.shapes.small, + ), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt index bba72cf98e..7069de6161 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt @@ -47,11 +47,12 @@ internal fun BasePreferenceWidget( val highlighted = LocalPreferenceHighlighted.current val minHeight = LocalPreferenceMinHeight.current Row( - modifier = modifier - .highlightBackground(highlighted) - .sizeIn(minHeight = minHeight) - .clickable(enabled = onClick != null, onClick = { onClick?.invoke() }) - .fillMaxWidth(), + modifier = + modifier + .highlightBackground(highlighted) + .sizeIn(minHeight = minHeight) + .clickable(enabled = onClick != null, onClick = { onClick?.invoke() }) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { if (icon != null) { @@ -61,9 +62,10 @@ internal fun BasePreferenceWidget( ) } Column( - modifier = Modifier - .weight(1f) - .padding(vertical = PrefsVerticalPadding), + modifier = + Modifier + .weight(1f) + .padding(vertical = PrefsVerticalPadding), ) { if (!title.isNullOrBlank()) { Text( @@ -86,38 +88,42 @@ internal fun BasePreferenceWidget( } } -internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed { - var highlightFlag by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - if (highlighted) { - highlightFlag = true - delay(3.seconds) - highlightFlag = false +internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = + composed { + var highlightFlag by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + if (highlighted) { + highlightFlag = true + delay(3.seconds) + highlightFlag = false + } } + val highlight by animateColorAsState( + targetValue = + if (highlightFlag) { + MaterialTheme.colorScheme.surfaceTint.copy(alpha = .12f) + } else { + Color.Transparent + }, + animationSpec = + if (highlightFlag) { + repeatable( + iterations = 5, + animation = tween(durationMillis = 200), + repeatMode = RepeatMode.Reverse, + initialStartOffset = + StartOffset( + offsetMillis = 600, + offsetType = StartOffsetType.Delay, + ), + ) + } else { + tween(200) + }, + label = "highlight", + ) + Modifier.background(color = highlight) } - val highlight by animateColorAsState( - targetValue = if (highlightFlag) { - MaterialTheme.colorScheme.surfaceTint.copy(alpha = .12f) - } else { - Color.Transparent - }, - animationSpec = if (highlightFlag) { - repeatable( - iterations = 5, - animation = tween(durationMillis = 200), - repeatMode = RepeatMode.Reverse, - initialStartOffset = StartOffset( - offsetMillis = 600, - offsetType = StartOffsetType.Delay, - ), - ) - } else { - tween(200) - }, - label = "highlight", - ) - Modifier.background(color = highlight) -} internal val TrailingWidgetBuffer = 16.dp internal val PrefsHorizontalPadding = 16.dp diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/EditTextPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/EditTextPreferenceWidget.kt index 00d8f16d43..299a21748a 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/EditTextPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/EditTextPreferenceWidget.kt @@ -69,9 +69,10 @@ fun EditTextPreferenceWidget( modifier = Modifier.fillMaxWidth(), ) }, - properties = DialogProperties( - usePlatformDefaultWidth = true, - ), + properties = + DialogProperties( + usePlatformDefaultWidth = true, + ), confirmButton = { TextButton( enabled = textFieldValue.text != value && textFieldValue.text.isNotBlank(), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt index 46fee1e1c7..09e4220f55 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt @@ -21,12 +21,12 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable internal fun InfoWidget(text: String) { Column( - modifier = Modifier - .padding( - horizontal = PrefsHorizontalPadding, - vertical = MaterialTheme.padding.medium, - ) - .secondaryItemAlpha(), + modifier = + Modifier + .padding( + horizontal = PrefsHorizontalPadding, + vertical = MaterialTheme.padding.medium, + ).secondaryItemAlpha(), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), ) { Icon( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt index 7daf651688..bd3b6b6c76 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt @@ -88,14 +88,14 @@ private fun DialogRow( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(MaterialTheme.shapes.small) - .selectable( - selected = isSelected, - onClick = { if (!isSelected) onSelected() }, - ) - .fillMaxWidth() - .minimumInteractiveComponentSize(), + modifier = + Modifier + .clip(MaterialTheme.shapes.small) + .selectable( + selected = isSelected, + onClick = { if (!isSelected) onSelected() }, + ).fillMaxWidth() + .minimumInteractiveComponentSize(), ) { RadioButton( selected = isSelected, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/MultiSelectListPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/MultiSelectListPreferenceWidget.kt index c9ac351842..87d7ab4e48 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/MultiSelectListPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/MultiSelectListPreferenceWidget.kt @@ -32,11 +32,12 @@ fun MultiSelectListPreferenceWidget( ) if (isDialogShown) { - val selected = remember { - preference.entries.keys - .filter { values.contains(it) } - .toMutableStateList() - } + val selected = + remember { + preference.entries.keys + .filter { values.contains(it) } + .toMutableStateList() + } AlertDialog( onDismissRequest = { isDialogShown = false }, title = { Text(text = preference.title) }, @@ -60,9 +61,10 @@ fun MultiSelectListPreferenceWidget( } } }, - properties = DialogProperties( - usePlatformDefaultWidth = true, - ), + properties = + DialogProperties( + usePlatformDefaultWidth = true, + ), confirmButton = { TextButton( onClick = { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt index d41b9a2778..d96a483e61 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt @@ -14,9 +14,10 @@ import androidx.compose.ui.unit.dp fun PreferenceGroupHeader(title: String) { Box( contentAlignment = Alignment.CenterStart, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp, top = 14.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 8.dp, top = 14.dp), ) { Text( text = title, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt index f3423d8e4e..cddeffa1ec 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt @@ -29,31 +29,34 @@ fun TextPreferenceWidget( BasePreferenceWidget( modifier = modifier, title = title, - subcomponent = if (!subtitle.isNullOrBlank()) { - { - Text( - text = subtitle, - modifier = Modifier - .padding(horizontal = PrefsHorizontalPadding) - .secondaryItemAlpha(), - style = MaterialTheme.typography.bodySmall, - maxLines = 10, - ) - } - } else { - null - }, - icon = if (icon != null) { - { - Icon( - imageVector = icon, - tint = iconTint, - contentDescription = null, - ) - } - } else { - null - }, + subcomponent = + if (!subtitle.isNullOrBlank()) { + { + Text( + text = subtitle, + modifier = + Modifier + .padding(horizontal = PrefsHorizontalPadding) + .secondaryItemAlpha(), + style = MaterialTheme.typography.bodySmall, + maxLines = 10, + ) + } + } else { + null + }, + icon = + if (icon != null) { + { + Icon( + imageVector = icon, + tint = iconTint, + contentDescription = null, + ) + } + } else { + null + }, onClick = onPreferenceClick, widget = widget, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt index 81362b66c5..65fcc889e4 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt @@ -32,18 +32,20 @@ fun TrackingPreferenceWidget( val highlighted = LocalPreferenceHighlighted.current Box(modifier = Modifier.highlightBackground(highlighted)) { Row( - modifier = modifier - .clickable(enabled = onClick != null, onClick = { onClick?.invoke() }) - .fillMaxWidth() - .padding(horizontal = PrefsHorizontalPadding, vertical = 8.dp), + modifier = + modifier + .clickable(enabled = onClick != null, onClick = { onClick?.invoke() }) + .fillMaxWidth() + .padding(horizontal = PrefsHorizontalPadding, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { TrackLogoIcon(tracker) Text( text = tracker.name, - modifier = Modifier - .weight(1f) - .padding(horizontal = 16.dp), + modifier = + Modifier + .weight(1f) + .padding(horizontal = 16.dp), maxLines = 1, style = MaterialTheme.typography.titleLarge, fontSize = TitleFontSize, @@ -51,9 +53,10 @@ fun TrackingPreferenceWidget( if (checked) { Icon( imageVector = Icons.Outlined.Done, - modifier = Modifier - .padding(4.dp) - .size(32.dp), + modifier = + Modifier + .padding(4.dp) + .size(32.dp), tint = Color(0xFF4CAF50), contentDescription = stringResource(MR.strings.login_success), ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt index be5029ac3d..bf24faf9e1 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt @@ -32,7 +32,9 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource private enum class State { - CHECKED, INVERSED, UNCHECKED + CHECKED, + INVERSED, + UNCHECKED, } @Composable @@ -46,17 +48,17 @@ fun TriStateListDialog( onDismissRequest: () -> Unit, onValueChanged: (newIncluded: List, newExcluded: List) -> Unit, ) { - val selected = remember { - items - .map { - when (it) { - in initialChecked -> State.CHECKED - in initialInversed -> State.INVERSED - else -> State.UNCHECKED - } - } - .toMutableStateList() - } + val selected = + remember { + items + .map { + when (it) { + in initialChecked -> State.CHECKED + in initialInversed -> State.INVERSED + else -> State.UNCHECKED + } + }.toMutableStateList() + } AlertDialog( onDismissRequest = onDismissRequest, title = { Text(text = title) }, @@ -75,38 +77,42 @@ fun TriStateListDialog( itemsIndexed(items = items) { index, item -> val state = selected[index] Row( - modifier = Modifier - .clip(MaterialTheme.shapes.small) - .clickable { - selected[index] = when (state) { - State.UNCHECKED -> State.CHECKED - State.CHECKED -> State.INVERSED - State.INVERSED -> State.UNCHECKED - } - } - .defaultMinSize(minHeight = 48.dp) - .fillMaxWidth(), + modifier = + Modifier + .clip(MaterialTheme.shapes.small) + .clickable { + selected[index] = + when (state) { + State.UNCHECKED -> State.CHECKED + State.CHECKED -> State.INVERSED + State.INVERSED -> State.UNCHECKED + } + }.defaultMinSize(minHeight = 48.dp) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier.padding(end = 20.dp), - imageVector = when (state) { - State.UNCHECKED -> Icons.Rounded.CheckBoxOutlineBlank - State.CHECKED -> Icons.Rounded.CheckBox - State.INVERSED -> Icons.Rounded.DisabledByDefault - }, - tint = if (state == State.UNCHECKED) { - LocalContentColor.current - } else { - MaterialTheme.colorScheme.primary - }, - contentDescription = stringResource( + imageVector = when (state) { - State.UNCHECKED -> MR.strings.not_selected - State.CHECKED -> MR.strings.selected - State.INVERSED -> MR.strings.disabled + State.UNCHECKED -> Icons.Rounded.CheckBoxOutlineBlank + State.CHECKED -> Icons.Rounded.CheckBox + State.INVERSED -> Icons.Rounded.DisabledByDefault + }, + tint = + if (state == State.UNCHECKED) { + LocalContentColor.current + } else { + MaterialTheme.colorScheme.primary }, - ), + contentDescription = + stringResource( + when (state) { + State.UNCHECKED -> MR.strings.not_selected + State.CHECKED -> MR.strings.selected + State.INVERSED -> MR.strings.disabled + }, + ), ) Text(text = itemLabel(item)) } @@ -126,12 +132,14 @@ fun TriStateListDialog( confirmButton = { TextButton( onClick = { - val included = items.mapIndexedNotNull { index, category -> - if (selected[index] == State.CHECKED) category else null - } - val excluded = items.mapIndexedNotNull { index, category -> - if (selected[index] == State.INVERSED) category else null - } + val included = + items.mapIndexedNotNull { index, category -> + if (selected[index] == State.CHECKED) category else null + } + val excluded = + items.mapIndexedNotNull { index, category -> + if (selected[index] == State.INVERSED) category else null + } onValueChanged(included, excluded) }, ) { diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt index 415f72ae5f..1438266d27 100644 --- a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt @@ -53,16 +53,15 @@ fun StatsScreenContent( } @Composable -private fun LazyItemScope.OverviewSection( - data: StatsData.Overview, -) { +private fun LazyItemScope.OverviewSection(data: StatsData.Overview) { val none = stringResource(MR.strings.none) val context = LocalContext.current - val readDurationString = remember(data.totalReadDuration) { - data.totalReadDuration - .toDuration(DurationUnit.MILLISECONDS) - .toDurationString(context, fallback = none) - } + val readDurationString = + remember(data.totalReadDuration) { + data.totalReadDuration + .toDuration(DurationUnit.MILLISECONDS) + .toDurationString(context, fallback = none) + } SectionCard(MR.strings.label_overview_section) { Row( modifier = Modifier.height(IntrinsicSize.Min), @@ -87,9 +86,7 @@ private fun LazyItemScope.OverviewSection( } @Composable -private fun LazyItemScope.TitlesStats( - data: StatsData.Titles, -) { +private fun LazyItemScope.TitlesStats(data: StatsData.Titles) { SectionCard(MR.strings.label_titles_section) { Row { StatsItem( @@ -109,9 +106,7 @@ private fun LazyItemScope.TitlesStats( } @Composable -private fun LazyItemScope.ChapterStats( - data: StatsData.Chapters, -) { +private fun LazyItemScope.ChapterStats(data: StatsData.Chapters) { SectionCard(MR.strings.chapters) { Row { StatsItem( @@ -131,18 +126,17 @@ private fun LazyItemScope.ChapterStats( } @Composable -private fun LazyItemScope.TrackerStats( - data: StatsData.Trackers, -) { +private fun LazyItemScope.TrackerStats(data: StatsData.Trackers) { val notApplicable = stringResource(MR.strings.not_applicable) - val meanScoreStr = remember(data.trackedTitleCount, data.meanScore) { - if (data.trackedTitleCount > 0 && !data.meanScore.isNaN()) { - // All other numbers are localized in English - "%.2f ★".format(Locale.ENGLISH, data.meanScore) - } else { - notApplicable + val meanScoreStr = + remember(data.trackedTitleCount, data.meanScore) { + if (data.trackedTitleCount > 0 && !data.meanScore.isNaN()) { + // All other numbers are localized in English + "%.2f ★".format(Locale.ENGLISH, data.meanScore) + } else { + notApplicable + } } - } SectionCard(MR.strings.label_tracker_section) { Row { StatsItem( diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt b/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt index 8002b3d048..d61b727f55 100644 --- a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt +++ b/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt @@ -55,26 +55,30 @@ private fun RowScope.BaseStatsItem( icon: ImageVector? = null, ) { Column( - modifier = Modifier - .weight(1f) - .fillMaxHeight(), + modifier = + Modifier + .weight(1f) + .fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = title, - style = titleStyle - .copy(fontWeight = FontWeight.Bold), + style = + titleStyle + .copy(fontWeight = FontWeight.Bold), textAlign = TextAlign.Center, maxLines = 1, ) Text( text = subtitle, - style = subtitleStyle - .copy( - color = MaterialTheme.colorScheme.onSurface - .copy(alpha = SecondaryItemAlpha), - ), + style = + subtitleStyle + .copy( + color = + MaterialTheme.colorScheme.onSurface + .copy(alpha = SecondaryItemAlpha), + ), textAlign = TextAlign.Center, ) if (icon != null) { diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/data/StatsData.kt b/app/src/main/java/eu/kanade/presentation/more/stats/data/StatsData.kt index 8ea31cd9cc..a8f2440808 100644 --- a/app/src/main/java/eu/kanade/presentation/more/stats/data/StatsData.kt +++ b/app/src/main/java/eu/kanade/presentation/more/stats/data/StatsData.kt @@ -1,7 +1,6 @@ package eu.kanade.presentation.more.stats.data sealed interface StatsData { - data class Overview( val libraryMangaCount: Int, val completedMangaCount: Int, diff --git a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt index 5fb7498284..7220b591ca 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt @@ -96,9 +96,10 @@ private fun TransitionText( chapterGap: Int, ) { Column( - modifier = Modifier - .widthIn(max = 460.dp) - .fillMaxWidth(), + modifier = + Modifier + .widthIn(max = 460.dp) + .fillMaxWidth(), ) { if (topChapter != null) { ChapterText( @@ -151,8 +152,9 @@ private fun NoChapterNotification( colors = CardColor, ) { Row( - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 12.dp), + modifier = + Modifier + .padding(horizontal = 16.dp, vertical = 12.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -224,39 +226,43 @@ private fun ChapterText( ) Text( - text = buildAnnotatedString { - if (downloaded) { - appendInlineContent(DownloadedIconContentId) - append(' ') - } - append(name) - }, + text = + buildAnnotatedString { + if (downloaded) { + appendInlineContent(DownloadedIconContentId) + append(' ') + } + append(name) + }, fontSize = 20.sp, maxLines = 5, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleLarge, - inlineContent = persistentMapOf( - DownloadedIconContentId to InlineTextContent( - Placeholder( - width = 22.sp, - height = 22.sp, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center, - ), - ) { - Icon( - imageVector = Icons.Filled.CheckCircle, - contentDescription = stringResource(MR.strings.label_downloaded), - ) - }, - ), + inlineContent = + persistentMapOf( + DownloadedIconContentId to + InlineTextContent( + Placeholder( + width = 22.sp, + height = 22.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + ) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = stringResource(MR.strings.label_downloaded), + ) + }, + ), ) scanlator?.let { Text( text = it, - modifier = Modifier - .secondaryItemAlpha() - .padding(top = 2.dp), + modifier = + Modifier + .secondaryItemAlpha() + .padding(top = 2.dp), maxLines = 2, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall, @@ -267,15 +273,20 @@ private fun ChapterText( private val CardColor: CardColors @Composable - get() = CardDefaults.outlinedCardColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onSurface, - ) + get() = + CardDefaults.outlinedCardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + ) private val VerticalSpacerSize = 24.dp private const val DownloadedIconContentId = "downloaded" -private fun previewChapter(name: String, scanlator: String, chapterNumber: Double) = Chapter.create().copy( +private fun previewChapter( + name: String, + scanlator: String, + chapterNumber: Double, +) = Chapter.create().copy( id = 0L, mangaId = 0L, url = "", @@ -283,25 +294,30 @@ private fun previewChapter(name: String, scanlator: String, chapterNumber: Doubl scanlator = scanlator, chapterNumber = chapterNumber, ) -private val FakeChapter = previewChapter( - name = "Vol.1, Ch.1 - Fake Chapter Title", - scanlator = "Scanlator Name", - chapterNumber = 1.0, -) -private val FakeGapChapter = previewChapter( - name = "Vol.5, Ch.44 - Fake Gap Chapter Title", - scanlator = "Scanlator Name", - chapterNumber = 44.0, -) -private val FakeChapterLongTitle = previewChapter( - name = "Vol.1, Ch.0 - The Mundane Musings of a Metafictional Manga: A Chapter About a Chapter, Featuring" + - " an Absurdly Long Title and a Surprisingly Normal Day in the Lives of Our Heroes, as They Grapple with the " + - "Daily Challenges of Existence, from Paying Rent to Finding Love, All While Navigating the Strange World of " + - "Fictional Realities and Reality-Bending Fiction, Where the Fourth Wall is Always in Danger of Being Broken " + - "and the Line Between Author and Character is Forever Blurred.", - scanlator = "Long Long Funny Scanlator Sniper Group Name Reborn", - chapterNumber = 1.0, -) + +private val FakeChapter = + previewChapter( + name = "Vol.1, Ch.1 - Fake Chapter Title", + scanlator = "Scanlator Name", + chapterNumber = 1.0, + ) +private val FakeGapChapter = + previewChapter( + name = "Vol.5, Ch.44 - Fake Gap Chapter Title", + scanlator = "Scanlator Name", + chapterNumber = 44.0, + ) +private val FakeChapterLongTitle = + previewChapter( + name = + "Vol.1, Ch.0 - The Mundane Musings of a Metafictional Manga: A Chapter About a Chapter, Featuring" + + " an Absurdly Long Title and a Surprisingly Normal Day in the Lives of Our Heroes, as They Grapple with the " + + "Daily Challenges of Existence, from Paying Rent to Finding Love, All While Navigating the Strange World of " + + "Fictional Realities and Reality-Bending Fiction, Where the Fourth Wall is Always in Danger of Being Broken " + + "and the Line Between Author and Character is Forever Blurred.", + scanlator = "Long Long Funny Scanlator Sniper Group Name Reborn", + chapterNumber = 1.0, + ) @PreviewLightDark @Composable diff --git a/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt b/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt index ecf26119d0..6ffb74d224 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt @@ -20,7 +20,6 @@ import kotlin.time.Duration.Companion.milliseconds @Stable class DisplayRefreshHost { - internal var currentDisplayRefresh by mutableStateOf(false) private val readerPreferences = Injekt.get() @@ -65,11 +64,12 @@ fun DisplayRefreshHost( } val refreshDurationHalf = refreshDuration.milliseconds / 2 - currentColor = if (flashMode == ReaderPreferences.FlashColor.BLACK) { - Color.Black - } else { - Color.White - } + currentColor = + if (flashMode == ReaderPreferences.FlashColor.BLACK) { + Color.Black + } else { + Color.White + } delay(refreshDurationHalf) if (flashMode == ReaderPreferences.FlashColor.WHITE_BLACK) { currentColor = Color.Black diff --git a/app/src/main/java/eu/kanade/presentation/reader/OrientationSelectDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/OrientationSelectDialog.kt index 5943dd65a8..919f6a2e6f 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/OrientationSelectDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/OrientationSelectDialog.kt @@ -55,11 +55,12 @@ private fun DialogContent( var selected by remember { mutableStateOf(orientation) } ModeSelectionDialog( - onUseDefault = { - onChangeOrientation( - ReaderOrientation.DEFAULT, - ) - }.takeIf { orientation != ReaderOrientation.DEFAULT }, + onUseDefault = + { + onChangeOrientation( + ReaderOrientation.DEFAULT, + ) + }.takeIf { orientation != ReaderOrientation.DEFAULT }, onApply = { onChangeOrientation(selected) }, ) { SettingsIconGrid(MR.strings.rotation_type) { diff --git a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt b/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt index 3dd057a565..424d8f3656 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt @@ -23,16 +23,18 @@ fun PageIndicatorText( val text = "$currentPage / $totalPages" - val style = TextStyle( - color = Color(235, 235, 235), - fontSize = MaterialTheme.typography.bodySmall.fontSize, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp, - ) - val strokeStyle = style.copy( - color = Color(45, 45, 45), - drawStyle = Stroke(width = 4f), - ) + val style = + TextStyle( + color = Color(235, 235, 235), + fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp, + ) + val strokeStyle = + style.copy( + color = Color(45, 45, 45), + drawStyle = Stroke(width = 4f), + ) Box( contentAlignment = Alignment.Center, diff --git a/app/src/main/java/eu/kanade/presentation/reader/ReaderContentOverlay.kt b/app/src/main/java/eu/kanade/presentation/reader/ReaderContentOverlay.kt index 4f72ac9af1..0a06d34140 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ReaderContentOverlay.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ReaderContentOverlay.kt @@ -20,16 +20,18 @@ fun ReaderContentOverlay( modifier: Modifier = Modifier, ) { if (brightness < 0) { - val brightnessAlpha = remember(brightness) { - abs(brightness) / 100f - } + val brightnessAlpha = + remember(brightness) { + abs(brightness) / 100f + } Canvas( - modifier = modifier - .fillMaxSize() - .graphicsLayer { - alpha = brightnessAlpha - }, + modifier = + modifier + .fillMaxSize() + .graphicsLayer { + alpha = brightnessAlpha + }, ) { drawRect(Color.Black) } @@ -37,8 +39,9 @@ fun ReaderContentOverlay( if (color != null) { Canvas( - modifier = modifier - .fillMaxSize(), + modifier = + modifier + .fillMaxSize(), ) { drawRect( color = Color(color), diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt b/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt index 8d0cae57b2..b8e49bd91d 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt @@ -33,10 +33,11 @@ fun BottomReaderBar( onClickSettings: () -> Unit, ) { Row( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor) - .padding(8.dp), + modifier = + Modifier + .fillMaxWidth() + .background(backgroundColor) + .padding(8.dp), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, ) { diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt index 9641724837..e44a8011ef 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt @@ -38,7 +38,6 @@ private val animationSpec = tween(200) fun ReaderAppBars( visible: Boolean, fullscreen: Boolean, - mangaTitle: String?, chapterTitle: String?, navigateUp: () -> Unit, @@ -47,7 +46,6 @@ fun ReaderAppBars( onToggleBookmarked: () -> Unit, onOpenInWebView: (() -> Unit)?, onShare: (() -> Unit)?, - viewer: Viewer?, onNextChapter: () -> Unit, enabledNext: Boolean, @@ -56,7 +54,6 @@ fun ReaderAppBars( currentPage: Int, totalPages: Int, onSliderValueChange: (Int) -> Unit, - readingMode: ReadingMode, onClickReadingMode: () -> Unit, orientation: ReaderOrientation, @@ -66,15 +63,17 @@ fun ReaderAppBars( onClickSettings: () -> Unit, ) { val isRtl = viewer is R2LPagerViewer - val backgroundColor = MaterialTheme.colorScheme - .surfaceColorAtElevation(3.dp) - .copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f) + val backgroundColor = + MaterialTheme.colorScheme + .surfaceColorAtElevation(3.dp) + .copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f) - val modifierWithInsetsPadding = if (fullscreen) { - Modifier.systemBarsPadding() - } else { - Modifier - } + val modifierWithInsetsPadding = + if (fullscreen) { + Modifier.systemBarsPadding() + } else { + Modifier + } Column( modifier = Modifier.fillMaxHeight(), @@ -82,61 +81,67 @@ fun ReaderAppBars( ) { AnimatedVisibility( visible = visible, - enter = slideInVertically( - initialOffsetY = { -it }, - animationSpec = animationSpec, - ), - exit = slideOutVertically( - targetOffsetY = { -it }, - animationSpec = animationSpec, - ), + enter = + slideInVertically( + initialOffsetY = { -it }, + animationSpec = animationSpec, + ), + exit = + slideOutVertically( + targetOffsetY = { -it }, + animationSpec = animationSpec, + ), ) { AppBar( - modifier = modifierWithInsetsPadding - .clickable(onClick = onClickTopAppBar), + modifier = + modifierWithInsetsPadding + .clickable(onClick = onClickTopAppBar), backgroundColor = backgroundColor, title = mangaTitle, subtitle = chapterTitle, navigateUp = navigateUp, actions = { AppBarActions( - actions = persistentListOf().builder() - .apply { - add( - AppBar.Action( - title = stringResource( - if (bookmarked) { - MR.strings.action_remove_bookmark - } else { - MR.strings.action_bookmark - }, - ), - icon = if (bookmarked) { - Icons.Outlined.Bookmark - } else { - Icons.Outlined.BookmarkBorder - }, - onClick = onToggleBookmarked, - ), - ) - onOpenInWebView?.let { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_open_in_web_view), - onClick = it, - ), - ) - } - onShare?.let { + actions = + persistentListOf() + .builder() + .apply { add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_share), - onClick = it, + AppBar.Action( + title = + stringResource( + if (bookmarked) { + MR.strings.action_remove_bookmark + } else { + MR.strings.action_bookmark + }, + ), + icon = + if (bookmarked) { + Icons.Outlined.Bookmark + } else { + Icons.Outlined.BookmarkBorder + }, + onClick = onToggleBookmarked, ), ) - } - } - .build(), + onOpenInWebView?.let { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_open_in_web_view), + onClick = it, + ), + ) + } + onShare?.let { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_share), + onClick = it, + ), + ) + } + }.build(), ) }, ) @@ -146,14 +151,16 @@ fun ReaderAppBars( AnimatedVisibility( visible = visible, - enter = slideInVertically( - initialOffsetY = { it }, - animationSpec = animationSpec, - ), - exit = slideOutVertically( - targetOffsetY = { it }, - animationSpec = animationSpec, - ), + enter = + slideInVertically( + initialOffsetY = { it }, + animationSpec = animationSpec, + ), + exit = + slideOutVertically( + targetOffsetY = { it }, + animationSpec = animationSpec, + ), ) { Column( modifier = modifierWithInsetsPadding, diff --git a/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt b/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt index 070b81e915..88a741e8b9 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt @@ -54,20 +54,23 @@ fun ChapterNavigator( val haptic = LocalHapticFeedback.current // Match with toolbar background color set in ReaderActivity - val backgroundColor = MaterialTheme.colorScheme - .surfaceColorAtElevation(3.dp) - .copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f) - val buttonColor = IconButtonDefaults.filledIconButtonColors( - containerColor = backgroundColor, - disabledContainerColor = backgroundColor, - ) + val backgroundColor = + MaterialTheme.colorScheme + .surfaceColorAtElevation(3.dp) + .copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f) + val buttonColor = + IconButtonDefaults.filledIconButtonColors( + containerColor = backgroundColor, + disabledContainerColor = backgroundColor, + ) // We explicitly handle direction based on the reader viewer rather than the system direction CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = horizontalPadding), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = horizontalPadding), verticalAlignment = Alignment.CenterVertically, ) { FilledIconButton( @@ -77,20 +80,22 @@ fun ChapterNavigator( ) { Icon( imageVector = Icons.Outlined.SkipPrevious, - contentDescription = stringResource( - if (isRtl) MR.strings.action_next_chapter else MR.strings.action_previous_chapter, - ), + contentDescription = + stringResource( + if (isRtl) MR.strings.action_next_chapter else MR.strings.action_previous_chapter, + ), ) } if (totalPages > 1) { CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) { Row( - modifier = Modifier - .weight(1f) - .clip(RoundedCornerShape(24.dp)) - .background(backgroundColor) - .padding(horizontal = 16.dp), + modifier = + Modifier + .weight(1f) + .clip(RoundedCornerShape(24.dp)) + .background(backgroundColor) + .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { Text(text = currentPage.toString()) @@ -103,9 +108,10 @@ fun ChapterNavigator( } } Slider( - modifier = Modifier - .weight(1f) - .padding(horizontal = 8.dp), + modifier = + Modifier + .weight(1f) + .padding(horizontal = 8.dp), value = currentPage.toFloat(), valueRange = 1f..totalPages.toFloat(), steps = totalPages - 2, @@ -129,9 +135,10 @@ fun ChapterNavigator( ) { Icon( imageVector = Icons.Outlined.SkipNext, - contentDescription = stringResource( - if (isRtl) MR.strings.action_previous_chapter else MR.strings.action_next_chapter, - ), + contentDescription = + stringResource( + if (isRtl) MR.strings.action_previous_chapter else MR.strings.action_next_chapter, + ), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt index c6286bf22d..61ac5c0781 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt @@ -36,9 +36,10 @@ fun ModeSelectionDialog( content() Row( - modifier = Modifier.padding( - horizontal = SettingsItemsPaddings.Horizontal, - ), + modifier = + Modifier.padding( + horizontal = SettingsItemsPaddings.Horizontal, + ), ) { onUseDefault?.let { OutlinedButton(onClick = it) { diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/ColorFilterPage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/ColorFilterPage.kt index cb8f5578be..8aa64100be 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/settings/ColorFilterPage.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/ColorFilterPage.kt @@ -119,9 +119,13 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) ) } -private fun getColorValue(currentColor: Int, color: Int, mask: Long, bitShift: Int): Int { - return (color shl bitShift) or (currentColor and mask.inv().toInt()) -} +private fun getColorValue( + currentColor: Int, + color: Int, + mask: Long, + bitShift: Int, +): Int = (color shl bitShift) or (currentColor and mask.inv().toInt()) + private const val ALPHA_MASK: Long = 0xFF000000 private const val RED_MASK: Long = 0x00FF0000 private const val GREEN_MASK: Long = 0x0000FF00 diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt index 0bb2da6cc0..ba48a8372e 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt @@ -15,18 +15,20 @@ import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState -private val themes = listOf( - MR.strings.black_background to 1, - MR.strings.gray_background to 2, - MR.strings.white_background to 0, - MR.strings.automatic_background to 3, -) - -private val flashColors = listOf( - MR.strings.pref_flash_style_black to ReaderPreferences.FlashColor.BLACK, - MR.strings.pref_flash_style_white to ReaderPreferences.FlashColor.WHITE, - MR.strings.pref_flash_style_white_black to ReaderPreferences.FlashColor.WHITE_BLACK, -) +private val themes = + listOf( + MR.strings.black_background to 1, + MR.strings.gray_background to 2, + MR.strings.white_background to 0, + MR.strings.automatic_background to 3, + ) + +private val flashColors = + listOf( + MR.strings.pref_flash_style_black to ReaderPreferences.FlashColor.BLACK, + MR.strings.pref_flash_style_white to ReaderPreferences.FlashColor.WHITE, + MR.strings.pref_flash_style_white_black to ReaderPreferences.FlashColor.WHITE_BLACK, + ) @Composable internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) { diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/ReaderSettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/ReaderSettingsDialog.kt index a15c9e5414..882b7da7e1 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/settings/ReaderSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/ReaderSettingsDialog.kt @@ -26,11 +26,12 @@ fun ReaderSettingsDialog( onHideMenus: () -> Unit, screenModel: ReaderSettingsScreenModel, ) { - val tabTitles = persistentListOf( - stringResource(MR.strings.pref_category_reading_mode), - stringResource(MR.strings.pref_category_general), - stringResource(MR.strings.custom_filter), - ) + val tabTitles = + persistentListOf( + stringResource(MR.strings.pref_category_reading_mode), + stringResource(MR.strings.pref_category_general), + stringResource(MR.strings.custom_filter), + ) val pagerState = rememberPagerState { tabTitles.size } BoxWithConstraints { @@ -56,9 +57,10 @@ fun ReaderSettingsDialog( } Column( - modifier = Modifier - .padding(vertical = TabbedDialogPaddings.Vertical) - .verticalScroll(rememberScrollState()), + modifier = + Modifier + .padding(vertical = TabbedDialogPaddings.Vertical) + .verticalScroll(rememberScrollState()), ) { when (page) { 0 -> ReadingModePage(screenModel) diff --git a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt index af550aa5de..ba47b85437 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt @@ -63,27 +63,29 @@ private fun getThemeColorScheme( appTheme: AppTheme, isAmoled: Boolean, ): ColorScheme { - val colorScheme = if (appTheme == AppTheme.MONET) { - MonetColorScheme(LocalContext.current) - } else { - colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme) - } + val colorScheme = + if (appTheme == AppTheme.MONET) { + MonetColorScheme(LocalContext.current) + } else { + colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme) + } return colorScheme.getColorScheme( isSystemInDarkTheme(), isAmoled, ) } -private val colorSchemes: Map = mapOf( - AppTheme.DEFAULT to TachiyomiColorScheme, - AppTheme.GREEN_APPLE to GreenAppleColorScheme, - AppTheme.LAVENDER to LavenderColorScheme, - AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme, - AppTheme.NORD to NordColorScheme, - AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme, - AppTheme.TAKO to TakoColorScheme, - AppTheme.TEALTURQUOISE to TealTurqoiseColorScheme, - AppTheme.TIDAL_WAVE to TidalWaveColorScheme, - AppTheme.YINYANG to YinYangColorScheme, - AppTheme.YOTSUBA to YotsubaColorScheme, -) +private val colorSchemes: Map = + mapOf( + AppTheme.DEFAULT to TachiyomiColorScheme, + AppTheme.GREEN_APPLE to GreenAppleColorScheme, + AppTheme.LAVENDER to LavenderColorScheme, + AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme, + AppTheme.NORD to NordColorScheme, + AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme, + AppTheme.TAKO to TakoColorScheme, + AppTheme.TEALTURQUOISE to TealTurqoiseColorScheme, + AppTheme.TIDAL_WAVE to TidalWaveColorScheme, + AppTheme.YINYANG to YinYangColorScheme, + AppTheme.YOTSUBA to YotsubaColorScheme, + ) diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/BaseColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/BaseColorScheme.kt index 22dd9a0a79..603a260734 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/BaseColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/BaseColorScheme.kt @@ -4,7 +4,6 @@ import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.Color internal abstract class BaseColorScheme { - abstract val darkScheme: ColorScheme abstract val lightScheme: ColorScheme @@ -14,7 +13,10 @@ internal abstract class BaseColorScheme { private val surfaceContainerHigh = Color(0xFF131313) private val surfaceContainerHighest = Color(0xFF1B1B1B) - fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme { + fun getColorScheme( + isDark: Boolean, + isAmoled: Boolean, + ): ColorScheme { if (!isDark) return lightScheme if (!isAmoled) return darkScheme diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/GreenAppleColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/GreenAppleColorScheme.kt index 566cce91bc..3bd3997fbc 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/GreenAppleColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/GreenAppleColorScheme.kt @@ -16,80 +16,81 @@ import androidx.compose.ui.graphics.Color * Neutral #5D5F5B */ internal object GreenAppleColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFF7ADB8F), + onPrimary = Color(0xFF003917), + primaryContainer = Color(0xFF017737), + onPrimaryContainer = Color(0xFFFFFFFF), + secondary = Color(0xFF7ADB8F), // Unread badge + onSecondary = Color(0xFF003917), // Unread badge text + secondaryContainer = Color(0xFF017737), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFFFFFFF), // Navigation bar selected icon + tertiary = Color(0xFFFFB3AC), // Downloaded badge + onTertiary = Color(0xFF680008), // Downloaded badge text + tertiaryContainer = Color(0xFFC7282A), + onTertiaryContainer = Color(0xFFFFFFFF), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690005), + errorContainer = Color(0xFF93000A), + onErrorContainer = Color(0xFFFFDAD6), + background = Color(0xFF0F1510), + onBackground = Color(0xFFDFE4DB), + surface = Color(0xFF0F1510), + onSurface = Color(0xFFDFE4DB), + surfaceVariant = Color(0xFF3F493F), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFBECABC), + outline = Color(0xFF889487), + outlineVariant = Color(0xFF3F493F), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFFDFE4DB), + inverseOnSurface = Color(0xFF2C322C), + inversePrimary = Color(0xFF006D32), + surfaceDim = Color(0xFF0F1510), + surfaceBright = Color(0xFF353B35), + surfaceContainerLowest = Color(0xFF0A0F0B), + surfaceContainerLow = Color(0xFF181D18), + surfaceContainer = Color(0xFF1C211C), // Navigation bar background + surfaceContainerHigh = Color(0xFF262B26), + surfaceContainerHighest = Color(0xFF313630), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFF7ADB8F), - onPrimary = Color(0xFF003917), - primaryContainer = Color(0xFF017737), - onPrimaryContainer = Color(0xFFFFFFFF), - secondary = Color(0xFF7ADB8F), // Unread badge - onSecondary = Color(0xFF003917), // Unread badge text - secondaryContainer = Color(0xFF017737), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFFFFFFF), // Navigation bar selected icon - tertiary = Color(0xFFFFB3AC), // Downloaded badge - onTertiary = Color(0xFF680008), // Downloaded badge text - tertiaryContainer = Color(0xFFC7282A), - onTertiaryContainer = Color(0xFFFFFFFF), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - background = Color(0xFF0F1510), - onBackground = Color(0xFFDFE4DB), - surface = Color(0xFF0F1510), - onSurface = Color(0xFFDFE4DB), - surfaceVariant = Color(0xFF3F493F), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFBECABC), - outline = Color(0xFF889487), - outlineVariant = Color(0xFF3F493F), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFFDFE4DB), - inverseOnSurface = Color(0xFF2C322C), - inversePrimary = Color(0xFF006D32), - surfaceDim = Color(0xFF0F1510), - surfaceBright = Color(0xFF353B35), - surfaceContainerLowest = Color(0xFF0A0F0B), - surfaceContainerLow = Color(0xFF181D18), - surfaceContainer = Color(0xFF1C211C), // Navigation bar background - surfaceContainerHigh = Color(0xFF262B26), - surfaceContainerHighest = Color(0xFF313630), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFF005927), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFF188140), - onPrimaryContainer = Color(0xFFFFFFFF), - secondary = Color(0xFF005927), // Unread badge - onSecondary = Color(0xFFFFFFFF), // Unread badge text - secondaryContainer = Color(0xFF97f7a9), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF000000), // Navigation bar selected icon - tertiary = Color(0xFF9D0012), // Downloaded badge - onTertiary = Color(0xFFFFFFFF), // Downloaded badge text - tertiaryContainer = Color(0xFFD33131), - onTertiaryContainer = Color(0xFFFFFFFF), - error = Color(0xFFBA1A1A), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF410002), - background = Color(0xFFF6FBF2), - onBackground = Color(0xFF181D18), - surface = Color(0xFFF6FBF2), - onSurface = Color(0xFF181D18), - surfaceVariant = Color(0xFFDAE6D7), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF3F493F), - outline = Color(0xFF6F7A6E), - outlineVariant = Color(0xFFBECABC), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFF2C322C), - inverseOnSurface = Color(0xFFEDF2E9), - inversePrimary = Color(0xFF7ADB8F), - surfaceDim = Color(0xFFD6DCD3), - surfaceBright = Color(0xFFF6FBF2), - surfaceContainerLowest = Color(0xFFFFFFFF), - surfaceContainerLow = Color(0xFFF0F5EC), - surfaceContainer = Color(0xFFEAEFE6), // Navigation bar background - surfaceContainerHigh = Color(0xFFE4EAE1), - surfaceContainerHighest = Color(0xFFDFE4DB), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFF005927), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFF188140), + onPrimaryContainer = Color(0xFFFFFFFF), + secondary = Color(0xFF005927), // Unread badge + onSecondary = Color(0xFFFFFFFF), // Unread badge text + secondaryContainer = Color(0xFF97f7a9), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF000000), // Navigation bar selected icon + tertiary = Color(0xFF9D0012), // Downloaded badge + onTertiary = Color(0xFFFFFFFF), // Downloaded badge text + tertiaryContainer = Color(0xFFD33131), + onTertiaryContainer = Color(0xFFFFFFFF), + error = Color(0xFFBA1A1A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFFDAD6), + onErrorContainer = Color(0xFF410002), + background = Color(0xFFF6FBF2), + onBackground = Color(0xFF181D18), + surface = Color(0xFFF6FBF2), + onSurface = Color(0xFF181D18), + surfaceVariant = Color(0xFFDAE6D7), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF3F493F), + outline = Color(0xFF6F7A6E), + outlineVariant = Color(0xFFBECABC), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFF2C322C), + inverseOnSurface = Color(0xFFEDF2E9), + inversePrimary = Color(0xFF7ADB8F), + surfaceDim = Color(0xFFD6DCD3), + surfaceBright = Color(0xFFF6FBF2), + surfaceContainerLowest = Color(0xFFFFFFFF), + surfaceContainerLow = Color(0xFFF0F5EC), + surfaceContainer = Color(0xFFEAEFE6), // Navigation bar background + surfaceContainerHigh = Color(0xFFE4EAE1), + surfaceContainerHighest = Color(0xFFDFE4DB), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/LavenderColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/LavenderColorScheme.kt index e1bb6b3ee4..66288fba79 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/LavenderColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/LavenderColorScheme.kt @@ -15,80 +15,81 @@ import androidx.compose.ui.graphics.Color * Neutral #111129 */ internal object LavenderColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFFA177FF), + onPrimary = Color(0xFF3D0090), + primaryContainer = Color(0xFFA177FF), + onPrimaryContainer = Color(0xFFFFFFFF), + secondary = Color(0xFFA177FF), // Unread badge + onSecondary = Color(0xFFFFFFFF), // Unread badge text + secondaryContainer = Color(0xFF423271), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFA177FF), // Navigation bar selected icon + tertiary = Color(0xFFCDBDFF), // Downloaded badge + onTertiary = Color(0xFF360096), // Downloaded badge text + tertiaryContainer = Color(0xFF5512D8), + onTertiaryContainer = Color(0xFFEFE6FF), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690005), + errorContainer = Color(0xFF93000A), + onErrorContainer = Color(0xFFFFDAD6), + background = Color(0xFF111129), + onBackground = Color(0xFFE7E0EC), + surface = Color(0xFF111129), + onSurface = Color(0xFFE7E0EC), + surfaceVariant = Color(0xFF3D2F6B), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFCBC3D6), + outline = Color(0xFF958E9F), + outlineVariant = Color(0xFF4A4453), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFFE7E0EC), + inverseOnSurface = Color(0xFF322F38), + inversePrimary = Color(0xFF6D41C8), + surfaceDim = Color(0xFF111129), + surfaceBright = Color(0xFF3B3841), + surfaceContainerLowest = Color(0xFF15132d), + surfaceContainerLow = Color(0xFF171531), + surfaceContainer = Color(0xFF1D193B), // Navigation bar background + surfaceContainerHigh = Color(0xFF241f41), + surfaceContainerHighest = Color(0xFF282446), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFFA177FF), - onPrimary = Color(0xFF3D0090), - primaryContainer = Color(0xFFA177FF), - onPrimaryContainer = Color(0xFFFFFFFF), - secondary = Color(0xFFA177FF), // Unread badge - onSecondary = Color(0xFFFFFFFF), // Unread badge text - secondaryContainer = Color(0xFF423271), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFA177FF), // Navigation bar selected icon - tertiary = Color(0xFFCDBDFF), // Downloaded badge - onTertiary = Color(0xFF360096), // Downloaded badge text - tertiaryContainer = Color(0xFF5512D8), - onTertiaryContainer = Color(0xFFEFE6FF), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - background = Color(0xFF111129), - onBackground = Color(0xFFE7E0EC), - surface = Color(0xFF111129), - onSurface = Color(0xFFE7E0EC), - surfaceVariant = Color(0xFF3D2F6B), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFCBC3D6), - outline = Color(0xFF958E9F), - outlineVariant = Color(0xFF4A4453), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFFE7E0EC), - inverseOnSurface = Color(0xFF322F38), - inversePrimary = Color(0xFF6D41C8), - surfaceDim = Color(0xFF111129), - surfaceBright = Color(0xFF3B3841), - surfaceContainerLowest = Color(0xFF15132d), - surfaceContainerLow = Color(0xFF171531), - surfaceContainer = Color(0xFF1D193B), // Navigation bar background - surfaceContainerHigh = Color(0xFF241f41), - surfaceContainerHighest = Color(0xFF282446), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFF6D41C8), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFF7B46AF), - onPrimaryContainer = Color(0xFF130038), - secondary = Color(0xFF7B46AF), // Unread badge - onSecondary = Color(0xFFEDE2FF), // Unread badge text - secondaryContainer = Color(0xFFC9B0E6), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF7B46AF), // Navigation bar selector icon - tertiary = Color(0xFFEDE2FF), // Downloaded badge - onTertiary = Color(0xFF7B46AF), // Downloaded badge text - tertiaryContainer = Color(0xFF6D3BF0), - onTertiaryContainer = Color(0xFFFFFFFF), - error = Color(0xFFBA1A1A), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF410002), - background = Color(0xFFEDE2FF), - onBackground = Color(0xFF1D1A22), - surface = Color(0xFFEDE2FF), - onSurface = Color(0xFF1D1A22), - surfaceVariant = Color(0xFFE4D5F8), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF4A4453), - outline = Color(0xFF7B7485), - outlineVariant = Color(0xFFCBC3D6), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFF322F38), - inverseOnSurface = Color(0xFFF5EEFA), - inversePrimary = Color(0xFFA177FF), - surfaceDim = Color(0xFFDED7E3), - surfaceBright = Color(0xFFEDE2FF), - surfaceContainerLowest = Color(0xFFDACCEC), - surfaceContainerLow = Color(0xFFDED0F1), - surfaceContainer = Color(0xFFE4D5F8), // Navigation bar background - surfaceContainerHigh = Color(0xFFEADCFD), - surfaceContainerHighest = Color(0xFFEEE2FF), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFF6D41C8), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFF7B46AF), + onPrimaryContainer = Color(0xFF130038), + secondary = Color(0xFF7B46AF), // Unread badge + onSecondary = Color(0xFFEDE2FF), // Unread badge text + secondaryContainer = Color(0xFFC9B0E6), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF7B46AF), // Navigation bar selector icon + tertiary = Color(0xFFEDE2FF), // Downloaded badge + onTertiary = Color(0xFF7B46AF), // Downloaded badge text + tertiaryContainer = Color(0xFF6D3BF0), + onTertiaryContainer = Color(0xFFFFFFFF), + error = Color(0xFFBA1A1A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFFDAD6), + onErrorContainer = Color(0xFF410002), + background = Color(0xFFEDE2FF), + onBackground = Color(0xFF1D1A22), + surface = Color(0xFFEDE2FF), + onSurface = Color(0xFF1D1A22), + surfaceVariant = Color(0xFFE4D5F8), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF4A4453), + outline = Color(0xFF7B7485), + outlineVariant = Color(0xFFCBC3D6), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFF322F38), + inverseOnSurface = Color(0xFFF5EEFA), + inversePrimary = Color(0xFFA177FF), + surfaceDim = Color(0xFFDED7E3), + surfaceBright = Color(0xFFEDE2FF), + surfaceContainerLowest = Color(0xFFDACCEC), + surfaceContainerLow = Color(0xFFDED0F1), + surfaceContainer = Color(0xFFE4D5F8), // Navigation bar background + surfaceContainerHigh = Color(0xFFEADCFD), + surfaceContainerHighest = Color(0xFFEEE2FF), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MidnightDuskColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MidnightDuskColorScheme.kt index 5ae86aa34f..e324251d86 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MidnightDuskColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MidnightDuskColorScheme.kt @@ -16,66 +16,67 @@ import androidx.compose.ui.graphics.Color * Neutral #16151D */ internal object MidnightDuskColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFFF02475), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFBD1C5C), + onPrimaryContainer = Color(0xFFFFFFFF), + inversePrimary = Color(0xFFF02475), + secondary = Color(0xFFF02475), // Unread badge + onSecondary = Color(0xFF16151D), // Unread badge text + secondaryContainer = Color(0xFF66183C), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFF02475), // Navigation bar selector icon + tertiary = Color(0xFF55971C), // Downloaded badge + onTertiary = Color(0xFF16151D), // Downloaded badge text + tertiaryContainer = Color(0xFF386412), + onTertiaryContainer = Color(0xFFE5E1E5), + background = Color(0xFF16151D), + onBackground = Color(0xFFE5E1E5), + surface = Color(0xFF16151D), + onSurface = Color(0xFFE5E1E5), + surfaceVariant = Color(0xFF281624), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFD6C1C4), + surfaceTint = Color(0xFFF02475), + inverseSurface = Color(0xFF333043), + inverseOnSurface = Color(0xFFFFFFFF), + outline = Color(0xFF9F8C8F), + surfaceContainerLowest = Color(0xFF221320), + surfaceContainerLow = Color(0xFF251522), + surfaceContainer = Color(0xFF281624), // Navigation bar background + surfaceContainerHigh = Color(0xFF2D1C2A), + surfaceContainerHighest = Color(0xFF2F1F2C), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFFF02475), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFBD1C5C), - onPrimaryContainer = Color(0xFFFFFFFF), - inversePrimary = Color(0xFFF02475), - secondary = Color(0xFFF02475), // Unread badge - onSecondary = Color(0xFF16151D), // Unread badge text - secondaryContainer = Color(0xFF66183C), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFF02475), // Navigation bar selector icon - tertiary = Color(0xFF55971C), // Downloaded badge - onTertiary = Color(0xFF16151D), // Downloaded badge text - tertiaryContainer = Color(0xFF386412), - onTertiaryContainer = Color(0xFFE5E1E5), - background = Color(0xFF16151D), - onBackground = Color(0xFFE5E1E5), - surface = Color(0xFF16151D), - onSurface = Color(0xFFE5E1E5), - surfaceVariant = Color(0xFF281624), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFD6C1C4), - surfaceTint = Color(0xFFF02475), - inverseSurface = Color(0xFF333043), - inverseOnSurface = Color(0xFFFFFFFF), - outline = Color(0xFF9F8C8F), - surfaceContainerLowest = Color(0xFF221320), - surfaceContainerLow = Color(0xFF251522), - surfaceContainer = Color(0xFF281624), // Navigation bar background - surfaceContainerHigh = Color(0xFF2D1C2A), - surfaceContainerHighest = Color(0xFF2F1F2C), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFFBB0054), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFFFD9E1), - onPrimaryContainer = Color(0xFF3F0017), - inversePrimary = Color(0xFFFFB1C4), - secondary = Color(0xFFBB0054), // Unread badge - onSecondary = Color(0xFFFFFFFF), // Unread badge text - secondaryContainer = Color(0xFFEFBAD4), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFD1377C), // Navigation bar selector icon - tertiary = Color(0xFF006638), // Downloaded badge - onTertiary = Color(0xFFFFFFFF), // Downloaded badge text - tertiaryContainer = Color(0xFF00894b), - onTertiaryContainer = Color(0xFF2D1600), - background = Color(0xFFFFFBFF), - onBackground = Color(0xFF1C1B1F), - surface = Color(0xFFFFFBFF), - onSurface = Color(0xFF1C1B1F), - surfaceVariant = Color(0xFFF9E6F1), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF524346), - surfaceTint = Color(0xFFBB0054), - inverseSurface = Color(0xFF313033), - inverseOnSurface = Color(0xFFF4F0F4), - outline = Color(0xFF847376), - surfaceContainerLowest = Color(0xFFDAC0CD), - surfaceContainerLow = Color(0xFFE8D1DD), - surfaceContainer = Color(0xFFF9E6F1), // Navigation bar background - surfaceContainerHigh = Color(0xFFFCF3F8), - surfaceContainerHighest = Color(0xFFFEF9FC), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFFBB0054), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFFFD9E1), + onPrimaryContainer = Color(0xFF3F0017), + inversePrimary = Color(0xFFFFB1C4), + secondary = Color(0xFFBB0054), // Unread badge + onSecondary = Color(0xFFFFFFFF), // Unread badge text + secondaryContainer = Color(0xFFEFBAD4), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFD1377C), // Navigation bar selector icon + tertiary = Color(0xFF006638), // Downloaded badge + onTertiary = Color(0xFFFFFFFF), // Downloaded badge text + tertiaryContainer = Color(0xFF00894b), + onTertiaryContainer = Color(0xFF2D1600), + background = Color(0xFFFFFBFF), + onBackground = Color(0xFF1C1B1F), + surface = Color(0xFFFFFBFF), + onSurface = Color(0xFF1C1B1F), + surfaceVariant = Color(0xFFF9E6F1), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF524346), + surfaceTint = Color(0xFFBB0054), + inverseSurface = Color(0xFF313033), + inverseOnSurface = Color(0xFFF4F0F4), + outline = Color(0xFF847376), + surfaceContainerLowest = Color(0xFFDAC0CD), + surfaceContainerLow = Color(0xFFE8D1DD), + surfaceContainer = Color(0xFFF9E6F1), // Navigation bar background + surfaceContainerHigh = Color(0xFFFCF3F8), + surfaceContainerHighest = Color(0xFFFEF9FC), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MonetColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MonetColorScheme.kt index adcbaf62fa..5e79256035 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MonetColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/MonetColorScheme.kt @@ -18,23 +18,27 @@ import com.google.android.material.color.utilities.QuantizerCelebi import com.google.android.material.color.utilities.SchemeContent import com.google.android.material.color.utilities.Score -internal class MonetColorScheme(context: Context) : BaseColorScheme() { - - private val monet = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MonetSystemColorScheme(context) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - val seed = WallpaperManager.getInstance(context) - .getWallpaperColors(WallpaperManager.FLAG_SYSTEM) - ?.primaryColor - ?.toArgb() - if (seed != null) { - MonetCompatColorScheme(context, seed) +internal class MonetColorScheme( + context: Context, +) : BaseColorScheme() { + private val monet = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + MonetSystemColorScheme(context) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + val seed = + WallpaperManager + .getInstance(context) + .getWallpaperColors(WallpaperManager.FLAG_SYSTEM) + ?.primaryColor + ?.toArgb() + if (seed != null) { + MonetCompatColorScheme(context, seed) + } else { + TachiyomiColorScheme + } } else { TachiyomiColorScheme } - } else { - TachiyomiColorScheme - } override val darkScheme get() = monet.darkScheme @@ -50,20 +54,25 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() { val height = bitmap.height val bitmapPixels = IntArray(width * height) bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height) - return Score.score(QuantizerCelebi.quantize(bitmapPixels, 128), 1, 0)[0] + return Score + .score(QuantizerCelebi.quantize(bitmapPixels, 128), 1, 0)[0] .takeIf { it != 0 } // Don't take fallback color } } } @RequiresApi(Build.VERSION_CODES.S) -private class MonetSystemColorScheme(context: Context) : BaseColorScheme() { +private class MonetSystemColorScheme( + context: Context, +) : BaseColorScheme() { override val lightScheme = dynamicLightColorScheme(context) override val darkScheme = dynamicDarkColorScheme(context) } -private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() { - +private class MonetCompatColorScheme( + context: Context, + seed: Int, +) : BaseColorScheme() { override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false) override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true) @@ -71,16 +80,21 @@ private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorSch private fun Int.toComposeColor(): Color = Color(this) @SuppressLint("PrivateResource", "RestrictedApi") - private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme { - val scheme = SchemeContent( - Hct.fromInt(seed), - dark, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - context.getSystemService()?.contrast?.toDouble() ?: 0.0 - } else { - 0.0 - }, - ) + private fun generateColorSchemeFromSeed( + context: Context, + seed: Int, + dark: Boolean, + ): ColorScheme { + val scheme = + SchemeContent( + Hct.fromInt(seed), + dark, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + context.getSystemService()?.contrast?.toDouble() ?: 0.0 + } else { + 0.0 + }, + ) val dynamicColors = MaterialDynamicColors() return ColorScheme( primary = dynamicColors.primary().getArgb(scheme).toComposeColor(), diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/NordColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/NordColorScheme.kt index 84fe7b49fa..df59ee5092 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/NordColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/NordColorScheme.kt @@ -10,73 +10,74 @@ import androidx.compose.ui.graphics.Color * for the light theme, the primary color is switched with the tertiary for better contrast in some case */ internal object NordColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFF88C0D0), + onPrimary = Color(0xFF2E3440), + primaryContainer = Color(0xFF88C0D0), + onPrimaryContainer = Color(0xFF2E3440), + inversePrimary = Color(0xFF397E91), + secondary = Color(0xFF81A1C1), // Unread badge + onSecondary = Color(0xFF2E3440), // Unread badge text + secondaryContainer = Color(0xFF506275), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF88C0D0), // Navigation bar selector icon + tertiary = Color(0xFF5E81AC), // Downloaded badge + onTertiary = Color(0xFF000000), // Downloaded badge text + tertiaryContainer = Color(0xFF5E81AC), + onTertiaryContainer = Color(0xFF000000), + background = Color(0xFF2E3440), + onBackground = Color(0xFFECEFF4), + surface = Color(0xFF2E3440), + onSurface = Color(0xFFECEFF4), + surfaceVariant = Color(0xFF414C5C), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFECEFF4), + surfaceTint = Color(0xFF88C0D0), + inverseSurface = Color(0xFFD8DEE9), + inverseOnSurface = Color(0xFF2E3440), + outline = Color(0xFF6d717b), + outlineVariant = Color(0xFF90939a), + onError = Color(0xFF2E3440), + errorContainer = Color(0xFFBF616A), + onErrorContainer = Color(0xFF000000), + surfaceContainerLowest = Color(0xFF373F4D), + surfaceContainerLow = Color(0xFF3E4756), + surfaceContainer = Color(0xFF414C5C), + surfaceContainerHigh = Color(0xFF4E5766), + surfaceContainerHighest = Color(0xFF505968), // Navigation bar background + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFF88C0D0), - onPrimary = Color(0xFF2E3440), - primaryContainer = Color(0xFF88C0D0), - onPrimaryContainer = Color(0xFF2E3440), - inversePrimary = Color(0xFF397E91), - secondary = Color(0xFF81A1C1), // Unread badge - onSecondary = Color(0xFF2E3440), // Unread badge text - secondaryContainer = Color(0xFF506275), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF88C0D0), // Navigation bar selector icon - tertiary = Color(0xFF5E81AC), // Downloaded badge - onTertiary = Color(0xFF000000), // Downloaded badge text - tertiaryContainer = Color(0xFF5E81AC), - onTertiaryContainer = Color(0xFF000000), - background = Color(0xFF2E3440), - onBackground = Color(0xFFECEFF4), - surface = Color(0xFF2E3440), - onSurface = Color(0xFFECEFF4), - surfaceVariant = Color(0xFF414C5C), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFECEFF4), - surfaceTint = Color(0xFF88C0D0), - inverseSurface = Color(0xFFD8DEE9), - inverseOnSurface = Color(0xFF2E3440), - outline = Color(0xFF6d717b), - outlineVariant = Color(0xFF90939a), - onError = Color(0xFF2E3440), - errorContainer = Color(0xFFBF616A), - onErrorContainer = Color(0xFF000000), - surfaceContainerLowest = Color(0xFF373F4D), - surfaceContainerLow = Color(0xFF3E4756), - surfaceContainer = Color(0xFF414C5C), - surfaceContainerHigh = Color(0xFF4E5766), - surfaceContainerHighest = Color(0xFF505968), // Navigation bar background - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFF5E81AC), - onPrimary = Color(0xFF000000), - primaryContainer = Color(0xFF5E81AC), - onPrimaryContainer = Color(0xFF000000), - inversePrimary = Color(0xFF8CA8CD), - secondary = Color(0xFF81A1C1), // Unread badge - onSecondary = Color(0xFF2E3440), // Unread badge text - secondaryContainer = Color(0xFF91B4D7), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF2E3440), // Navigation bar selector icon - tertiary = Color(0xFF88C0D0), // Downloaded badge - onTertiary = Color(0xFF2E3440), // Downloaded badge text - tertiaryContainer = Color(0xFF88C0D0), - onTertiaryContainer = Color(0xFF2E3440), - background = Color(0xFFECEFF4), - onBackground = Color(0xFF2E3440), - surface = Color(0xFFE5E9F0), - onSurface = Color(0xFF2E3440), - surfaceVariant = Color(0xFFDAE0EA), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF2E3440), - surfaceTint = Color(0xFF5E81AC), - inverseSurface = Color(0xFF3B4252), - inverseOnSurface = Color(0xFFECEFF4), - outline = Color(0xFF2E3440), - onError = Color(0xFFECEFF4), - errorContainer = Color(0xFFBF616A), - onErrorContainer = Color(0xFF000000), - surfaceContainerLowest = Color(0xFFD1D7E0), - surfaceContainerLow = Color(0xFFD6DCE6), - surfaceContainer = Color(0xFFDAE0EA), // Navigation bar background - surfaceContainerHigh = Color(0xFFE9EDF3), - surfaceContainerHighest = Color(0xFFF2F4F8), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFF5E81AC), + onPrimary = Color(0xFF000000), + primaryContainer = Color(0xFF5E81AC), + onPrimaryContainer = Color(0xFF000000), + inversePrimary = Color(0xFF8CA8CD), + secondary = Color(0xFF81A1C1), // Unread badge + onSecondary = Color(0xFF2E3440), // Unread badge text + secondaryContainer = Color(0xFF91B4D7), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF2E3440), // Navigation bar selector icon + tertiary = Color(0xFF88C0D0), // Downloaded badge + onTertiary = Color(0xFF2E3440), // Downloaded badge text + tertiaryContainer = Color(0xFF88C0D0), + onTertiaryContainer = Color(0xFF2E3440), + background = Color(0xFFECEFF4), + onBackground = Color(0xFF2E3440), + surface = Color(0xFFE5E9F0), + onSurface = Color(0xFF2E3440), + surfaceVariant = Color(0xFFDAE0EA), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF2E3440), + surfaceTint = Color(0xFF5E81AC), + inverseSurface = Color(0xFF3B4252), + inverseOnSurface = Color(0xFFECEFF4), + outline = Color(0xFF2E3440), + onError = Color(0xFFECEFF4), + errorContainer = Color(0xFFBF616A), + onErrorContainer = Color(0xFF000000), + surfaceContainerLowest = Color(0xFFD1D7E0), + surfaceContainerLow = Color(0xFFD6DCE6), + surfaceContainer = Color(0xFFDAE0EA), // Navigation bar background + surfaceContainerHigh = Color(0xFFE9EDF3), + surfaceContainerHighest = Color(0xFFF2F4F8), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/StrawberryColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/StrawberryColorScheme.kt index e1b096e259..9f97e4ddd5 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/StrawberryColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/StrawberryColorScheme.kt @@ -16,80 +16,81 @@ import androidx.compose.ui.graphics.Color * Neutral #655C5C */ internal object StrawberryColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFFFFB2B8), + onPrimary = Color(0xFF67001D), + primaryContainer = Color(0xFFD53855), + onPrimaryContainer = Color(0xFFFFFFFF), + secondary = Color(0xFFED4A65), // Unread badge + onSecondary = Color(0xFF201A1A), // Unread badge text + secondaryContainer = Color(0xFF91002A), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFFFFFFF), // Navigation bar selector icon + tertiary = Color(0xFFE8C08E), // Downloaded badge + onTertiary = Color(0xFF201A1A), // Downloaded badge text + tertiaryContainer = Color(0xFF775930), + onTertiaryContainer = Color(0xFFFFF7F1), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690005), + errorContainer = Color(0xFF93000A), + onErrorContainer = Color(0xFFFFDAD6), + background = Color(0xFF201A1A), + onBackground = Color(0xFFF7DCDD), + surface = Color(0xFF201A1A), + onSurface = Color(0xFFF7DCDD), + surfaceVariant = Color(0xFF322727), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFE1BEC0), + outline = Color(0xFFA9898B), + outlineVariant = Color(0xFF594042), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFFF7DCDD), + inverseOnSurface = Color(0xFF3D2C2D), + inversePrimary = Color(0xFFB61F40), + surfaceDim = Color(0xFF1D1011), + surfaceBright = Color(0xFF463536), + surfaceContainerLowest = Color(0xFF2C2222), + surfaceContainerLow = Color(0xFF302525), + surfaceContainer = Color(0xFF322727), // Navigation bar background + surfaceContainerHigh = Color(0xFF3C2F2F), + surfaceContainerHighest = Color(0xFF463737), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFFFFB2B8), - onPrimary = Color(0xFF67001D), - primaryContainer = Color(0xFFD53855), - onPrimaryContainer = Color(0xFFFFFFFF), - secondary = Color(0xFFED4A65), // Unread badge - onSecondary = Color(0xFF201A1A), // Unread badge text - secondaryContainer = Color(0xFF91002A), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFFFFFFF), // Navigation bar selector icon - tertiary = Color(0xFFE8C08E), // Downloaded badge - onTertiary = Color(0xFF201A1A), // Downloaded badge text - tertiaryContainer = Color(0xFF775930), - onTertiaryContainer = Color(0xFFFFF7F1), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - background = Color(0xFF201A1A), - onBackground = Color(0xFFF7DCDD), - surface = Color(0xFF201A1A), - onSurface = Color(0xFFF7DCDD), - surfaceVariant = Color(0xFF322727), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFE1BEC0), - outline = Color(0xFFA9898B), - outlineVariant = Color(0xFF594042), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFFF7DCDD), - inverseOnSurface = Color(0xFF3D2C2D), - inversePrimary = Color(0xFFB61F40), - surfaceDim = Color(0xFF1D1011), - surfaceBright = Color(0xFF463536), - surfaceContainerLowest = Color(0xFF2C2222), - surfaceContainerLow = Color(0xFF302525), - surfaceContainer = Color(0xFF322727), // Navigation bar background - surfaceContainerHigh = Color(0xFF3C2F2F), - surfaceContainerHighest = Color(0xFF463737), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFFA10833), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFD53855), - onPrimaryContainer = Color(0xFFFFFFFF), - secondary = Color(0xFFA10833), // Unread badge - onSecondary = Color(0xFFFFFFFF), // Unread badge text - secondaryContainer = Color(0xFFD53855), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFF6EAED), // Navigation bar selector icon - tertiary = Color(0xFF5F441D), // Downloaded badge - onTertiary = Color(0xFFFFFFFF), // Downloaded badge text - tertiaryContainer = Color(0xFF87683D), - onTertiaryContainer = Color(0xFFFFFFFF), - error = Color(0xFFBA1A1A), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF410002), - background = Color(0xFFFAFAFA), - onBackground = Color(0xFF261819), - surface = Color(0xFFFAFAFA), - onSurface = Color(0xFF261819), - surfaceVariant = Color(0xFFF6EAED), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF594042), - outline = Color(0xFF8D7071), - outlineVariant = Color(0xFFE1BEC0), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFF3D2C2D), - inverseOnSurface = Color(0xFFFFECED), - inversePrimary = Color(0xFFFFB2B8), - surfaceDim = Color(0xFFEED4D5), - surfaceBright = Color(0xFFFFF8F7), - surfaceContainerLowest = Color(0xFFF7DCDD), - surfaceContainerLow = Color(0xFFFDE2E3), - surfaceContainer = Color(0xFFF6EAED), // Navigation bar background - surfaceContainerHigh = Color(0xFFFFF0F0), - surfaceContainerHighest = Color(0xFFFFFFFF), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFFA10833), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFD53855), + onPrimaryContainer = Color(0xFFFFFFFF), + secondary = Color(0xFFA10833), // Unread badge + onSecondary = Color(0xFFFFFFFF), // Unread badge text + secondaryContainer = Color(0xFFD53855), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFF6EAED), // Navigation bar selector icon + tertiary = Color(0xFF5F441D), // Downloaded badge + onTertiary = Color(0xFFFFFFFF), // Downloaded badge text + tertiaryContainer = Color(0xFF87683D), + onTertiaryContainer = Color(0xFFFFFFFF), + error = Color(0xFFBA1A1A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFFDAD6), + onErrorContainer = Color(0xFF410002), + background = Color(0xFFFAFAFA), + onBackground = Color(0xFF261819), + surface = Color(0xFFFAFAFA), + onSurface = Color(0xFF261819), + surfaceVariant = Color(0xFFF6EAED), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF594042), + outline = Color(0xFF8D7071), + outlineVariant = Color(0xFFE1BEC0), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFF3D2C2D), + inverseOnSurface = Color(0xFFFFECED), + inversePrimary = Color(0xFFFFB2B8), + surfaceDim = Color(0xFFEED4D5), + surfaceBright = Color(0xFFFFF8F7), + surfaceContainerLowest = Color(0xFFF7DCDD), + surfaceContainerLow = Color(0xFFFDE2E3), + surfaceContainer = Color(0xFFF6EAED), // Navigation bar background + surfaceContainerHigh = Color(0xFFFFF0F0), + surfaceContainerHighest = Color(0xFFFFFFFF), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TachiyomiColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TachiyomiColorScheme.kt index 5faeb7df01..b0763287a8 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TachiyomiColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TachiyomiColorScheme.kt @@ -15,76 +15,77 @@ import androidx.compose.ui.graphics.Color * Neutral #919094 */ internal object TachiyomiColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFFB0C6FF), + onPrimary = Color(0xFF002D6E), + primaryContainer = Color(0xFF00429B), + onPrimaryContainer = Color(0xFFD9E2FF), + inversePrimary = Color(0xFF0058CA), + secondary = Color(0xFFB0C6FF), // Unread badge + onSecondary = Color(0xFF002D6E), // Unread badge text + secondaryContainer = Color(0xFF00429B), // Navigation bar selector pill & pro + onSecondaryContainer = Color(0xFFD9E2FF), // Navigation bar selector icon + tertiary = Color(0xFF7ADC77), // Downloaded badge + onTertiary = Color(0xFF003909), // Downloaded badge text + tertiaryContainer = Color(0xFF005312), + onTertiaryContainer = Color(0xFF95F990), + background = Color(0xFF1B1B1F), + onBackground = Color(0xFFE3E2E6), + surface = Color(0xFF1B1B1F), + onSurface = Color(0xFFE3E2E6), + surfaceVariant = Color(0xFF211F26), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFC5C6D0), + surfaceTint = Color(0xFFB0C6FF), + inverseSurface = Color(0xFFE3E2E6), + inverseOnSurface = Color(0xFF1B1B1F), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690005), + errorContainer = Color(0xFF93000A), + onErrorContainer = Color(0xFFFFDAD6), + outline = Color(0xFF8F9099), + outlineVariant = Color(0xFF44464F), + surfaceContainerLowest = Color(0xFF1A181D), + surfaceContainerLow = Color(0xFF1E1C22), + surfaceContainer = Color(0xFF211F26), // Navigation bar background + surfaceContainerHigh = Color(0xFF292730), + surfaceContainerHighest = Color(0xFF302E38), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFFB0C6FF), - onPrimary = Color(0xFF002D6E), - primaryContainer = Color(0xFF00429B), - onPrimaryContainer = Color(0xFFD9E2FF), - inversePrimary = Color(0xFF0058CA), - secondary = Color(0xFFB0C6FF), // Unread badge - onSecondary = Color(0xFF002D6E), // Unread badge text - secondaryContainer = Color(0xFF00429B), // Navigation bar selector pill & pro - onSecondaryContainer = Color(0xFFD9E2FF), // Navigation bar selector icon - tertiary = Color(0xFF7ADC77), // Downloaded badge - onTertiary = Color(0xFF003909), // Downloaded badge text - tertiaryContainer = Color(0xFF005312), - onTertiaryContainer = Color(0xFF95F990), - background = Color(0xFF1B1B1F), - onBackground = Color(0xFFE3E2E6), - surface = Color(0xFF1B1B1F), - onSurface = Color(0xFFE3E2E6), - surfaceVariant = Color(0xFF211F26), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFC5C6D0), - surfaceTint = Color(0xFFB0C6FF), - inverseSurface = Color(0xFFE3E2E6), - inverseOnSurface = Color(0xFF1B1B1F), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - outline = Color(0xFF8F9099), - outlineVariant = Color(0xFF44464F), - surfaceContainerLowest = Color(0xFF1A181D), - surfaceContainerLow = Color(0xFF1E1C22), - surfaceContainer = Color(0xFF211F26), // Navigation bar background - surfaceContainerHigh = Color(0xFF292730), - surfaceContainerHighest = Color(0xFF302E38), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFF0058CA), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFD9E2FF), - onPrimaryContainer = Color(0xFF001945), - inversePrimary = Color(0xFFB0C6FF), - secondary = Color(0xFF0058CA), // Unread badge - onSecondary = Color(0xFFFFFFFF), // Unread badge text - secondaryContainer = Color(0xFFD9E2FF), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF001945), // Navigation bar selector icon - tertiary = Color(0xFF006E1B), // Downloaded badge - onTertiary = Color(0xFFFFFFFF), // Downloaded badge text - tertiaryContainer = Color(0xFF95F990), - onTertiaryContainer = Color(0xFF002203), - background = Color(0xFFFEFBFF), - onBackground = Color(0xFF1B1B1F), - surface = Color(0xFFFEFBFF), - onSurface = Color(0xFF1B1B1F), - surfaceVariant = Color(0xFFF3EDF7), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF44464F), - surfaceTint = Color(0xFF0058CA), - inverseSurface = Color(0xFF303034), - inverseOnSurface = Color(0xFFF2F0F4), - error = Color(0xFFBA1A1A), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF410002), - outline = Color(0xFF757780), - outlineVariant = Color(0xFFC5C6D0), - surfaceContainerLowest = Color(0xFFF5F1F8), - surfaceContainerLow = Color(0xFFF7F2FA), - surfaceContainer = Color(0xFFF3EDF7), // Navigation bar background - surfaceContainerHigh = Color(0xFFFCF7FF), - surfaceContainerHighest = Color(0xFFFCF7FF), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFF0058CA), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFD9E2FF), + onPrimaryContainer = Color(0xFF001945), + inversePrimary = Color(0xFFB0C6FF), + secondary = Color(0xFF0058CA), // Unread badge + onSecondary = Color(0xFFFFFFFF), // Unread badge text + secondaryContainer = Color(0xFFD9E2FF), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF001945), // Navigation bar selector icon + tertiary = Color(0xFF006E1B), // Downloaded badge + onTertiary = Color(0xFFFFFFFF), // Downloaded badge text + tertiaryContainer = Color(0xFF95F990), + onTertiaryContainer = Color(0xFF002203), + background = Color(0xFFFEFBFF), + onBackground = Color(0xFF1B1B1F), + surface = Color(0xFFFEFBFF), + onSurface = Color(0xFF1B1B1F), + surfaceVariant = Color(0xFFF3EDF7), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF44464F), + surfaceTint = Color(0xFF0058CA), + inverseSurface = Color(0xFF303034), + inverseOnSurface = Color(0xFFF2F0F4), + error = Color(0xFFBA1A1A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFFDAD6), + onErrorContainer = Color(0xFF410002), + outline = Color(0xFF757780), + outlineVariant = Color(0xFFC5C6D0), + surfaceContainerLowest = Color(0xFFF5F1F8), + surfaceContainerLow = Color(0xFFF7F2FA), + surfaceContainer = Color(0xFFF3EDF7), // Navigation bar background + surfaceContainerHigh = Color(0xFFFCF7FF), + surfaceContainerHighest = Color(0xFFFCF7FF), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TakoColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TakoColorScheme.kt index 7988129638..2c6cd37efc 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TakoColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TakoColorScheme.kt @@ -16,66 +16,67 @@ import androidx.compose.ui.graphics.Color * Neutral #21212E */ internal object TakoColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFFF3B375), + onPrimary = Color(0xFF38294E), + primaryContainer = Color(0xFFF3B375), + onPrimaryContainer = Color(0xFF38294E), + inversePrimary = Color(0xFF84531E), + secondary = Color(0xFFF3B375), // Unread badge + onSecondary = Color(0xFF38294E), // Unread badge text + secondaryContainer = Color(0xFF5C4D4B), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFF3B375), // Navigation bar selector icon + tertiary = Color(0xFF66577E), // Downloaded badge + onTertiary = Color(0xFFF3B375), // Downloaded badge text + tertiaryContainer = Color(0xFF4E4065), + onTertiaryContainer = Color(0xFFEDDCFF), + background = Color(0xFF21212E), + onBackground = Color(0xFFE3E0F2), + surface = Color(0xFF21212E), + onSurface = Color(0xFFE3E0F2), + surfaceVariant = Color(0xFF2A2A3C), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFCBC4CE), + surfaceTint = Color(0xFF66577E), + inverseSurface = Color(0xFFE5E1E6), + inverseOnSurface = Color(0xFF1B1B1E), + outline = Color(0xFF958F99), + surfaceContainerLowest = Color(0xFF20202E), + surfaceContainerLow = Color(0xFF262636), + surfaceContainer = Color(0xFF2A2A3C), // Navigation bar background + surfaceContainerHigh = Color(0xFF303044), + surfaceContainerHighest = Color(0xFF36364D), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFFF3B375), - onPrimary = Color(0xFF38294E), - primaryContainer = Color(0xFFF3B375), - onPrimaryContainer = Color(0xFF38294E), - inversePrimary = Color(0xFF84531E), - secondary = Color(0xFFF3B375), // Unread badge - onSecondary = Color(0xFF38294E), // Unread badge text - secondaryContainer = Color(0xFF5C4D4B), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFF3B375), // Navigation bar selector icon - tertiary = Color(0xFF66577E), // Downloaded badge - onTertiary = Color(0xFFF3B375), // Downloaded badge text - tertiaryContainer = Color(0xFF4E4065), - onTertiaryContainer = Color(0xFFEDDCFF), - background = Color(0xFF21212E), - onBackground = Color(0xFFE3E0F2), - surface = Color(0xFF21212E), - onSurface = Color(0xFFE3E0F2), - surfaceVariant = Color(0xFF2A2A3C), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFCBC4CE), - surfaceTint = Color(0xFF66577E), - inverseSurface = Color(0xFFE5E1E6), - inverseOnSurface = Color(0xFF1B1B1E), - outline = Color(0xFF958F99), - surfaceContainerLowest = Color(0xFF20202E), - surfaceContainerLow = Color(0xFF262636), - surfaceContainer = Color(0xFF2A2A3C), // Navigation bar background - surfaceContainerHigh = Color(0xFF303044), - surfaceContainerHighest = Color(0xFF36364D), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFF66577E), - onPrimary = Color(0xFFF3B375), - primaryContainer = Color(0xFF66577E), - onPrimaryContainer = Color(0xFFF3B375), - inversePrimary = Color(0xFFD6BAFF), - secondary = Color(0xFF66577E), // Unread badge - onSecondary = Color(0xFFF3B375), // Unread badge text - secondaryContainer = Color(0xFFC8BED0), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF66577E), // Navigation bar selector icon - tertiary = Color(0xFFF3B375), // Downloaded badge - onTertiary = Color(0xFF574360), // Downloaded badge text - tertiaryContainer = Color(0xFFFDD6B0), - onTertiaryContainer = Color(0xFF221437), - background = Color(0xFFF7F5FF), - onBackground = Color(0xFF1B1B22), - surface = Color(0xFFF7F5FF), - onSurface = Color(0xFF1B1B22), - surfaceVariant = Color(0xFFE8E0EB), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF49454E), - surfaceTint = Color(0xFF66577E), - inverseSurface = Color(0xFF313033), - inverseOnSurface = Color(0xFFF3EFF4), - outline = Color(0xFF7A757E), - surfaceContainerLowest = Color(0xFFD7D0DA), - surfaceContainerLow = Color(0xFFDFD8E2), - surfaceContainer = Color(0xFFE8E0EB), // Navigation bar background - surfaceContainerHigh = Color(0xFFEEE6F1), - surfaceContainerHighest = Color(0xFFF7EEFA), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFF66577E), + onPrimary = Color(0xFFF3B375), + primaryContainer = Color(0xFF66577E), + onPrimaryContainer = Color(0xFFF3B375), + inversePrimary = Color(0xFFD6BAFF), + secondary = Color(0xFF66577E), // Unread badge + onSecondary = Color(0xFFF3B375), // Unread badge text + secondaryContainer = Color(0xFFC8BED0), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF66577E), // Navigation bar selector icon + tertiary = Color(0xFFF3B375), // Downloaded badge + onTertiary = Color(0xFF574360), // Downloaded badge text + tertiaryContainer = Color(0xFFFDD6B0), + onTertiaryContainer = Color(0xFF221437), + background = Color(0xFFF7F5FF), + onBackground = Color(0xFF1B1B22), + surface = Color(0xFFF7F5FF), + onSurface = Color(0xFF1B1B22), + surfaceVariant = Color(0xFFE8E0EB), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF49454E), + surfaceTint = Color(0xFF66577E), + inverseSurface = Color(0xFF313033), + inverseOnSurface = Color(0xFFF3EFF4), + outline = Color(0xFF7A757E), + surfaceContainerLowest = Color(0xFFD7D0DA), + surfaceContainerLow = Color(0xFFDFD8E2), + surfaceContainer = Color(0xFFE8E0EB), // Navigation bar background + surfaceContainerHigh = Color(0xFFEEE6F1), + surfaceContainerHighest = Color(0xFFF7EEFA), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TealTurqoiseColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TealTurqoiseColorScheme.kt index 28811fa6a3..6f5c17b41a 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TealTurqoiseColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TealTurqoiseColorScheme.kt @@ -8,66 +8,67 @@ import androidx.compose.ui.graphics.Color * Colors for Teal Turqoise theme */ internal object TealTurqoiseColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFF40E0D0), + onPrimary = Color(0xFF000000), + primaryContainer = Color(0xFF40E0D0), + onPrimaryContainer = Color(0xFF000000), + inversePrimary = Color(0xFF008080), + secondary = Color(0xFF40E0D0), // Unread badge + onSecondary = Color(0xFF000000), // Unread badge text + secondaryContainer = Color(0xFF18544E), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF40E0D0), // Navigation bar selector icon + tertiary = Color(0xFFBF1F2F), // Downloaded badge + onTertiary = Color(0xFFFFFFFF), // Downloaded badge text + tertiaryContainer = Color(0xFF200508), + onTertiaryContainer = Color(0xFFBF1F2F), + background = Color(0xFF202125), + onBackground = Color(0xFFDFDEDA), + surface = Color(0xFF202125), + onSurface = Color(0xFFDFDEDA), + surfaceVariant = Color(0xFF233133), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFDFDEDA), + surfaceTint = Color(0xFF40E0D0), + inverseSurface = Color(0xFFDFDEDA), + inverseOnSurface = Color(0xFF202125), + outline = Color(0xFF899391), + surfaceContainerLowest = Color(0xFF202C2E), + surfaceContainerLow = Color(0xFF222F31), + surfaceContainer = Color(0xFF233133), // Navigation bar background + surfaceContainerHigh = Color(0xFF28383A), + surfaceContainerHighest = Color(0xFF2F4244), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFF40E0D0), - onPrimary = Color(0xFF000000), - primaryContainer = Color(0xFF40E0D0), - onPrimaryContainer = Color(0xFF000000), - inversePrimary = Color(0xFF008080), - secondary = Color(0xFF40E0D0), // Unread badge - onSecondary = Color(0xFF000000), // Unread badge text - secondaryContainer = Color(0xFF18544E), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF40E0D0), // Navigation bar selector icon - tertiary = Color(0xFFBF1F2F), // Downloaded badge - onTertiary = Color(0xFFFFFFFF), // Downloaded badge text - tertiaryContainer = Color(0xFF200508), - onTertiaryContainer = Color(0xFFBF1F2F), - background = Color(0xFF202125), - onBackground = Color(0xFFDFDEDA), - surface = Color(0xFF202125), - onSurface = Color(0xFFDFDEDA), - surfaceVariant = Color(0xFF233133), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFDFDEDA), - surfaceTint = Color(0xFF40E0D0), - inverseSurface = Color(0xFFDFDEDA), - inverseOnSurface = Color(0xFF202125), - outline = Color(0xFF899391), - surfaceContainerLowest = Color(0xFF202C2E), - surfaceContainerLow = Color(0xFF222F31), - surfaceContainer = Color(0xFF233133), // Navigation bar background - surfaceContainerHigh = Color(0xFF28383A), - surfaceContainerHighest = Color(0xFF2F4244), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFF008080), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFF008080), - onPrimaryContainer = Color(0xFFFFFFFF), - inversePrimary = Color(0xFF40E0D0), - secondary = Color(0xFF008080), // Unread badge text - onSecondary = Color(0xFFFFFFFF), // Unread badge text - secondaryContainer = Color(0xFFCFE5E4), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF008080), // Navigation bar selector icon - tertiary = Color(0xFFFF7F7F), // Downloaded badge - onTertiary = Color(0xFF000000), // Downloaded badge text - tertiaryContainer = Color(0xFF2A1616), - onTertiaryContainer = Color(0xFFFF7F7F), - background = Color(0xFFFAFAFA), - onBackground = Color(0xFF050505), - surface = Color(0xFFFAFAFA), - onSurface = Color(0xFF050505), - surfaceVariant = Color(0xFFEBF3F1), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF050505), - surfaceTint = Color(0xFFBFDFDF), - inverseSurface = Color(0xFF050505), - inverseOnSurface = Color(0xFFFAFAFA), - outline = Color(0xFF6F7977), - surfaceContainerLowest = Color(0xFFE1E9E7), - surfaceContainerLow = Color(0xFFE6EEEC), - surfaceContainer = Color(0xFFEBF3F1), // Navigation bar background - surfaceContainerHigh = Color(0xFFF0F8F6), - surfaceContainerHighest = Color(0xFFF7FFFD), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFF008080), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFF008080), + onPrimaryContainer = Color(0xFFFFFFFF), + inversePrimary = Color(0xFF40E0D0), + secondary = Color(0xFF008080), // Unread badge text + onSecondary = Color(0xFFFFFFFF), // Unread badge text + secondaryContainer = Color(0xFFCFE5E4), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF008080), // Navigation bar selector icon + tertiary = Color(0xFFFF7F7F), // Downloaded badge + onTertiary = Color(0xFF000000), // Downloaded badge text + tertiaryContainer = Color(0xFF2A1616), + onTertiaryContainer = Color(0xFFFF7F7F), + background = Color(0xFFFAFAFA), + onBackground = Color(0xFF050505), + surface = Color(0xFFFAFAFA), + onSurface = Color(0xFF050505), + surfaceVariant = Color(0xFFEBF3F1), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF050505), + surfaceTint = Color(0xFFBFDFDF), + inverseSurface = Color(0xFF050505), + inverseOnSurface = Color(0xFFFAFAFA), + outline = Color(0xFF6F7977), + surfaceContainerLowest = Color(0xFFE1E9E7), + surfaceContainerLow = Color(0xFFE6EEEC), + surfaceContainer = Color(0xFFEBF3F1), // Navigation bar background + surfaceContainerHigh = Color(0xFFF0F8F6), + surfaceContainerHighest = Color(0xFFF7FFFD), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TidalWaveColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TidalWaveColorScheme.kt index 09dc248c0a..d3d4113f0d 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TidalWaveColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/TidalWaveColorScheme.kt @@ -15,66 +15,67 @@ import androidx.compose.ui.graphics.Color * Neutral #16151D */ internal object TidalWaveColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFF5ed4fc), + onPrimary = Color(0xFF003544), + primaryContainer = Color(0xFF004d61), + onPrimaryContainer = Color(0xFFb8eaff), + inversePrimary = Color(0xFFa12b03), + secondary = Color(0xFF5ed4fc), // Unread badge + onSecondary = Color(0xFF003544), // Unread badge text + secondaryContainer = Color(0xFF004d61), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFb8eaff), // Navigation bar selector icon + tertiary = Color(0xFF92f7bc), // Downloaded badge + onTertiary = Color(0xFF001c3b), // Downloaded badge text + tertiaryContainer = Color(0xFFc3fada), + onTertiaryContainer = Color(0xFF78ffd6), + background = Color(0xFF001c3b), + onBackground = Color(0xFFd5e3ff), + surface = Color(0xFF001c3b), + onSurface = Color(0xFFd5e3ff), + surfaceVariant = Color(0xFF082b4b), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFbfc8cc), + surfaceTint = Color(0xFF5ed4fc), + inverseSurface = Color(0xFFffe3c4), + inverseOnSurface = Color(0xFF001c3b), + outline = Color(0xFF8a9296), + surfaceContainerLowest = Color(0xFF072642), + surfaceContainerLow = Color(0xFF072947), + surfaceContainer = Color(0xFF082b4b), // Navigation bar background + surfaceContainerHigh = Color(0xFF093257), + surfaceContainerHighest = Color(0xFF0A3861), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFF5ed4fc), - onPrimary = Color(0xFF003544), - primaryContainer = Color(0xFF004d61), - onPrimaryContainer = Color(0xFFb8eaff), - inversePrimary = Color(0xFFa12b03), - secondary = Color(0xFF5ed4fc), // Unread badge - onSecondary = Color(0xFF003544), // Unread badge text - secondaryContainer = Color(0xFF004d61), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFb8eaff), // Navigation bar selector icon - tertiary = Color(0xFF92f7bc), // Downloaded badge - onTertiary = Color(0xFF001c3b), // Downloaded badge text - tertiaryContainer = Color(0xFFc3fada), - onTertiaryContainer = Color(0xFF78ffd6), - background = Color(0xFF001c3b), - onBackground = Color(0xFFd5e3ff), - surface = Color(0xFF001c3b), - onSurface = Color(0xFFd5e3ff), - surfaceVariant = Color(0xFF082b4b), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFbfc8cc), - surfaceTint = Color(0xFF5ed4fc), - inverseSurface = Color(0xFFffe3c4), - inverseOnSurface = Color(0xFF001c3b), - outline = Color(0xFF8a9296), - surfaceContainerLowest = Color(0xFF072642), - surfaceContainerLow = Color(0xFF072947), - surfaceContainer = Color(0xFF082b4b), // Navigation bar background - surfaceContainerHigh = Color(0xFF093257), - surfaceContainerHighest = Color(0xFF0A3861), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFF006780), - onPrimary = Color(0xFFffffff), - primaryContainer = Color(0xFFB4D4DF), - onPrimaryContainer = Color(0xFF001f28), - inversePrimary = Color(0xFFff987f), - secondary = Color(0xFF006780), // Unread badge - onSecondary = Color(0xFFffffff), // Unread badge text - secondaryContainer = Color(0xFF9AE1FF), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF001f28), // Navigation bar selector icon - tertiary = Color(0xFF92f7bc), // Downloaded badge - onTertiary = Color(0xFF001c3b), // Downloaded badge text - tertiaryContainer = Color(0xFFc3fada), - onTertiaryContainer = Color(0xFF78ffd6), - background = Color(0xFFfdfbff), - onBackground = Color(0xFF001c3b), - surface = Color(0xFFfdfbff), - onSurface = Color(0xFF001c3b), - surfaceVariant = Color(0xFFe8eff5), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF40484c), - surfaceTint = Color(0xFF006780), - inverseSurface = Color(0xFF020400), - inverseOnSurface = Color(0xFFffe3c4), - outline = Color(0xFF70787c), - surfaceContainerLowest = Color(0xFFe2e8ec), - surfaceContainerLow = Color(0xFFe5ecf1), - surfaceContainer = Color(0xFFe8eff5), // Navigation bar background - surfaceContainerHigh = Color(0xFFedf4fA), - surfaceContainerHighest = Color(0xFFf5faff), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFF006780), + onPrimary = Color(0xFFffffff), + primaryContainer = Color(0xFFB4D4DF), + onPrimaryContainer = Color(0xFF001f28), + inversePrimary = Color(0xFFff987f), + secondary = Color(0xFF006780), // Unread badge + onSecondary = Color(0xFFffffff), // Unread badge text + secondaryContainer = Color(0xFF9AE1FF), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF001f28), // Navigation bar selector icon + tertiary = Color(0xFF92f7bc), // Downloaded badge + onTertiary = Color(0xFF001c3b), // Downloaded badge text + tertiaryContainer = Color(0xFFc3fada), + onTertiaryContainer = Color(0xFF78ffd6), + background = Color(0xFFfdfbff), + onBackground = Color(0xFF001c3b), + surface = Color(0xFFfdfbff), + onSurface = Color(0xFF001c3b), + surfaceVariant = Color(0xFFe8eff5), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF40484c), + surfaceTint = Color(0xFF006780), + inverseSurface = Color(0xFF020400), + inverseOnSurface = Color(0xFFffe3c4), + outline = Color(0xFF70787c), + surfaceContainerLowest = Color(0xFFe2e8ec), + surfaceContainerLow = Color(0xFFe5ecf1), + surfaceContainer = Color(0xFFe8eff5), // Navigation bar background + surfaceContainerHigh = Color(0xFFedf4fA), + surfaceContainerHighest = Color(0xFFf5faff), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/YinYangColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/YinYangColorScheme.kt index 1e9b129785..e103247df3 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/YinYangColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/YinYangColorScheme.kt @@ -10,66 +10,67 @@ import androidx.compose.ui.graphics.Color * M3 colors generated by yours truly + tweaked manually */ internal object YinYangColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFFFFFFFF), + onPrimary = Color(0xFF5A5A5A), + primaryContainer = Color(0xFFFFFFFF), + onPrimaryContainer = Color(0xFF000000), + inversePrimary = Color(0xFFCECECE), + secondary = Color(0xFFFFFFFF), // Unread badge + onSecondary = Color(0xFF5A5A5A), // Unread badge text + secondaryContainer = Color(0xFF717171), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFE4E4E4), // Navigation bar selector icon + tertiary = Color(0xFF000000), // Downloaded badge + onTertiary = Color(0xFFFFFFFF), // Downloaded badge text + tertiaryContainer = Color(0xFF00419E), + onTertiaryContainer = Color(0xFFD8E2FF), + background = Color(0xFF1E1E1E), + onBackground = Color(0xFFE6E6E6), + surface = Color(0xFF1E1E1E), + onSurface = Color(0xFFE6E6E6), + surfaceVariant = Color(0xFF313131), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFD1D1D1), + surfaceTint = Color(0xFFFFFFFF), + inverseSurface = Color(0xFFE6E6E6), + inverseOnSurface = Color(0xFF1E1E1E), + outline = Color(0xFF999999), + surfaceContainerLowest = Color(0xFF2A2A2A), + surfaceContainerLow = Color(0xFF2D2D2D), + surfaceContainer = Color(0xFF313131), // Navigation bar background + surfaceContainerHigh = Color(0xFF383838), + surfaceContainerHighest = Color(0xFF3F3F3F), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFFFFFFFF), - onPrimary = Color(0xFF5A5A5A), - primaryContainer = Color(0xFFFFFFFF), - onPrimaryContainer = Color(0xFF000000), - inversePrimary = Color(0xFFCECECE), - secondary = Color(0xFFFFFFFF), // Unread badge - onSecondary = Color(0xFF5A5A5A), // Unread badge text - secondaryContainer = Color(0xFF717171), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFE4E4E4), // Navigation bar selector icon - tertiary = Color(0xFF000000), // Downloaded badge - onTertiary = Color(0xFFFFFFFF), // Downloaded badge text - tertiaryContainer = Color(0xFF00419E), - onTertiaryContainer = Color(0xFFD8E2FF), - background = Color(0xFF1E1E1E), - onBackground = Color(0xFFE6E6E6), - surface = Color(0xFF1E1E1E), - onSurface = Color(0xFFE6E6E6), - surfaceVariant = Color(0xFF313131), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFD1D1D1), - surfaceTint = Color(0xFFFFFFFF), - inverseSurface = Color(0xFFE6E6E6), - inverseOnSurface = Color(0xFF1E1E1E), - outline = Color(0xFF999999), - surfaceContainerLowest = Color(0xFF2A2A2A), - surfaceContainerLow = Color(0xFF2D2D2D), - surfaceContainer = Color(0xFF313131), // Navigation bar background - surfaceContainerHigh = Color(0xFF383838), - surfaceContainerHighest = Color(0xFF3F3F3F), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFF000000), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFF000000), - onPrimaryContainer = Color(0xFFFFFFFF), - inversePrimary = Color(0xFFA6A6A6), - secondary = Color(0xFF000000), // Unread badge - onSecondary = Color(0xFFFFFFFF), // Unread badge text - secondaryContainer = Color(0xFFDDDDDD), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF0C0C0C), // Navigation bar selector icon - tertiary = Color(0xFFFFFFFF), // Downloaded badge - onTertiary = Color(0xFF000000), // Downloaded badge text - tertiaryContainer = Color(0xFFD8E2FF), - onTertiaryContainer = Color(0xFF001947), - background = Color(0xFFFDFDFD), - onBackground = Color(0xFF222222), - surface = Color(0xFFFDFDFD), - onSurface = Color(0xFF222222), - surfaceVariant = Color(0xFFE8E8E8), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF515151), - surfaceTint = Color(0xFF000000), - inverseSurface = Color(0xFF333333), - inverseOnSurface = Color(0xFFF4F4F4), - outline = Color(0xFF838383), - surfaceContainerLowest = Color(0xFFCFCFCF), - surfaceContainerLow = Color(0xFFDADADA), - surfaceContainer = Color(0xFFE8E8E8), // Navigation bar background - surfaceContainerHigh = Color(0xFFECECEC), - surfaceContainerHighest = Color(0xFFEFEFEF), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFF000000), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFF000000), + onPrimaryContainer = Color(0xFFFFFFFF), + inversePrimary = Color(0xFFA6A6A6), + secondary = Color(0xFF000000), // Unread badge + onSecondary = Color(0xFFFFFFFF), // Unread badge text + secondaryContainer = Color(0xFFDDDDDD), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF0C0C0C), // Navigation bar selector icon + tertiary = Color(0xFFFFFFFF), // Downloaded badge + onTertiary = Color(0xFF000000), // Downloaded badge text + tertiaryContainer = Color(0xFFD8E2FF), + onTertiaryContainer = Color(0xFF001947), + background = Color(0xFFFDFDFD), + onBackground = Color(0xFF222222), + surface = Color(0xFFFDFDFD), + onSurface = Color(0xFF222222), + surfaceVariant = Color(0xFFE8E8E8), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF515151), + surfaceTint = Color(0xFF000000), + inverseSurface = Color(0xFF333333), + inverseOnSurface = Color(0xFFF4F4F4), + outline = Color(0xFF838383), + surfaceContainerLowest = Color(0xFFCFCFCF), + surfaceContainerLow = Color(0xFFDADADA), + surfaceContainer = Color(0xFFE8E8E8), // Navigation bar background + surfaceContainerHigh = Color(0xFFECECEC), + surfaceContainerHighest = Color(0xFFEFEFEF), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/YotsubaColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/YotsubaColorScheme.kt index 58007a6802..560b291cae 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/colorscheme/YotsubaColorScheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/YotsubaColorScheme.kt @@ -16,66 +16,67 @@ import androidx.compose.ui.graphics.Color * Neutral 0xFF655C5A */ internal object YotsubaColorScheme : BaseColorScheme() { + override val darkScheme = + darkColorScheme( + primary = Color(0xFFFFB59D), + onPrimary = Color(0xFF5F1600), + primaryContainer = Color(0xFF862200), + onPrimaryContainer = Color(0xFFFFDBCF), + inversePrimary = Color(0xFFAE3200), + secondary = Color(0xFFFFB59D), // Unread badge + onSecondary = Color(0xFF5F1600), // Unread badge text + secondaryContainer = Color(0xFF862200), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFFFFDBCF), // Navigation bar selector icon + tertiary = Color(0xFFD7C68D), // Downloaded badge + onTertiary = Color(0xFF3A2F05), // Downloaded badge text + tertiaryContainer = Color(0xFF524619), + onTertiaryContainer = Color(0xFFF5E2A7), + background = Color(0xFF211A18), + onBackground = Color(0xFFEDE0DD), + surface = Color(0xFF211A18), + onSurface = Color(0xFFEDE0DD), + surfaceVariant = Color(0xFF332723), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFFD8C2BC), + surfaceTint = Color(0xFFFFB59D), + inverseSurface = Color(0xFFEDE0DD), + inverseOnSurface = Color(0xFF211A18), + outline = Color(0xFFA08C87), + surfaceContainerLowest = Color(0xFF2E221F), + surfaceContainerLow = Color(0xFF312521), + surfaceContainer = Color(0xFF332723), // Navigation bar background + surfaceContainerHigh = Color(0xFF413531), + surfaceContainerHighest = Color(0xFF4C403D), + ) - override val darkScheme = darkColorScheme( - primary = Color(0xFFFFB59D), - onPrimary = Color(0xFF5F1600), - primaryContainer = Color(0xFF862200), - onPrimaryContainer = Color(0xFFFFDBCF), - inversePrimary = Color(0xFFAE3200), - secondary = Color(0xFFFFB59D), // Unread badge - onSecondary = Color(0xFF5F1600), // Unread badge text - secondaryContainer = Color(0xFF862200), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFFFFDBCF), // Navigation bar selector icon - tertiary = Color(0xFFD7C68D), // Downloaded badge - onTertiary = Color(0xFF3A2F05), // Downloaded badge text - tertiaryContainer = Color(0xFF524619), - onTertiaryContainer = Color(0xFFF5E2A7), - background = Color(0xFF211A18), - onBackground = Color(0xFFEDE0DD), - surface = Color(0xFF211A18), - onSurface = Color(0xFFEDE0DD), - surfaceVariant = Color(0xFF332723), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFFD8C2BC), - surfaceTint = Color(0xFFFFB59D), - inverseSurface = Color(0xFFEDE0DD), - inverseOnSurface = Color(0xFF211A18), - outline = Color(0xFFA08C87), - surfaceContainerLowest = Color(0xFF2E221F), - surfaceContainerLow = Color(0xFF312521), - surfaceContainer = Color(0xFF332723), // Navigation bar background - surfaceContainerHigh = Color(0xFF413531), - surfaceContainerHighest = Color(0xFF4C403D), - ) - - override val lightScheme = lightColorScheme( - primary = Color(0xFFAE3200), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFFFDBCF), - onPrimaryContainer = Color(0xFF3B0A00), - inversePrimary = Color(0xFFFFB59D), - secondary = Color(0xFFAE3200), // Unread badge - onSecondary = Color(0xFFFFFFFF), // Unread badge text - secondaryContainer = Color(0xFFEBCDC2), // Navigation bar selector pill & progress indicator (remaining) - onSecondaryContainer = Color(0xFF3B0A00), // Navigation bar selector icon - tertiary = Color(0xFF6B5E2F), // Downloaded badge - onTertiary = Color(0xFFFFFFFF), // Downloaded badge text - tertiaryContainer = Color(0xFFF5E2A7), - onTertiaryContainer = Color(0xFF231B00), - background = Color(0xFFFCFCFC), - onBackground = Color(0xFF211A18), - surface = Color(0xFFFCFCFC), - onSurface = Color(0xFF211A18), - surfaceVariant = Color(0xFFF6EBE7), // Navigation bar background (ThemePrefWidget) - onSurfaceVariant = Color(0xFF53433F), - surfaceTint = Color(0xFFAE3200), - inverseSurface = Color(0xFF362F2D), - inverseOnSurface = Color(0xFFFBEEEB), - outline = Color(0xFF85736E), - surfaceContainerLowest = Color(0xFFECE3E0), - surfaceContainerLow = Color(0xFFF1E7E4), - surfaceContainer = Color(0xFFF6EBE7), // Navigation bar background - surfaceContainerHigh = Color(0xFFFAF4F2), - surfaceContainerHighest = Color(0xFFFBF6F4), - ) + override val lightScheme = + lightColorScheme( + primary = Color(0xFFAE3200), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFFFDBCF), + onPrimaryContainer = Color(0xFF3B0A00), + inversePrimary = Color(0xFFFFB59D), + secondary = Color(0xFFAE3200), // Unread badge + onSecondary = Color(0xFFFFFFFF), // Unread badge text + secondaryContainer = Color(0xFFEBCDC2), // Navigation bar selector pill & progress indicator (remaining) + onSecondaryContainer = Color(0xFF3B0A00), // Navigation bar selector icon + tertiary = Color(0xFF6B5E2F), // Downloaded badge + onTertiary = Color(0xFFFFFFFF), // Downloaded badge text + tertiaryContainer = Color(0xFFF5E2A7), + onTertiaryContainer = Color(0xFF231B00), + background = Color(0xFFFCFCFC), + onBackground = Color(0xFF211A18), + surface = Color(0xFFFCFCFC), + onSurface = Color(0xFF211A18), + surfaceVariant = Color(0xFFF6EBE7), // Navigation bar background (ThemePrefWidget) + onSurfaceVariant = Color(0xFF53433F), + surfaceTint = Color(0xFFAE3200), + inverseSurface = Color(0xFF362F2D), + inverseOnSurface = Color(0xFFFBEEEB), + outline = Color(0xFF85736E), + surfaceContainerLowest = Color(0xFFECE3E0), + surfaceContainerLow = Color(0xFFF1E7E4), + surfaceContainer = Color(0xFFF6EBE7), // Navigation bar background + surfaceContainerHigh = Color(0xFFFAF4F2), + surfaceContainerHighest = Color(0xFFFBF6F4), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt index 7ed6b3cc07..663b8ecc19 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt @@ -74,12 +74,13 @@ fun TrackInfoDialogHome( onRemoved: (TrackItem) -> Unit, ) { Column( - modifier = Modifier - .animateContentSize() - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(16.dp) - .windowInsetsPadding(WindowInsets.systemBars), + modifier = + Modifier + .animateContentSize() + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(16.dp) + .windowInsetsPadding(WindowInsets.systemBars), verticalArrangement = Arrangement.spacedBy(24.dp), ) { trackItems.forEach { item -> @@ -91,28 +92,37 @@ fun TrackInfoDialogHome( tracker = item.tracker, status = item.tracker.getStatus(item.track.status), onStatusClick = { onStatusClick(item) }, - chapters = "${item.track.lastChapterRead.toInt()}".let { - val totalChapters = item.track.totalChapters - if (totalChapters > 0) { - // Add known total chapter count - "$it / $totalChapters" - } else { - it - } - }, + chapters = + "${item.track.lastChapterRead.toInt()}".let { + val totalChapters = item.track.totalChapters + if (totalChapters > 0) { + // Add known total chapter count + "$it / $totalChapters" + } else { + it + } + }, onChaptersClick = { onChapterClick(item) }, - score = item.tracker.displayScore(item.track) - .takeIf { supportsScoring && item.track.score != 0.0 }, - onScoreClick = { onScoreClick(item) } - .takeIf { supportsScoring }, - startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate.toLocalDate()) } - .takeIf { supportsReadingDates && item.track.startDate != 0L }, - onStartDateClick = { onStartDateEdit(item) } // TODO - .takeIf { supportsReadingDates }, - endDate = dateFormat.format(item.track.finishDate.toLocalDate()) - .takeIf { supportsReadingDates && item.track.finishDate != 0L }, - onEndDateClick = { onEndDateEdit(item) } - .takeIf { supportsReadingDates }, + score = + item.tracker + .displayScore(item.track) + .takeIf { supportsScoring && item.track.score != 0.0 }, + onScoreClick = + { onScoreClick(item) } + .takeIf { supportsScoring }, + startDate = + remember(item.track.startDate) { dateFormat.format(item.track.startDate.toLocalDate()) } + .takeIf { supportsReadingDates && item.track.startDate != 0L }, + onStartDateClick = + { onStartDateEdit(item) } // TODO + .takeIf { supportsReadingDates }, + endDate = + dateFormat + .format(item.track.finishDate.toLocalDate()) + .takeIf { supportsReadingDates && item.track.finishDate != 0L }, + onEndDateClick = + { onEndDateEdit(item) } + .takeIf { supportsReadingDates }, onNewSearch = { onNewSearch(item) }, onOpenInBrowser = { onOpenInBrowser(item) }, onRemoved = { onRemoved(item) }, @@ -155,16 +165,16 @@ private fun TrackInfoItem( onClick = onOpenInBrowser, ) Box( - modifier = Modifier - .height(48.dp) - .weight(1f) - .combinedClickable( - onClick = onNewSearch, - onLongClick = { - context.copyToClipboard(title, title) - }, - ) - .padding(start = 16.dp), + modifier = + Modifier + .height(48.dp) + .weight(1f) + .combinedClickable( + onClick = onNewSearch, + onLongClick = { + context.copyToClipboard(title, title) + }, + ).padding(start = 16.dp), contentAlignment = Alignment.CenterStart, ) { Text( @@ -183,12 +193,13 @@ private fun TrackInfoItem( } Box( - modifier = Modifier - .padding(top = 12.dp) - .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.surfaceContainerHighest) - .padding(8.dp) - .clip(RoundedCornerShape(6.dp)), + modifier = + Modifier + .padding(top = 12.dp) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(8.dp) + .clip(RoundedCornerShape(6.dp)), ) { Column { Row(modifier = Modifier.height(IntrinsicSize.Min)) { @@ -206,9 +217,10 @@ private fun TrackInfoItem( if (onScoreClick != null) { VerticalDivider() TrackDetailsItem( - modifier = Modifier - .weight(1f) - .alpha(if (score == null) UnsetStatusTextAlpha else 1f), + modifier = + Modifier + .weight(1f) + .alpha(if (score == null) UnsetStatusTextAlpha else 1f), text = score ?: stringResource(MR.strings.score), onClick = onScoreClick, ) @@ -246,10 +258,11 @@ private fun TrackDetailsItem( placeholder: String = "", ) { Box( - modifier = modifier - .clickable(onClick = onClick) - .fillMaxHeight() - .padding(12.dp), + modifier = + modifier + .clickable(onClick = onClick) + .fillMaxHeight() + .padding(12.dp), contentAlignment = Alignment.Center, ) { Text( @@ -274,9 +287,10 @@ private fun TrackInfoItemEmpty( TrackLogoIcon(tracker) TextButton( onClick = onNewSearch, - modifier = Modifier - .padding(start = 16.dp) - .weight(1f), + modifier = + Modifier + .padding(start = 16.dp) + .weight(1f), ) { Text(text = stringResource(MR.strings.add_tracking)) } diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt index aa58403eac..95e3bd7505 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt @@ -8,45 +8,49 @@ import tachiyomi.domain.track.model.Track import java.time.format.DateTimeFormatter import java.time.format.FormatStyle -internal class TrackInfoDialogHomePreviewProvider : - PreviewParameterProvider<@Composable () -> Unit> { - - private val aTrack = Track( - id = 1L, - mangaId = 2L, - trackerId = 3L, - remoteId = 4L, - libraryId = null, - title = "Manage Name On Tracker Site", - lastChapterRead = 2.0, - totalChapters = 12L, - status = 1L, - score = 2.0, - remoteUrl = "https://example.com", - startDate = 0L, - finishDate = 0L, - ) - private val trackItemWithoutTrack = TrackItem( - track = null, - tracker = DummyTracker( +internal class TrackInfoDialogHomePreviewProvider : PreviewParameterProvider<@Composable () -> Unit> { + private val aTrack = + Track( id = 1L, - name = "Example Tracker", - ), - ) - private val trackItemWithTrack = TrackItem( - track = aTrack, - tracker = DummyTracker( - id = 2L, - name = "Example Tracker 2", - ), - ) + mangaId = 2L, + trackerId = 3L, + remoteId = 4L, + libraryId = null, + title = "Manage Name On Tracker Site", + lastChapterRead = 2.0, + totalChapters = 12L, + status = 1L, + score = 2.0, + remoteUrl = "https://example.com", + startDate = 0L, + finishDate = 0L, + ) + private val trackItemWithoutTrack = + TrackItem( + track = null, + tracker = + DummyTracker( + id = 1L, + name = "Example Tracker", + ), + ) + private val trackItemWithTrack = + TrackItem( + track = aTrack, + tracker = + DummyTracker( + id = 2L, + name = "Example Tracker 2", + ), + ) private val trackersWithAndWithoutTrack = @Composable { TrackInfoDialogHome( - trackItems = listOf( - trackItemWithoutTrack, - trackItemWithTrack, - ), + trackItems = + listOf( + trackItemWithoutTrack, + trackItemWithTrack, + ), dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM), onStatusClick = {}, onChapterClick = {}, @@ -75,8 +79,9 @@ internal class TrackInfoDialogHomePreviewProvider : } override val values: Sequence<@Composable () -> Unit> - get() = sequenceOf( - trackersWithAndWithoutTrack, - noTrackers, - ) + get() = + sequenceOf( + trackersWithAndWithoutTrack, + noTrackers, + ) } diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt index c6fda5cc50..ca7b9bf6a1 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt @@ -62,14 +62,14 @@ fun TrackStatusSelector( item { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .selectable( - selected = isSelected, - onClick = { onSelectionChange(key) }, - ) - .fillMaxWidth() - .minimumInteractiveComponentSize(), + modifier = + Modifier + .clip(RoundedCornerShape(8.dp)) + .selectable( + selected = isSelected, + onClick = { onSelectionChange(key) }, + ).fillMaxWidth() + .minimumInteractiveComponentSize(), ) { RadioButton( selected = isSelected, @@ -147,10 +147,11 @@ fun TrackDateSelector( onRemove: (() -> Unit)?, onDismissRequest: () -> Unit, ) { - val pickerState = rememberDatePickerState( - initialSelectedDateMillis = initialSelectedDateMillis, - selectableDates = selectableDates, - ) + val pickerState = + rememberDatePickerState( + initialSelectedDateMillis = initialSelectedDateMillis, + selectableDates = selectableDates, + ) AlertDialogContent( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars), title = { Text(text = title) }, @@ -164,9 +165,10 @@ fun TrackDateSelector( ) Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 12.dp, top = 8.dp, end = 12.dp, bottom = 24.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(start = 12.dp, top = 8.dp, end = 12.dp, bottom = 24.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small, Alignment.End), ) { if (onRemove != null) { @@ -232,15 +234,16 @@ private fun TrackStatusSelectorPreviews() { TrackStatusSelector( selection = 1, onSelectionChange = {}, - selections = persistentMapOf( - // Anilist values - 1L to MR.strings.reading, - 2L to MR.strings.plan_to_read, - 3L to MR.strings.completed, - 4L to MR.strings.on_hold, - 5L to MR.strings.dropped, - 6L to MR.strings.repeating, - ), + selections = + persistentMapOf( + // Anilist values + 1L to MR.strings.reading, + 2L to MR.strings.plan_to_read, + 3L to MR.strings.completed, + 4L to MR.strings.on_hold, + 5L to MR.strings.dropped, + 6L to MR.strings.repeating, + ), onConfirm = {}, onDismissRequest = {}, ) diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt index ef158218fb..7f61990a1f 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt @@ -117,12 +117,14 @@ fun TrackerSearch( BasicTextField( value = query, onValueChange = onQueryChange, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .runOnEnterKeyPressed(action = dispatchQueryAndClearFocus), - textStyle = MaterialTheme.typography.bodyLarge - .copy(color = MaterialTheme.colorScheme.onSurface), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + .runOnEnterKeyPressed(action = dispatchQueryAndClearFocus), + textStyle = + MaterialTheme.typography.bodyLarge + .copy(color = MaterialTheme.colorScheme.onSurface), singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardActions = KeyboardActions(onSearch = { dispatchQueryAndClearFocus() }), @@ -167,10 +169,11 @@ fun TrackerSearch( ) { Button( onClick = { onConfirmSelection() }, - modifier = Modifier - .padding(12.dp) - .windowInsetsPadding(WindowInsets.navigationBars) - .fillMaxWidth(), + modifier = + Modifier + .padding(12.dp) + .windowInsetsPadding(WindowInsets.navigationBars) + .fillMaxWidth(), elevation = ButtonDefaults.elevatedButtonElevation(), ) { Text(text = stringResource(MR.strings.action_track)) @@ -208,8 +211,9 @@ fun TrackerSearch( } else { EmptyScreen( modifier = Modifier.padding(innerPadding), - message = queryResult.exceptionOrNull()?.message - ?: stringResource(MR.strings.unknown_error), + message = + queryResult.exceptionOrNull()?.message + ?: stringResource(MR.strings.unknown_error), ) } } @@ -231,21 +235,20 @@ private fun SearchResultItem( val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent var dropDownMenuExpanded by remember { mutableStateOf(false) } Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp) - .clip(shape) - .background(MaterialTheme.colorScheme.surface) - .border( - width = 2.dp, - color = borderColor, - shape = shape, - ) - .combinedClickable( - onLongClick = { dropDownMenuExpanded = true }, - onClick = onClick, - ) - .padding(12.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .clip(shape) + .background(MaterialTheme.colorScheme.surface) + .border( + width = 2.dp, + color = borderColor, + shape = shape, + ).combinedClickable( + onLongClick = { dropDownMenuExpanded = true }, + onClick = onClick, + ).padding(12.dp), ) { if (selected) { Icon( @@ -312,9 +315,10 @@ private fun SearchResultItem( if (description.isNotBlank()) { Text( text = description, - modifier = Modifier - .paddingFromBaseline(top = 24.dp) - .secondaryItemAlpha(), + modifier = + Modifier + .paddingFromBaseline(top = 24.dp) + .secondaryItemAlpha(), maxLines = 4, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall, @@ -364,9 +368,10 @@ private fun SearchResultItemDetails( ) Text( text = text, - modifier = Modifier - .weight(1f) - .secondaryItemAlpha(), + modifier = + Modifier + .weight(1f) + .secondaryItemAlpha(), maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt index 808f41f7de..b187ee159f 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt @@ -47,38 +47,40 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab onDismissRequest = {}, ) } - override val values: Sequence<@Composable () -> Unit> = sequenceOf( - fullPageWithSecondSelected, - fullPageWithoutSelected, - loading, - ) + override val values: Sequence<@Composable () -> Unit> = + sequenceOf( + fullPageWithSecondSelected, + fullPageWithoutSelected, + loading, + ) - private fun someTrackSearches(): Sequence = sequence { - while (true) { - yield(randTrackSearch()) + private fun someTrackSearches(): Sequence = + sequence { + while (true) { + yield(randTrackSearch()) + } } - } - private fun randTrackSearch() = TrackSearch().let { - it.id = Random.nextLong() - it.manga_id = Random.nextLong() - it.tracker_id = Random.nextLong() - it.remote_id = Random.nextLong() - it.library_id = Random.nextLong() - it.title = lorem((1..10).random()).joinToString() - it.last_chapter_read = (0..100).random().toDouble() - it.total_chapters = (100L..1000L).random() - it.score = (0..10).random().toDouble() - it.status = Random.nextLong() - it.started_reading_date = 0L - it.finished_reading_date = 0L - it.tracking_url = "https://example.com/tracker-example" - it.cover_url = "https://example.com/cover.png" - it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString() - it.summary = lorem((0..40).random()).joinToString() - it - } + private fun randTrackSearch() = + TrackSearch().let { + it.id = Random.nextLong() + it.manga_id = Random.nextLong() + it.tracker_id = Random.nextLong() + it.remote_id = Random.nextLong() + it.library_id = Random.nextLong() + it.title = lorem((1..10).random()).joinToString() + it.last_chapter_read = (0..100).random().toDouble() + it.total_chapters = (100L..1000L).random() + it.score = (0..10).random().toDouble() + it.status = Random.nextLong() + it.started_reading_date = 0L + it.finished_reading_date = 0L + it.tracking_url = "https://example.com/tracker-example" + it.cover_url = "https://example.com/cover.png" + it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString() + it.summary = lorem((0..40).random()).joinToString() + it + } - private fun lorem(words: Int): Sequence = - LoremIpsum(words).values + private fun lorem(words: Int): Sequence = LoremIpsum(words).values } diff --git a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt index 4dd03ecc71..9a5de71129 100644 --- a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt +++ b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt @@ -23,17 +23,19 @@ fun TrackLogoIcon( tracker: Tracker, onClick: (() -> Unit)? = null, ) { - val modifier = if (onClick != null) { - Modifier.clickableNoIndication(onClick = onClick) - } else { - Modifier - } + val modifier = + if (onClick != null) { + Modifier.clickableNoIndication(onClick = onClick) + } else { + Modifier + } Box( - modifier = modifier - .size(48.dp) - .background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium) - .padding(4.dp), + modifier = + modifier + .size(48.dp) + .background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium) + .padding(4.dp), contentAlignment = Alignment.Center, ) { Image( diff --git a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIconPreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIconPreviewProvider.kt index 3f5e4b840a..d8bafe8de4 100644 --- a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIconPreviewProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIconPreviewProvider.kt @@ -7,14 +7,14 @@ import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.test.DummyTracker internal class TrackLogoIconPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - DummyTracker( - id = 1L, - name = "Dummy Tracker", - valLogoColor = Color.rgb(18, 25, 35), - valLogo = R.drawable.ic_tracker_anilist, - ), - ) + get() = + sequenceOf( + DummyTracker( + id = 1L, + name = "Dummy Tracker", + valLogoColor = Color.rgb(18, 25, 35), + valLogo = R.drawable.ic_tracker_anilist, + ), + ) } 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 5693185c0c..34090acc04 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -84,10 +84,11 @@ fun UpdateScreen( ) { contentPadding -> when { state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) - state.items.isEmpty() -> EmptyScreen( - stringRes = MR.strings.information_no_recent, - modifier = Modifier.padding(contentPadding), - ) + state.items.isEmpty() -> + EmptyScreen( + stringRes = MR.strings.information_no_recent, + modifier = Modifier.padding(contentPadding), + ) else -> { val scope = rememberCoroutineScope() var isRefreshing by remember { mutableStateOf(false) } @@ -191,30 +192,41 @@ private fun UpdatesBottomBar( MangaBottomActionMenu( visible = selected.isNotEmpty(), modifier = Modifier.fillMaxWidth(), - onBookmarkClicked = { - onMultiBookmarkClicked.invoke(selected, true) - }.takeIf { selected.fastAny { !it.update.bookmark } }, - onRemoveBookmarkClicked = { - onMultiBookmarkClicked.invoke(selected, false) - }.takeIf { selected.fastAll { it.update.bookmark } }, - onMarkAsReadClicked = { - onMultiMarkAsReadClicked(selected, true) - }.takeIf { selected.fastAny { !it.update.read } }, - onMarkAsUnreadClicked = { - onMultiMarkAsReadClicked(selected, false) - }.takeIf { selected.fastAny { it.update.read || it.update.lastPageRead > 0L } }, - onDownloadClicked = { - onDownloadChapter(selected, ChapterDownloadAction.START) - }.takeIf { - selected.fastAny { it.downloadStateProvider() != Download.State.DOWNLOADED } - }, - onDeleteClicked = { - onMultiDeleteClicked(selected) - }.takeIf { selected.fastAny { it.downloadStateProvider() == Download.State.DOWNLOADED } }, + onBookmarkClicked = + { + onMultiBookmarkClicked.invoke(selected, true) + }.takeIf { selected.fastAny { !it.update.bookmark } }, + onRemoveBookmarkClicked = + { + onMultiBookmarkClicked.invoke(selected, false) + }.takeIf { selected.fastAll { it.update.bookmark } }, + onMarkAsReadClicked = + { + onMultiMarkAsReadClicked(selected, true) + }.takeIf { selected.fastAny { !it.update.read } }, + onMarkAsUnreadClicked = + { + onMultiMarkAsReadClicked(selected, false) + }.takeIf { selected.fastAny { it.update.read || it.update.lastPageRead > 0L } }, + onDownloadClicked = + { + onDownloadChapter(selected, ChapterDownloadAction.START) + }.takeIf { + selected.fastAny { it.downloadStateProvider() != Download.State.DOWNLOADED } + }, + onDeleteClicked = + { + onMultiDeleteClicked(selected) + }.takeIf { selected.fastAny { it.downloadStateProvider() == Download.State.DOWNLOADED } }, ) } sealed interface UpdatesUiModel { - data class Header(val date: LocalDate) : UpdatesUiModel - data class Item(val item: UpdatesItem) : UpdatesUiModel + data class Header( + val date: LocalDate, + ) : UpdatesUiModel + + data class Item( + val item: UpdatesItem, + ) : UpdatesUiModel } 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 9f5c924796..11e1385c1a 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -48,14 +48,13 @@ import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.selectedBackground -internal fun LazyListScope.updatesLastUpdatedItem( - lastUpdated: Long, -) { +internal fun LazyListScope.updatesLastUpdatedItem(lastUpdated: Long) { item(key = "updates-lastUpdated") { Box( - modifier = Modifier - .animateItem() - .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), + modifier = + Modifier + .animateItem() + .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), ) { Text( text = stringResource(MR.strings.updates_last_update_info, relativeTimeSpanString(lastUpdated)), @@ -101,14 +100,15 @@ internal fun LazyListScope.updatesUiItems( modifier = Modifier.animateItem(), update = updatesItem.update, selected = updatesItem.selected, - readProgress = updatesItem.update.lastPageRead - .takeIf { !updatesItem.update.read && it > 0L } - ?.let { - stringResource( - MR.strings.chapter_progress, - it + 1, - ) - }, + readProgress = + updatesItem.update.lastPageRead + .takeIf { !updatesItem.update.read && it > 0L } + ?.let { + stringResource( + MR.strings.chapter_progress, + it + 1, + ) + }, onLongClick = { onUpdateSelected(updatesItem, !updatesItem.selected, true, true) }, @@ -119,9 +119,10 @@ internal fun LazyListScope.updatesUiItems( } }, onClickCover = { onClickCover(updatesItem) }.takeIf { !selectionMode }, - onDownloadChapter = { action: ChapterDownloadAction -> - onDownloadChapter(listOf(updatesItem), action) - }.takeIf { !selectionMode }, + onDownloadChapter = + { action: ChapterDownloadAction -> + onDownloadChapter(listOf(updatesItem), action) + }.takeIf { !selectionMode }, downloadStateProvider = updatesItem.downloadStateProvider, downloadProgressProvider = updatesItem.downloadProgressProvider, ) @@ -148,31 +149,33 @@ private fun UpdatesUiItem( val textAlpha = if (update.read) ReadItemAlpha else 1f Row( - modifier = modifier - .selectedBackground(selected) - .combinedClickable( - onClick = onClick, - onLongClick = { - onLongClick() - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - }, - ) - .height(56.dp) - .padding(horizontal = MaterialTheme.padding.medium), + modifier = + modifier + .selectedBackground(selected) + .combinedClickable( + onClick = onClick, + onLongClick = { + onLongClick() + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + ).height(56.dp) + .padding(horizontal = MaterialTheme.padding.medium), verticalAlignment = Alignment.CenterVertically, ) { MangaCover.Square( - modifier = Modifier - .padding(vertical = 6.dp) - .fillMaxHeight(), + modifier = + Modifier + .padding(vertical = 6.dp) + .fillMaxHeight(), data = update.coverData, onClick = onClickCover, ) Column( - modifier = Modifier - .padding(horizontal = MaterialTheme.padding.medium) - .weight(1f), + modifier = + Modifier + .padding(horizontal = MaterialTheme.padding.medium) + .weight(1f), ) { Text( text = update.mangaTitle, @@ -188,9 +191,10 @@ private fun UpdatesUiItem( Icon( imageVector = Icons.Filled.Circle, contentDescription = stringResource(MR.strings.unread), - modifier = Modifier - .height(8.dp) - .padding(end = 4.dp), + modifier = + Modifier + .height(8.dp) + .padding(end = 4.dp), tint = MaterialTheme.colorScheme.primary, ) } @@ -198,8 +202,9 @@ private fun UpdatesUiItem( Icon( imageVector = Icons.Filled.Bookmark, contentDescription = stringResource(MR.strings.action_filter_bookmarked), - modifier = Modifier - .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), + modifier = + Modifier + .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.width(2.dp)) @@ -211,8 +216,9 @@ private fun UpdatesUiItem( color = LocalContentColor.current.copy(alpha = textAlpha), overflow = TextOverflow.Ellipsis, onTextLayout = { textHeight = it.size.height }, - modifier = Modifier - .weight(weight = 1f, fill = false), + modifier = + Modifier + .weight(weight = 1f, fill = false), ) if (readProgress != null) { DotSeparatorText() diff --git a/app/src/main/java/eu/kanade/presentation/util/ChapterNumberFormatter.kt b/app/src/main/java/eu/kanade/presentation/util/ChapterNumberFormatter.kt index a4f1e4ed16..9f26b27654 100644 --- a/app/src/main/java/eu/kanade/presentation/util/ChapterNumberFormatter.kt +++ b/app/src/main/java/eu/kanade/presentation/util/ChapterNumberFormatter.kt @@ -3,11 +3,10 @@ package eu.kanade.presentation.util import java.text.DecimalFormat import java.text.DecimalFormatSymbols -private val formatter = DecimalFormat( - "#.###", - DecimalFormatSymbols().apply { decimalSeparator = '.' }, -) +private val formatter = + DecimalFormat( + "#.###", + DecimalFormatSymbols().apply { decimalSeparator = '.' }, + ) -fun formatChapterNumber(chapterNumber: Double): String { - return formatter.format(chapterNumber) -} +fun formatChapterNumber(chapterNumber: Double): String = formatter.format(chapterNumber) diff --git a/app/src/main/java/eu/kanade/presentation/util/Navigator.kt b/app/src/main/java/eu/kanade/presentation/util/Navigator.kt index 86311d8e05..6de7e82c5d 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Navigator.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Navigator.kt @@ -36,7 +36,6 @@ interface Tab : cafe.adriel.voyager.navigator.tab.Tab { } abstract class Screen : Screen { - override val key: ScreenKey = uniqueScreenKey } @@ -45,12 +44,13 @@ abstract class Screen : Screen { * main dispatcher. */ val ScreenModel.ioCoroutineScope: CoroutineScope - get() = ScreenModelStore.getOrPutDependency( - screenModel = this, - name = "ScreenModelIoCoroutineScope", - factory = { key -> CoroutineScope(Dispatchers.IO + SupervisorJob()) + CoroutineName(key) }, - onDispose = { scope -> scope.cancel() }, - ) + get() = + ScreenModelStore.getOrPutDependency( + screenModel = this, + name = "ScreenModelIoCoroutineScope", + factory = { key -> CoroutineScope(Dispatchers.IO + SupervisorJob()) + CoroutineName(key) }, + onDispose = { scope -> scope.cancel() }, + ) interface AssistContentScreen { fun onProvideAssistUrl(): String? diff --git a/app/src/main/java/eu/kanade/presentation/util/Permissions.kt b/app/src/main/java/eu/kanade/presentation/util/Permissions.kt index 80c2f90b92..031886e9ff 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Permissions.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Permissions.kt @@ -19,11 +19,12 @@ fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false) var installGranted by remember { mutableStateOf(initialValue) } DisposableEffect(lifecycleOwner.lifecycle) { - val observer = object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - installGranted = context.packageManager.canRequestPackageInstalls() + val observer = + object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + installGranted = context.packageManager.canRequestPackageInstalls() + } } - } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) diff --git a/app/src/main/java/eu/kanade/presentation/util/Resources.kt b/app/src/main/java/eu/kanade/presentation/util/Resources.kt index 5b7bc9948d..71f3b801fb 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Resources.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Resources.kt @@ -19,11 +19,14 @@ import androidx.core.graphics.drawable.toBitmap * @return the bitmap associated with the resource */ @Composable -fun rememberResourceBitmapPainter(@DrawableRes id: Int): BitmapPainter { +fun rememberResourceBitmapPainter( + @DrawableRes id: Int, +): BitmapPainter { val context = LocalContext.current return remember(id) { - val drawable = ContextCompat.getDrawable(context, id) - ?: throw Resources.NotFoundException() + val drawable = + ContextCompat.getDrawable(context, id) + ?: throw Resources.NotFoundException() BitmapPainter(drawable.toBitmap().asImageBitmap()) } } diff --git a/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt b/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt index 643f19f6e3..728ead706b 100644 --- a/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt +++ b/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt @@ -11,8 +11,11 @@ import java.time.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes -fun Duration.toDurationString(context: Context, fallback: String): String { - return toComponents { days, hours, minutes, seconds, _ -> +fun Duration.toDurationString( + context: Context, + fallback: String, +): String = + toComponents { days, hours, minutes, seconds, _ -> buildList(4) { if (days != 0L) add(context.stringResource(MR.strings.day_short, days)) if (hours != 0) add(context.stringResource(MR.strings.hour_short, hours)) @@ -24,7 +27,6 @@ fun Duration.toDurationString(context: Context, fallback: String): String { if (seconds != 0 && days == 0L && hours == 0) add(context.stringResource(MR.strings.seconds_short, seconds)) }.joinToString(" ").ifBlank { fallback } } -} @Composable @ReadOnlyComposable @@ -32,9 +34,10 @@ fun relativeTimeSpanString(epochMillis: Long): String { val now = Instant.now().toEpochMilli() return when { epochMillis <= 0L -> stringResource(MR.strings.relative_time_span_never) - now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource( - MR.strings.updates_last_update_info_just_now, - ) + now - epochMillis < 1.minutes.inWholeMilliseconds -> + stringResource( + MR.strings.updates_last_update_info_just_now, + ) else -> DateUtils.getRelativeTimeSpanString(epochMillis, now, DateUtils.MINUTE_IN_MILLIS).toString() } } diff --git a/app/src/main/java/eu/kanade/presentation/util/WindowSize.kt b/app/src/main/java/eu/kanade/presentation/util/WindowSize.kt index 675eb2a647..d85f69e1a2 100644 --- a/app/src/main/java/eu/kanade/presentation/util/WindowSize.kt +++ b/app/src/main/java/eu/kanade/presentation/util/WindowSize.kt @@ -7,6 +7,4 @@ import eu.kanade.tachiyomi.util.system.isTabletUi @Composable @ReadOnlyComposable -fun isTabletUi(): Boolean { - return LocalConfiguration.current.isTabletUi() -} +fun isTabletUi(): Boolean = LocalConfiguration.current.isTabletUi() diff --git a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt index 508790fff1..1504de6275 100644 --- a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt @@ -64,53 +64,61 @@ fun WebViewScreenContent( var currentUrl by remember { mutableStateOf(url) } var showCloudflareHelp by remember { mutableStateOf(false) } - val webClient = remember { - object : AccompanistWebViewClient() { - override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - url?.let { - currentUrl = it - onUrlChange(it) + val webClient = + remember { + object : AccompanistWebViewClient() { + override fun onPageStarted( + view: WebView, + url: String?, + favicon: Bitmap?, + ) { + super.onPageStarted(view, url, favicon) + url?.let { + currentUrl = it + onUrlChange(it) + } } - } - override fun onPageFinished(view: WebView, url: String?) { - super.onPageFinished(view, url) - scope.launch { - val html = view.getHtml() - showCloudflareHelp = "window._cf_chl_opt" in html || "Ray ID is" in html + override fun onPageFinished( + view: WebView, + url: String?, + ) { + super.onPageFinished(view, url) + scope.launch { + val html = view.getHtml() + showCloudflareHelp = "window._cf_chl_opt" in html || "Ray ID is" in html + } } - } - override fun doUpdateVisitedHistory( - view: WebView, - url: String?, - isReload: Boolean, - ) { - super.doUpdateVisitedHistory(view, url, isReload) - url?.let { - currentUrl = it - onUrlChange(it) + override fun doUpdateVisitedHistory( + view: WebView, + url: String?, + isReload: Boolean, + ) { + super.doUpdateVisitedHistory(view, url, isReload) + url?.let { + currentUrl = it + onUrlChange(it) + } } - } - override fun shouldOverrideUrlLoading( - view: WebView?, - request: WebResourceRequest?, - ): Boolean { - request?.let { - // Don't attempt to open blobs as webpages - if (it.url.toString().startsWith("blob:http")) { - return false - } + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest?, + ): Boolean { + request?.let { + // Don't attempt to open blobs as webpages + if (it.url.toString().startsWith("blob:http")) { + return false + } - // Continue with request, but with custom headers - view?.loadUrl(it.url.toString(), headers) + // Continue with request, but with custom headers + view?.loadUrl(it.url.toString(), headers) + } + return super.shouldOverrideUrlLoading(view, request) } - return super.shouldOverrideUrlLoading(view, request) } } - } Scaffold( topBar = { @@ -171,29 +179,34 @@ fun WebViewScreenContent( ) { WarningBanner( textRes = MR.strings.information_cloudflare_help, - modifier = Modifier - .clip(MaterialTheme.shapes.small) - .clickable { - uriHandler.openUri( - "https://mihon.app/docs/guides/troubleshooting/#cloudflare", - ) - }, + modifier = + Modifier + .clip(MaterialTheme.shapes.small) + .clickable { + uriHandler.openUri( + "https://mihon.app/docs/guides/troubleshooting/#cloudflare", + ) + }, ) } } } when (val loadingState = state.loadingState) { - is LoadingState.Initializing -> LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - ) - is LoadingState.Loading -> LinearProgressIndicator( - progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f }, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - ) + is LoadingState.Initializing -> + LinearProgressIndicator( + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) + is LoadingState.Loading -> + LinearProgressIndicator( + progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f }, + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) else -> {} } } @@ -201,9 +214,10 @@ fun WebViewScreenContent( ) { contentPadding -> WebView( state = state, - modifier = Modifier - .fillMaxSize() - .padding(contentPadding), + modifier = + Modifier + .fillMaxSize() + .padding(contentPadding), navigator = navigator, onCreated = { webView -> webView.setDefaultSettings() diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 558ad213eb..fadc3493b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -63,8 +63,10 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.security.Security -class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory { - +class App : + Application(), + DefaultLifecycleObserver, + SingletonImageLoader.Factory { private val basePreferences: BasePreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy() @@ -96,7 +98,9 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor ProcessLifecycleOwner.get().lifecycle.addObserver(this) // Show notification to disable Incognito Mode when it's enabled - basePreferences.incognitoMode().changes() + basePreferences + .incognitoMode() + .changes() .onEach { enabled -> if (enabled) { disableIncognitoReceiver.register() @@ -109,20 +113,20 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor setSmallIcon(R.drawable.ic_glasses_24dp) setOngoing(true) - val pendingIntent = PendingIntent.getBroadcast( - this@App, - 0, - Intent(ACTION_DISABLE_INCOGNITO_MODE), - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, - ) + val pendingIntent = + PendingIntent.getBroadcast( + this@App, + 0, + Intent(ACTION_DISABLE_INCOGNITO_MODE), + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, + ) setContentIntent(pendingIntent) } } else { disableIncognitoReceiver.unregister() cancelNotification(Notifications.ID_INCOGNITO_MODE) } - } - .launchIn(ProcessLifecycleOwner.get().lifecycleScope) + }.launchIn(ProcessLifecycleOwner.get().lifecycleScope) setAppCompatDelegateThemeMode(Injekt.get().themeMode().get()) @@ -153,27 +157,28 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor ) } - override fun newImageLoader(context: Context): ImageLoader { - return ImageLoader.Builder(this).apply { - val callFactoryLazy = lazy { Injekt.get().client } - components { - add(OkHttpNetworkFetcherFactory(callFactoryLazy::value)) - add(TachiyomiImageDecoder.Factory()) - add(MangaCoverFetcher.MangaFactory(callFactoryLazy)) - add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy)) - add(MangaKeyer()) - add(MangaCoverKeyer()) - add(BufferedSourceFetcher.Factory()) - } - crossfade((300 * this@App.animatorDurationScale).toInt()) - allowRgb565(DeviceUtil.isLowRamDevice(this@App)) - if (networkPreferences.verboseLogging().get()) logger(DebugLogger()) - - // Coil spawns a new thread for every image load by default - fetcherDispatcher(Dispatchers.IO.limitedParallelism(8)) - decoderDispatcher(Dispatchers.IO.limitedParallelism(2)) - }.build() - } + override fun newImageLoader(context: Context): ImageLoader = + ImageLoader + .Builder(this) + .apply { + val callFactoryLazy = lazy { Injekt.get().client } + components { + add(OkHttpNetworkFetcherFactory(callFactoryLazy::value)) + add(TachiyomiImageDecoder.Factory()) + add(MangaCoverFetcher.MangaFactory(callFactoryLazy)) + add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy)) + add(MangaKeyer()) + add(MangaCoverKeyer()) + add(BufferedSourceFetcher.Factory()) + } + crossfade((300 * this@App.animatorDurationScale).toInt()) + allowRgb565(DeviceUtil.isLowRamDevice(this@App)) + if (networkPreferences.verboseLogging().get()) logger(DebugLogger()) + + // Coil spawns a new thread for every image load by default + fetcherDispatcher(Dispatchers.IO.limitedParallelism(8)) + decoderDispatcher(Dispatchers.IO.limitedParallelism(2)) + }.build() override fun onStart(owner: LifecycleOwner) { SecureActivityDelegate.onApplicationStart() @@ -187,12 +192,13 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor try { // Override the value passed as X-Requested-With in WebView requests val stackTrace = Looper.getMainLooper().thread.stackTrace - val chromiumElement = stackTrace.find { - it.className.equals( - "org.chromium.base.BuildInfo", - ignoreCase = true, - ) - } + val chromiumElement = + stackTrace.find { + it.className.equals( + "org.chromium.base.BuildInfo", + ignoreCase = true, + ) + } if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) { return WebViewUtil.SPOOF_PACKAGE_NAME } @@ -212,7 +218,10 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor private inner class DisableIncognitoReceiver : BroadcastReceiver() { private var registered = false - override fun onReceive(context: Context, intent: Intent) { + override fun onReceive( + context: Context, + intent: Intent, + ) { basePreferences.incognitoMode().set(false) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/crash/CrashActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/crash/CrashActivity.kt index b7aa496edf..23e5207676 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/crash/CrashActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/crash/CrashActivity.kt @@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.view.setComposeContent class CrashActivity : BaseActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt index 600dac444d..2fd7124655 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt @@ -18,19 +18,22 @@ class GlobalExceptionHandler private constructor( private val defaultHandler: Thread.UncaughtExceptionHandler, private val activityToBeLaunched: Class<*>, ) : Thread.UncaughtExceptionHandler { - object ThrowableSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Throwable", PrimitiveKind.STRING) - override fun deserialize(decoder: Decoder): Throwable = - Throwable(message = decoder.decodeString()) + override fun deserialize(decoder: Decoder): Throwable = Throwable(message = decoder.decodeString()) - override fun serialize(encoder: Encoder, value: Throwable) = - encoder.encodeString(value.stackTraceToString()) + override fun serialize( + encoder: Encoder, + value: Throwable, + ) = encoder.encodeString(value.stackTraceToString()) } - override fun uncaughtException(thread: Thread, exception: Throwable) { + override fun uncaughtException( + thread: Thread, + exception: Throwable, + ) { try { logcat(priority = LogPriority.ERROR, throwable = exception) launchActivity(applicationContext, activityToBeLaunched, exception) @@ -45,11 +48,12 @@ class GlobalExceptionHandler private constructor( activity: Class<*>, exception: Throwable, ) { - val intent = Intent(applicationContext, activity).apply { - putExtra(INTENT_EXTRA, Json.encodeToString(ThrowableSerializer, exception)) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) - } + val intent = + Intent(applicationContext, activity).apply { + putExtra(INTENT_EXTRA, Json.encodeToString(ThrowableSerializer, exception)) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + } applicationContext.startActivity(intent) } @@ -60,21 +64,21 @@ class GlobalExceptionHandler private constructor( applicationContext: Context, activityToBeLaunched: Class<*>, ) { - val handler = GlobalExceptionHandler( - applicationContext, - Thread.getDefaultUncaughtExceptionHandler() as Thread.UncaughtExceptionHandler, - activityToBeLaunched, - ) + val handler = + GlobalExceptionHandler( + applicationContext, + Thread.getDefaultUncaughtExceptionHandler() as Thread.UncaughtExceptionHandler, + activityToBeLaunched, + ) Thread.setDefaultUncaughtExceptionHandler(handler) } - fun getThrowableFromIntent(intent: Intent): Throwable? { - return try { + fun getThrowableFromIntent(intent: Intent): Throwable? = + try { Json.decodeFromString(ThrowableSerializer, intent.getStringExtra(INTENT_EXTRA)!!) } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Wasn't able to retrieve throwable from intent" } null } - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt index 34f862548f..bc1d783514 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt @@ -14,25 +14,25 @@ class BackupDecoder( private val context: Context, private val parser: ProtoBuf = Injekt.get(), ) { - /** * Decode a potentially-gzipped backup. */ - fun decode(uri: Uri): Backup { - return context.contentResolver.openInputStream(uri)!!.use { inputStream -> + fun decode(uri: Uri): Backup = + context.contentResolver.openInputStream(uri)!!.use { inputStream -> val source = inputStream.source().buffer() - val peeked = source.peek().apply { - require(2) - } + val peeked = + source.peek().apply { + require(2) + } val id1id2 = peeked.readShort() - val backupString = if (id1id2.toInt() == 0x1f8b) { // 0x1f8b is gzip magic bytes - source.gzip().buffer() - } else { - source - }.use { it.readByteArray() } + val backupString = + if (id1id2.toInt() == 0x1f8b) { // 0x1f8b is gzip magic bytes + source.gzip().buffer() + } else { + source + }.use { it.readByteArray() } parser.decodeFromByteArray(Backup.serializer(), backupString) } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt index 5a7b87ce9a..5f213bb54c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt @@ -9,46 +9,48 @@ import uy.kohesive.injekt.api.get class BackupFileValidator( private val context: Context, - private val sourceManager: SourceManager = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), ) { - /** * Checks for critical backup file data. * * @return List of missing sources or missing trackers. */ fun validate(uri: Uri): Results { - val backup = try { - BackupDecoder(context).decode(uri) - } catch (e: Exception) { - throw IllegalStateException(e) - } - - val sources = backup.backupSources.associate { it.sourceId to it.name } - val missingSources = sources - .filter { sourceManager.get(it.key) == null } - .values.map { - val id = it.toLongOrNull() - if (id == null) { - it - } else { - sourceManager.getOrStub(id).toString() - } + val backup = + try { + BackupDecoder(context).decode(uri) + } catch (e: Exception) { + throw IllegalStateException(e) } - .distinct() - .sorted() - val trackers = backup.backupManga - .flatMap { it.tracking } - .map { it.syncId } - .distinct() - val missingTrackers = trackers - .mapNotNull { trackerManager.get(it.toLong()) } - .filter { !it.isLoggedIn } - .map { it.name } - .sorted() + val sources = backup.backupSources.associate { it.sourceId to it.name } + val missingSources = + sources + .filter { sourceManager.get(it.key) == null } + .values + .map { + val id = it.toLongOrNull() + if (id == null) { + it + } else { + sourceManager.getOrStub(id).toString() + } + }.distinct() + .sorted() + + val trackers = + backup.backupManga + .flatMap { it.tracking } + .map { it.syncId } + .distinct() + val missingTrackers = + trackers + .mapNotNull { trackerManager.get(it.toLong()) } + .filter { !it.isLoggedIn } + .map { it.name } + .sorted() return Results(missingSources, missingTrackers) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt index fc24f245d0..96818474b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt @@ -20,38 +20,42 @@ import uy.kohesive.injekt.injectLazy import java.io.File import java.util.concurrent.TimeUnit -class BackupNotifier(private val context: Context) { - +class BackupNotifier( + private val context: Context, +) { private val preferences: SecurityPreferences by injectLazy() - private val progressNotificationBuilder = context.notificationBuilder( - Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS, - ) { - setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) - setSmallIcon(R.drawable.ic_mihon) - setAutoCancel(false) - setOngoing(true) - setOnlyAlertOnce(true) - } + private val progressNotificationBuilder = + context.notificationBuilder( + Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS, + ) { + setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) + setSmallIcon(R.drawable.ic_mihon) + setAutoCancel(false) + setOngoing(true) + setOnlyAlertOnce(true) + } - private val completeNotificationBuilder = context.notificationBuilder( - Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE, - ) { - setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) - setSmallIcon(R.drawable.ic_mihon) - setAutoCancel(false) - } + private val completeNotificationBuilder = + context.notificationBuilder( + Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE, + ) { + setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) + setSmallIcon(R.drawable.ic_mihon) + setAutoCancel(false) + } private fun NotificationCompat.Builder.show(id: Int) { context.notify(id, build()) } fun showBackupProgress(): NotificationCompat.Builder { - val builder = with(progressNotificationBuilder) { - setContentTitle(context.stringResource(MR.strings.creating_backup)) + val builder = + with(progressNotificationBuilder) { + setContentTitle(context.stringResource(MR.strings.creating_backup)) - setProgress(0, 0, true) - } + setProgress(0, 0, true) + } builder.show(Notifications.ID_BACKUP_PROGRESS) @@ -93,29 +97,31 @@ class BackupNotifier(private val context: Context) { maxAmount: Int = 100, sync: Boolean = false, ): NotificationCompat.Builder { - val builder = with(progressNotificationBuilder) { - val contentTitle = if (sync) { - context.stringResource(MR.strings.syncing_library) - } else { - context.stringResource(MR.strings.restoring_backup) - } - setContentTitle(contentTitle) - - if (!preferences.hideNotificationContent().get()) { - setContentText(content) + val builder = + with(progressNotificationBuilder) { + val contentTitle = + if (sync) { + context.stringResource(MR.strings.syncing_library) + } else { + context.stringResource(MR.strings.restoring_backup) + } + setContentTitle(contentTitle) + + if (!preferences.hideNotificationContent().get()) { + setContentText(content) + } + + setProgress(maxAmount, progress, false) + setOnlyAlertOnce(true) + + clearActions() + addAction( + R.drawable.ic_close_24dp, + context.stringResource(MR.strings.action_cancel), + NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS), + ) } - setProgress(maxAmount, progress, false) - setOnlyAlertOnce(true) - - clearActions() - addAction( - R.drawable.ic_close_24dp, - context.stringResource(MR.strings.action_cancel), - NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS), - ) - } - builder.show(Notifications.ID_RESTORE_PROGRESS) return builder @@ -139,21 +145,24 @@ class BackupNotifier(private val context: Context) { file: String?, sync: Boolean, ) { - val contentTitle = if (sync) { - context.stringResource(MR.strings.library_sync_complete) - } else { - context.stringResource(MR.strings.restore_completed) - } + val contentTitle = + if (sync) { + context.stringResource(MR.strings.library_sync_complete) + } else { + context.stringResource(MR.strings.restore_completed) + } context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) - val timeString = context.stringResource( - MR.strings.restore_duration, - TimeUnit.MILLISECONDS.toMinutes(time), - TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds( + val timeString = + context.stringResource( + MR.strings.restore_duration, TimeUnit.MILLISECONDS.toMinutes(time), - ), - ) + TimeUnit.MILLISECONDS.toSeconds(time) - + TimeUnit.MINUTES.toSeconds( + TimeUnit.MILLISECONDS.toMinutes(time), + ), + ) with(completeNotificationBuilder) { setContentTitle(contentTitle) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt index 80c11c62f4..4cb7e929ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt @@ -31,9 +31,10 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.TimeUnit -class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) : - CoroutineWorker(context, workerParams) { - +class BackupCreateJob( + private val context: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(context, workerParams) { private val notifier = BackupNotifier(context) override suspend fun doWork(): Result { @@ -41,14 +42,16 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry() - val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() - ?: getAutomaticBackupLocation() - ?: return Result.failure() + val uri = + inputData.getString(LOCATION_URI_KEY)?.toUri() + ?: getAutomaticBackupLocation() + ?: return Result.failure() setForegroundSafely() - val options = inputData.getBooleanArray(OPTIONS_KEY)?.let { BackupOptions.fromBooleanArray(it) } - ?: BackupOptions() + val options = + inputData.getBooleanArray(OPTIONS_KEY)?.let { BackupOptions.fromBooleanArray(it) } + ?: BackupOptions() return try { val location = BackupCreator(context, isAutoBackup).backup(uri, options) @@ -65,8 +68,8 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete } } - override suspend fun getForegroundInfo(): ForegroundInfo { - return ForegroundInfo( + override suspend fun getForegroundInfo(): ForegroundInfo = + ForegroundInfo( Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build(), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -75,7 +78,6 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete 0 }, ) - } private fun getAutomaticBackupLocation(): Uri? { val storageManager = Injekt.get() @@ -83,29 +85,31 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete } companion object { - fun isManualJobRunning(context: Context): Boolean { - return context.workManager.isRunning(TAG_MANUAL) - } + fun isManualJobRunning(context: Context): Boolean = context.workManager.isRunning(TAG_MANUAL) - fun setupTask(context: Context, prefInterval: Int? = null) { + fun setupTask( + context: Context, + prefInterval: Int? = null, + ) { val backupPreferences = Injekt.get() val interval = prefInterval ?: backupPreferences.backupInterval().get() if (interval > 0) { - val constraints = Constraints( - requiresBatteryNotLow = true, - ) - - val request = PeriodicWorkRequestBuilder( - interval.toLong(), - TimeUnit.HOURS, - 10, - TimeUnit.MINUTES, - ) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES) - .addTag(TAG_AUTO) - .setConstraints(constraints) - .setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true)) - .build() + val constraints = + Constraints( + requiresBatteryNotLow = true, + ) + + val request = + PeriodicWorkRequestBuilder( + interval.toLong(), + TimeUnit.HOURS, + 10, + TimeUnit.MINUTES, + ).setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES) + .addTag(TAG_AUTO) + .setConstraints(constraints) + .setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true)) + .build() context.workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.UPDATE, request) } else { @@ -113,16 +117,22 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete } } - fun startNow(context: Context, uri: Uri, options: BackupOptions) { - val inputData = workDataOf( - IS_AUTO_BACKUP_KEY to false, - LOCATION_URI_KEY to uri.toString(), - OPTIONS_KEY to options.asBooleanArray(), - ) - val request = OneTimeWorkRequestBuilder() - .addTag(TAG_MANUAL) - .setInputData(inputData) - .build() + fun startNow( + context: Context, + uri: Uri, + options: BackupOptions, + ) { + val inputData = + workDataOf( + IS_AUTO_BACKUP_KEY to false, + LOCATION_URI_KEY to uri.toString(), + OPTIONS_KEY to options.asBooleanArray(), + ) + val request = + OneTimeWorkRequestBuilder() + .addTag(TAG_MANUAL) + .setInputData(inputData) + .build() context.workManager.enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request) } } 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 42c97ca9c5..3a35e35f5c 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 @@ -37,18 +37,18 @@ import java.util.Locale class BackupCreator( private val context: Context, private val isAutoBackup: Boolean, - private val parser: ProtoBuf = Injekt.get(), private val getFavorites: GetFavorites = Injekt.get(), private val backupPreferences: BackupPreferences = Injekt.get(), - private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(), private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(), private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(), private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(), ) { - - suspend fun backup(uri: Uri, options: BackupOptions): String { + suspend fun backup( + uri: Uri, + options: BackupOptions, + ): String { var file: UniFile? = null try { file = ( @@ -57,7 +57,8 @@ class BackupCreator( val dir = UniFile.fromUri(context, uri) // Delete older backups - dir?.listFiles { _, filename -> FILENAME_REGEX.matches(filename) } + dir + ?.listFiles { _, filename -> FILENAME_REGEX.matches(filename) } .orEmpty() .sortedByDescending { it.name } .drop(MAX_AUTO_BACKUPS - 1) @@ -68,32 +69,36 @@ class BackupCreator( } else { UniFile.fromUri(context, uri) } - ) + ) if (file == null || !file.isFile) { throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error)) } val databaseManga = getFavorites.await() - val backup = Backup( - backupManga = backupMangas(databaseManga, options), - backupCategories = backupCategories(options), - backupSources = backupSources(databaseManga), - backupPreferences = backupAppPreferences(options), - backupSourcePreferences = backupSourcePreferences(options), - ) + val backup = + Backup( + backupManga = backupMangas(databaseManga, options), + backupCategories = backupCategories(options), + backupSources = backupSources(databaseManga), + backupPreferences = backupAppPreferences(options), + backupSourcePreferences = backupSourcePreferences(options), + ) val byteArray = parser.encodeToByteArray(Backup.serializer(), backup) if (byteArray.isEmpty()) { throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error)) } - file.openOutputStream() + file + .openOutputStream() .also { // Force overwrite old file (it as? FileOutputStream)?.channel?.truncate(0) - } - .sink().gzip().buffer().use { + }.sink() + .gzip() + .buffer() + .use { it.write(byteArray) } val fileUri = file.uri @@ -119,13 +124,12 @@ class BackupCreator( return categoriesBackupCreator.backupCategories() } - private suspend fun backupMangas(mangas: List, options: BackupOptions): List { - return mangaBackupCreator.backupMangas(mangas, options) - } + private suspend fun backupMangas( + mangas: List, + options: BackupOptions, + ): List = mangaBackupCreator.backupMangas(mangas, options) - private fun backupSources(mangas: List): List { - return sourcesBackupCreator.backupSources(mangas) - } + private fun backupSources(mangas: List): List = sourcesBackupCreator.backupSources(mangas) private fun backupAppPreferences(options: BackupOptions): List { if (!options.appSettings) return emptyList() 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 868458b8a8..115ee6f63c 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,82 +14,85 @@ data class BackupOptions( val sourceSettings: Boolean = true, val privateSettings: Boolean = false, ) { - - fun asBooleanArray() = booleanArrayOf( - libraryEntries, - categories, - chapters, - tracking, - history, - appSettings, - sourceSettings, - privateSettings, - ) + fun asBooleanArray() = + booleanArrayOf( + libraryEntries, + categories, + chapters, + tracking, + history, + appSettings, + sourceSettings, + privateSettings, + ) fun anyEnabled() = libraryEntries || appSettings || sourceSettings companion object { - val libraryOptions = persistentListOf( - Entry( - label = MR.strings.manga, - getter = BackupOptions::libraryEntries, - setter = { options, enabled -> options.copy(libraryEntries = enabled) }, - ), - Entry( - label = MR.strings.categories, - getter = BackupOptions::categories, - setter = { options, enabled -> options.copy(categories = enabled) }, - enabled = { it.libraryEntries }, - ), - Entry( - label = MR.strings.chapters, - getter = BackupOptions::chapters, - setter = { options, enabled -> options.copy(chapters = enabled) }, - enabled = { it.libraryEntries }, - ), - Entry( - label = MR.strings.track, - getter = BackupOptions::tracking, - setter = { options, enabled -> options.copy(tracking = enabled) }, - enabled = { it.libraryEntries }, - ), - Entry( - label = MR.strings.history, - getter = BackupOptions::history, - setter = { options, enabled -> options.copy(history = enabled) }, - enabled = { it.libraryEntries }, - ), - ) + val libraryOptions = + persistentListOf( + Entry( + label = MR.strings.manga, + getter = BackupOptions::libraryEntries, + setter = { options, enabled -> options.copy(libraryEntries = enabled) }, + ), + Entry( + label = MR.strings.categories, + getter = BackupOptions::categories, + setter = { options, enabled -> options.copy(categories = enabled) }, + enabled = { it.libraryEntries }, + ), + Entry( + label = MR.strings.chapters, + getter = BackupOptions::chapters, + setter = { options, enabled -> options.copy(chapters = enabled) }, + enabled = { it.libraryEntries }, + ), + Entry( + label = MR.strings.track, + getter = BackupOptions::tracking, + setter = { options, enabled -> options.copy(tracking = enabled) }, + enabled = { it.libraryEntries }, + ), + Entry( + label = MR.strings.history, + getter = BackupOptions::history, + setter = { options, enabled -> options.copy(history = enabled) }, + enabled = { it.libraryEntries }, + ), + ) - val settingsOptions = persistentListOf( - Entry( - label = MR.strings.app_settings, - getter = BackupOptions::appSettings, - setter = { options, enabled -> options.copy(appSettings = enabled) }, - ), - Entry( - label = MR.strings.source_settings, - getter = BackupOptions::sourceSettings, - setter = { options, enabled -> options.copy(sourceSettings = enabled) }, - ), - Entry( - label = MR.strings.private_settings, - getter = BackupOptions::privateSettings, - setter = { options, enabled -> options.copy(privateSettings = enabled) }, - enabled = { it.appSettings || it.sourceSettings }, - ), - ) + val settingsOptions = + persistentListOf( + Entry( + label = MR.strings.app_settings, + getter = BackupOptions::appSettings, + setter = { options, enabled -> options.copy(appSettings = enabled) }, + ), + Entry( + label = MR.strings.source_settings, + getter = BackupOptions::sourceSettings, + setter = { options, enabled -> options.copy(sourceSettings = enabled) }, + ), + Entry( + label = MR.strings.private_settings, + getter = BackupOptions::privateSettings, + setter = { options, enabled -> options.copy(privateSettings = enabled) }, + enabled = { it.appSettings || it.sourceSettings }, + ), + ) - fun fromBooleanArray(array: BooleanArray) = BackupOptions( - libraryEntries = array[0], - categories = array[1], - chapters = array[2], - tracking = array[3], - history = array[4], - appSettings = array[5], - sourceSettings = array[6], - privateSettings = array[7], - ) + fun fromBooleanArray(array: BooleanArray) = + BackupOptions( + libraryEntries = array[0], + categories = array[1], + chapters = array[2], + tracking = array[3], + history = array[4], + appSettings = array[5], + sourceSettings = array[6], + privateSettings = array[7], + ) } data class Entry( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt index e1ed56ee1a..380f49e4e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt @@ -10,10 +10,9 @@ import uy.kohesive.injekt.api.get class CategoriesBackupCreator( private val getCategories: GetCategories = Injekt.get(), ) { - - suspend fun backupCategories(): List { - return getCategories.await() + suspend fun backupCategories(): List = + getCategories + .await() .filterNot(Category::isSystemCategory) .map(backupCategoryMapper) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt index fd10293a97..cb7e137c44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt @@ -19,31 +19,36 @@ class MangaBackupCreator( private val getCategories: GetCategories = Injekt.get(), private val getHistory: GetHistory = Injekt.get(), ) { - - suspend fun backupMangas(mangas: List, options: BackupOptions): List { - return mangas.map { + suspend fun backupMangas( + mangas: List, + options: BackupOptions, + ): List = + mangas.map { backupManga(it, options) } - } - private suspend fun backupManga(manga: Manga, options: BackupOptions): BackupManga { + private suspend fun backupManga( + manga: Manga, + options: BackupOptions, + ): BackupManga { // Entry for this manga val mangaObject = manga.toBackupManga() - mangaObject.excludedScanlators = handler.awaitList { - excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id) - } + mangaObject.excludedScanlators = + handler.awaitList { + excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id) + } if (options.chapters) { // Backup all the chapters - handler.awaitList { - chaptersQueries.getChaptersByMangaId( - mangaId = manga.id, - applyScanlatorFilter = 0, // false - mapper = backupChapterMapper, - ) - } - .takeUnless(List::isEmpty) + handler + .awaitList { + chaptersQueries.getChaptersByMangaId( + mangaId = manga.id, + applyScanlatorFilter = 0, // false + mapper = backupChapterMapper, + ) + }.takeUnless(List::isEmpty) ?.let { mangaObject.chapters = it } } @@ -65,10 +70,11 @@ class MangaBackupCreator( if (options.history) { val historyByMangaId = getHistory.await(manga.id) if (historyByMangaId.isNotEmpty()) { - val history = historyByMangaId.map { history -> - val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) } - BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration) - } + val history = + historyByMangaId.map { history -> + val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) } + BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration) + } if (history.isNotEmpty()) { mangaObject.history = history } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt index d14cef2307..377f4c10dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt @@ -21,28 +21,30 @@ class PreferenceBackupCreator( private val sourceManager: SourceManager = Injekt.get(), private val preferenceStore: PreferenceStore = Injekt.get(), ) { - - fun backupAppPreferences(includePrivatePreferences: Boolean): List { - return preferenceStore.getAll().toBackupPreferences() + fun backupAppPreferences(includePrivatePreferences: Boolean): List = + preferenceStore + .getAll() + .toBackupPreferences() .withPrivatePreferences(includePrivatePreferences) - } - fun backupSourcePreferences(includePrivatePreferences: Boolean): List { - return sourceManager.getCatalogueSources() + fun backupSourcePreferences(includePrivatePreferences: Boolean): List = + sourceManager + .getCatalogueSources() .filterIsInstance() .map { BackupSourcePreferences( it.preferenceKey(), - it.sourcePreferences().all.toBackupPreferences() + it + .sourcePreferences() + .all + .toBackupPreferences() .withPrivatePreferences(includePrivatePreferences), ) - } - .filter { it.prefs.isNotEmpty() } - } + }.filter { it.prefs.isNotEmpty() } @Suppress("UNCHECKED_CAST") - private fun Map.toBackupPreferences(): List { - return this + private fun Map.toBackupPreferences(): List = + this .filterKeys { !Preference.isAppState(it) } .mapNotNull { (key, value) -> when (value) { @@ -51,13 +53,13 @@ class PreferenceBackupCreator( is Float -> BackupPreference(key, FloatPreferenceValue(value)) is String -> BackupPreference(key, StringPreferenceValue(value)) is Boolean -> BackupPreference(key, BooleanPreferenceValue(value)) - is Set<*> -> (value as? Set)?.let { - BackupPreference(key, StringSetPreferenceValue(it)) - } + is Set<*> -> + (value as? Set)?.let { + BackupPreference(key, StringSetPreferenceValue(it)) + } else -> null } } - } private fun List.withPrivatePreferences(include: Boolean) = if (include) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt index 075e449a72..75b18e3eb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt @@ -10,16 +10,14 @@ import uy.kohesive.injekt.api.get class SourcesBackupCreator( private val sourceManager: SourceManager = Injekt.get(), ) { - - fun backupSources(mangas: List): List { - return mangas + fun backupSources(mangas: List): List = + mangas .asSequence() .map(Manga::source) .distinct() .map(sourceManager::getOrStub) .map { it.toBackupSource() } .toList() - } } private fun Source.toBackupSource() = diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt index df517e8eda..cb6913261a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt @@ -11,12 +11,13 @@ class BackupCategory( // @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x @ProtoNumber(100) var flags: Long = 0, ) { - fun toCategory(id: Long) = Category( - id = id, - name = this@BackupCategory.name, - flags = this@BackupCategory.flags, - order = this@BackupCategory.order, - ) + fun toCategory(id: Long) = + Category( + id = id, + name = this@BackupCategory.name, + flags = this@BackupCategory.flags, + order = this@BackupCategory.order, + ) } val backupCategoryMapper = { category: Category -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt index d729efe165..908f4eb571 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt @@ -24,8 +24,8 @@ data class BackupChapter( @ProtoNumber(11) var lastModifiedAt: Long = 0, @ProtoNumber(12) var version: Long = 0, ) { - fun toChapterImpl(): Chapter { - return Chapter.create().copy( + fun toChapterImpl(): Chapter = + Chapter.create().copy( url = this@BackupChapter.url, name = this@BackupChapter.name, chapterNumber = this@BackupChapter.chapterNumber.toDouble(), @@ -39,7 +39,6 @@ data class BackupChapter( lastModifiedAt = this@BackupChapter.lastModifiedAt, version = this@BackupChapter.version, ) - } } val backupChapterMapper = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt index 1108a376e7..8262394c9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt @@ -11,12 +11,11 @@ data class BackupHistory( @ProtoNumber(2) var lastRead: Long, @ProtoNumber(3) var readDuration: Long = 0, ) { - fun getHistoryImpl(): History { - return History.create().copy( + fun getHistoryImpl(): History = + History.create().copy( readAt = Date(lastRead), readDuration = readDuration, ) - } } @Deprecated("Replaced with BackupHistory. This is retained for legacy reasons.") @@ -26,7 +25,5 @@ data class BrokenBackupHistory( @ProtoNumber(1) var lastRead: Long, @ProtoNumber(2) var readDuration: Long = 0, ) { - fun toBackupHistory(): BackupHistory { - return BackupHistory(url, lastRead, readDuration) - } + fun toBackupHistory(): BackupHistory = BackupHistory(url, lastRead, readDuration) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index 410ecc67ad..c2e1bf7527 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt @@ -44,8 +44,8 @@ data class BackupManga( @ProtoNumber(108) var excludedScanlators: List = emptyList(), @ProtoNumber(109) var version: Long = 0, ) { - fun getMangaImpl(): Manga { - return Manga.create().copy( + fun getMangaImpl(): Manga = + Manga.create().copy( url = this@BackupManga.url, title = this@BackupManga.title, artist = this@BackupManga.artist, @@ -64,5 +64,4 @@ data class BackupManga( favoriteModifiedAt = this@BackupManga.favoriteModifiedAt, version = this@BackupManga.version, ) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt index 3884f37e3f..42d41db90c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt @@ -19,19 +19,31 @@ data class BackupSourcePreferences( sealed class PreferenceValue @Serializable -data class IntPreferenceValue(val value: Int) : PreferenceValue() +data class IntPreferenceValue( + val value: Int, +) : PreferenceValue() @Serializable -data class LongPreferenceValue(val value: Long) : PreferenceValue() +data class LongPreferenceValue( + val value: Long, +) : PreferenceValue() @Serializable -data class FloatPreferenceValue(val value: Float) : PreferenceValue() +data class FloatPreferenceValue( + val value: Float, +) : PreferenceValue() @Serializable -data class StringPreferenceValue(val value: String) : PreferenceValue() +data class StringPreferenceValue( + val value: String, +) : PreferenceValue() @Serializable -data class BooleanPreferenceValue(val value: Boolean) : PreferenceValue() +data class BooleanPreferenceValue( + val value: Boolean, +) : PreferenceValue() @Serializable -data class StringSetPreferenceValue(val value: Set) : PreferenceValue() +data class StringSetPreferenceValue( + val value: Set, +) : PreferenceValue() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt index 910a8adac0..08fe45131b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt @@ -27,18 +27,18 @@ data class BackupTracking( @ProtoNumber(11) var finishedReadingDate: Long = 0, @ProtoNumber(100) var mediaId: Long = 0, ) { - @Suppress("DEPRECATION") - fun getTrackImpl(): Track { - return Track( + fun getTrackImpl(): Track = + Track( id = -1, mangaId = -1, trackerId = this@BackupTracking.syncId.toLong(), - remoteId = if (this@BackupTracking.mediaIdInt != 0) { - this@BackupTracking.mediaIdInt.toLong() - } else { - this@BackupTracking.mediaId - }, + remoteId = + if (this@BackupTracking.mediaIdInt != 0) { + this@BackupTracking.mediaIdInt.toLong() + } else { + this@BackupTracking.mediaId + }, libraryId = this@BackupTracking.libraryId, title = this@BackupTracking.title, lastChapterRead = this@BackupTracking.lastChapterRead.toDouble(), @@ -49,7 +49,6 @@ data class BackupTracking( finishDate = this@BackupTracking.finishedReadingDate, remoteUrl = this@BackupTracking.trackingUrl, ) - } } val backupTrackMapper = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt index 0ecf97e067..75c8ee9c23 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt @@ -23,9 +23,10 @@ import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.system.logcat import tachiyomi.i18n.MR -class BackupRestoreJob(private val context: Context, workerParams: WorkerParameters) : - CoroutineWorker(context, workerParams) { - +class BackupRestoreJob( + private val context: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(context, workerParams) { private val notifier = BackupNotifier(context) override suspend fun doWork(): Result { @@ -57,8 +58,8 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet } } - override suspend fun getForegroundInfo(): ForegroundInfo { - return ForegroundInfo( + override suspend fun getForegroundInfo(): ForegroundInfo = + ForegroundInfo( Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build(), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -67,12 +68,9 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet 0 }, ) - } companion object { - fun isRunning(context: Context): Boolean { - return context.workManager.isRunning(TAG) - } + fun isRunning(context: Context): Boolean = context.workManager.isRunning(TAG) fun start( context: Context, @@ -80,15 +78,17 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet options: RestoreOptions, sync: Boolean = false, ) { - val inputData = workDataOf( - LOCATION_URI_KEY to uri.toString(), - SYNC_KEY to sync, - OPTIONS_KEY to options.asBooleanArray(), - ) - val request = OneTimeWorkRequestBuilder() - .addTag(TAG) - .setInputData(inputData) - .build() + val inputData = + workDataOf( + LOCATION_URI_KEY to uri.toString(), + SYNC_KEY to sync, + OPTIONS_KEY to options.asBooleanArray(), + ) + val request = + OneTimeWorkRequestBuilder() + .addTag(TAG) + .setInputData(inputData) + .build() context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request) } 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 a17d33f43f..bf731240f4 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 @@ -27,12 +27,10 @@ class BackupRestorer( private val context: Context, private val notifier: BackupNotifier, private val isSync: Boolean, - private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(), private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context), private val mangaRestorer: MangaRestorer = MangaRestorer(), ) { - private var restoreAmount = 0 private var restoreProgress = 0 private val errors = mutableListOf>() @@ -42,7 +40,10 @@ class BackupRestorer( */ private var sourceMapping: Map = emptyMap() - suspend fun restore(uri: Uri, options: RestoreOptions) { + suspend fun restore( + uri: Uri, + options: RestoreOptions, + ) { val startTime = System.currentTimeMillis() restoreFromFile(uri, options) @@ -60,7 +61,10 @@ class BackupRestorer( ) } - private suspend fun restoreFromFile(uri: Uri, options: RestoreOptions) { + private suspend fun restoreFromFile( + uri: Uri, + options: RestoreOptions, + ) { val backup = BackupDecoder(context).decode(uri) // Store source mapping for error messages @@ -95,24 +99,26 @@ class BackupRestorer( } } - private fun CoroutineScope.restoreCategories(backupCategories: List) = launch { - ensureActive() - categoriesRestorer.restoreCategories(backupCategories) - - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(MR.strings.categories), - restoreProgress, - restoreAmount, - isSync, - ) - } + private fun CoroutineScope.restoreCategories(backupCategories: List) = + launch { + ensureActive() + categoriesRestorer.restoreCategories(backupCategories) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.categories), + restoreProgress, + restoreAmount, + isSync, + ) + } private fun CoroutineScope.restoreManga( backupMangas: List, backupCategories: List, ) = launch { - mangaRestorer.sortByNew(backupMangas) + mangaRestorer + .sortByNew(backupMangas) .forEach { ensureActive() @@ -128,31 +134,33 @@ class BackupRestorer( } } - private fun CoroutineScope.restoreAppPreferences(preferences: List) = launch { - ensureActive() - preferenceRestorer.restoreAppPreferences(preferences) - - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(MR.strings.app_settings), - restoreProgress, - restoreAmount, - isSync, - ) - } - - private fun CoroutineScope.restoreSourcePreferences(preferences: List) = launch { - ensureActive() - preferenceRestorer.restoreSourcePreferences(preferences) + private fun CoroutineScope.restoreAppPreferences(preferences: List) = + launch { + ensureActive() + preferenceRestorer.restoreAppPreferences(preferences) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.app_settings), + restoreProgress, + restoreAmount, + isSync, + ) + } - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(MR.strings.source_settings), - restoreProgress, - restoreAmount, - isSync, - ) - } + private fun CoroutineScope.restoreSourcePreferences(preferences: List) = + launch { + ensureActive() + preferenceRestorer.restoreSourcePreferences(preferences) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.source_settings), + restoreProgress, + restoreAmount, + isSync, + ) + } private fun writeErrorLog(): File { try { 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 c824cb3d4f..e3e3544d72 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,39 +9,41 @@ data class RestoreOptions( val appSettings: Boolean = true, val sourceSettings: Boolean = true, ) { - - fun asBooleanArray() = booleanArrayOf( - library, - appSettings, - sourceSettings, - ) + fun asBooleanArray() = + booleanArrayOf( + library, + appSettings, + sourceSettings, + ) fun anyEnabled() = library || appSettings || sourceSettings companion object { - val options = persistentListOf( - Entry( - label = MR.strings.label_library, - getter = RestoreOptions::library, - setter = { options, enabled -> options.copy(library = enabled) }, - ), - Entry( - label = MR.strings.app_settings, - getter = RestoreOptions::appSettings, - setter = { options, enabled -> options.copy(appSettings = enabled) }, - ), - Entry( - label = MR.strings.source_settings, - getter = RestoreOptions::sourceSettings, - setter = { options, enabled -> options.copy(sourceSettings = enabled) }, - ), - ) + val options = + persistentListOf( + Entry( + label = MR.strings.label_library, + getter = RestoreOptions::library, + setter = { options, enabled -> options.copy(library = enabled) }, + ), + Entry( + label = MR.strings.app_settings, + getter = RestoreOptions::appSettings, + setter = { options, enabled -> options.copy(appSettings = enabled) }, + ), + Entry( + label = MR.strings.source_settings, + getter = RestoreOptions::sourceSettings, + setter = { options, enabled -> options.copy(sourceSettings = enabled) }, + ), + ) - fun fromBooleanArray(array: BooleanArray) = RestoreOptions( - library = array[0], - appSettings = array[1], - sourceSettings = array[2], - ) + fun fromBooleanArray(array: BooleanArray) = + RestoreOptions( + library = array[0], + appSettings = array[1], + sourceSettings = array[2], + ) } data class Entry( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt index 23a2d47fa6..2e378da75b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt @@ -12,25 +12,25 @@ class CategoriesRestorer( private val getCategories: GetCategories = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), ) { - suspend fun restoreCategories(backupCategories: List) { if (backupCategories.isNotEmpty()) { val dbCategories = getCategories.await() val dbCategoriesByName = dbCategories.associateBy { it.name } var nextOrder = dbCategories.maxOfOrNull { it.order }?.plus(1) ?: 0 - val categories = backupCategories - .sortedBy { it.order } - .map { - val dbCategory = dbCategoriesByName[it.name] - if (dbCategory != null) return@map dbCategory - val order = nextOrder++ - handler.awaitOneExecutable { - categoriesQueries.insert(it.name, order, it.flags) - categoriesQueries.selectLastInsertedRowId() + val categories = + backupCategories + .sortedBy { it.order } + .map { + val dbCategory = dbCategoriesByName[it.name] + if (dbCategory != null) return@map dbCategory + val order = nextOrder++ + handler + .awaitOneExecutable { + categoriesQueries.insert(it.name, order, it.flags) + categoriesQueries.selectLastInsertedRowId() + }.let { id -> it.toCategory(id).copy(order = order) } } - .let { id -> it.toCategory(id).copy(order = order) } - } libraryPreferences.categorizedDisplaySettings().set( (dbCategories + categories) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt index b0c86e2ab1..bcb6ecfb6f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt @@ -33,7 +33,6 @@ class MangaRestorer( private val insertTrack: InsertTrack = Injekt.get(), fetchInterval: FetchInterval = Injekt.get(), ) { - private var now = ZonedDateTime.now() private var currentFetchWindow = fetchInterval.getWindow(now) @@ -43,8 +42,10 @@ class MangaRestorer( } suspend fun sortByNew(backupMangas: List): List { - val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() } - .groupBy({ it.source }, { it.url }) + val urlsBySource = + handler + .awaitList { mangasQueries.getAllMangaSourceAndUrl() } + .groupBy({ it.source }, { it.url }) return backupMangas .sortedWith( @@ -60,11 +61,12 @@ class MangaRestorer( handler.await(inTransaction = true) { val dbManga = findExistingManga(backupManga) val manga = backupManga.getMangaImpl() - val restoredManga = if (dbManga == null) { - restoreNewManga(manga) - } else { - restoreExistingManga(manga, dbManga) - } + val restoredManga = + if (dbManga == null) { + restoreNewManga(manga) + } else { + restoreExistingManga(manga, dbManga) + } restoreMangaDetails( manga = restoredManga, @@ -78,20 +80,21 @@ class MangaRestorer( } } - private suspend fun findExistingManga(backupManga: BackupManga): Manga? { - return getMangaByUrlAndSourceId.await(backupManga.url, backupManga.source) - } + private suspend fun findExistingManga(backupManga: BackupManga): Manga? = + getMangaByUrlAndSourceId.await(backupManga.url, backupManga.source) - private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga { - return if (manga.version > dbManga.version) { + private suspend fun restoreExistingManga( + manga: Manga, + dbManga: Manga, + ): Manga = + if (manga.version > dbManga.version) { updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id)) } else { updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id)) } - } - private fun Manga.copyFrom(newer: Manga): Manga { - return this.copy( + private fun Manga.copyFrom(newer: Manga): Manga = + this.copy( favorite = this.favorite || newer.favorite, author = newer.author, artist = newer.artist, @@ -102,7 +105,6 @@ class MangaRestorer( initialized = this.initialized || newer.initialized, version = newer.version, ) - } private suspend fun updateManga(manga: Manga): Manga { handler.await(true) { @@ -134,53 +136,59 @@ class MangaRestorer( return manga } - private suspend fun restoreNewManga( - manga: Manga, - ): Manga { - return manga.copy( + private suspend fun restoreNewManga(manga: Manga): Manga = + manga.copy( initialized = manga.description != null, id = insertManga(manga), version = manga.version, ) - } - private suspend fun restoreChapters(manga: Manga, backupChapters: List) { - val dbChaptersByUrl = getChaptersByMangaId.await(manga.id) - .associateBy { it.url } - - val (existingChapters, newChapters) = backupChapters - .mapNotNull { - val chapter = it.toChapterImpl().copy(mangaId = manga.id) - - val dbChapter = dbChaptersByUrl[chapter.url] - ?: // New chapter - return@mapNotNull chapter - - if (chapter.forComparison() == dbChapter.forComparison()) { - // Same state; skip - return@mapNotNull null - } - - // Update to an existing chapter - var updatedChapter = chapter - .copyFrom(dbChapter) - .copy( - id = dbChapter.id, - bookmark = chapter.bookmark || dbChapter.bookmark, - ) - if (dbChapter.read && !updatedChapter.read) { - updatedChapter = updatedChapter.copy( - read = true, - lastPageRead = dbChapter.lastPageRead, - ) - } else if (updatedChapter.lastPageRead == 0L && dbChapter.lastPageRead != 0L) { - updatedChapter = updatedChapter.copy( - lastPageRead = dbChapter.lastPageRead, - ) - } - updatedChapter - } - .partition { it.id > 0 } + private suspend fun restoreChapters( + manga: Manga, + backupChapters: List, + ) { + val dbChaptersByUrl = + getChaptersByMangaId + .await(manga.id) + .associateBy { it.url } + + val (existingChapters, newChapters) = + backupChapters + .mapNotNull { + val chapter = it.toChapterImpl().copy(mangaId = manga.id) + + val dbChapter = + dbChaptersByUrl[chapter.url] + ?: // New chapter + return@mapNotNull chapter + + if (chapter.forComparison() == dbChapter.forComparison()) { + // Same state; skip + return@mapNotNull null + } + + // Update to an existing chapter + var updatedChapter = + chapter + .copyFrom(dbChapter) + .copy( + id = dbChapter.id, + bookmark = chapter.bookmark || dbChapter.bookmark, + ) + if (dbChapter.read && !updatedChapter.read) { + updatedChapter = + updatedChapter.copy( + read = true, + lastPageRead = dbChapter.lastPageRead, + ) + } else if (updatedChapter.lastPageRead == 0L && dbChapter.lastPageRead != 0L) { + updatedChapter = + updatedChapter.copy( + lastPageRead = dbChapter.lastPageRead, + ) + } + updatedChapter + }.partition { it.id > 0 } insertNewChapters(newChapters) updateExistingChapters(existingChapters) @@ -238,8 +246,8 @@ class MangaRestorer( * * @return id of [Manga], null if not found */ - private suspend fun insertManga(manga: Manga): Long { - return handler.awaitOneExecutable(true) { + private suspend fun insertManga(manga: Manga): Long = + handler.awaitOneExecutable(true) { mangasQueries.insert( source = manga.source, url = manga.url, @@ -264,7 +272,6 @@ class MangaRestorer( ) mangasQueries.selectLastInsertedRowId() } - } private suspend fun restoreMangaDetails( manga: Manga, @@ -300,13 +307,14 @@ class MangaRestorer( val backupCategoriesByOrder = backupCategories.associateBy { it.order } - val mangaCategoriesToUpdate = categories.mapNotNull { backupCategoryOrder -> - backupCategoriesByOrder[backupCategoryOrder]?.let { backupCategory -> - dbCategoriesByName[backupCategory.name]?.let { dbCategory -> - Pair(manga.id, dbCategory.id) + val mangaCategoriesToUpdate = + categories.mapNotNull { backupCategoryOrder -> + backupCategoriesByOrder[backupCategoryOrder]?.let { backupCategory -> + dbCategoriesByName[backupCategory.name]?.let { dbCategory -> + Pair(manga.id, dbCategory.id) + } } } - } if (mangaCategoriesToUpdate.isNotEmpty()) { handler.await(true) { @@ -319,31 +327,33 @@ class MangaRestorer( } private suspend fun restoreHistory(backupHistory: List) { - val toUpdate = backupHistory.mapNotNull { history -> - val dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(history.url) } - val item = history.getHistoryImpl() - - if (dbHistory == null) { - val chapter = handler.awaitOneOrNull { chaptersQueries.getChapterByUrl(history.url) } - return@mapNotNull if (chapter == null) { - // Chapter doesn't exist; skip - null - } else { - // New history entry - item.copy(chapterId = chapter._id) + val toUpdate = + backupHistory.mapNotNull { history -> + val dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(history.url) } + val item = history.getHistoryImpl() + + if (dbHistory == null) { + val chapter = handler.awaitOneOrNull { chaptersQueries.getChapterByUrl(history.url) } + return@mapNotNull if (chapter == null) { + // Chapter doesn't exist; skip + null + } else { + // New history entry + item.copy(chapterId = chapter._id) + } } - } - // Update history entry - item.copy( - id = dbHistory._id, - chapterId = dbHistory.chapter_id, - readAt = max(item.readAt?.time ?: 0L, dbHistory.last_read?.time ?: 0L) - .takeIf { it > 0L } - ?.let { Date(it) }, - readDuration = max(item.readDuration, dbHistory.time_read) - dbHistory.time_read, - ) - } + // Update history entry + item.copy( + id = dbHistory._id, + chapterId = dbHistory.chapter_id, + readAt = + max(item.readAt?.time ?: 0L, dbHistory.last_read?.time ?: 0L) + .takeIf { it > 0L } + ?.let { Date(it) }, + readDuration = max(item.readDuration, dbHistory.time_read) - dbHistory.time_read, + ) + } if (toUpdate.isNotEmpty()) { handler.await(true) { @@ -358,32 +368,36 @@ class MangaRestorer( } } - private suspend fun restoreTracking(manga: Manga, backupTracks: List) { + private suspend fun restoreTracking( + manga: Manga, + backupTracks: List, + ) { val dbTrackByTrackerId = getTracks.await(manga.id).associateBy { it.trackerId } - val (existingTracks, newTracks) = backupTracks - .mapNotNull { - val track = it.getTrackImpl() - val dbTrack = dbTrackByTrackerId[track.trackerId] - ?: // New track - return@mapNotNull track.copy( - id = 0, // Let DB assign new ID - mangaId = manga.id, + val (existingTracks, newTracks) = + backupTracks + .mapNotNull { + val track = it.getTrackImpl() + val dbTrack = + dbTrackByTrackerId[track.trackerId] + ?: // New track + return@mapNotNull track.copy( + id = 0, // Let DB assign new ID + mangaId = manga.id, + ) + + if (track.forComparison() == dbTrack.forComparison()) { + // Same state; skip + return@mapNotNull null + } + + // Update to an existing track + dbTrack.copy( + remoteId = track.remoteId, + libraryId = track.libraryId, + lastChapterRead = max(dbTrack.lastChapterRead, track.lastChapterRead), ) - - if (track.forComparison() == dbTrack.forComparison()) { - // Same state; skip - return@mapNotNull null - } - - // Update to an existing track - dbTrack.copy( - remoteId = track.remoteId, - libraryId = track.libraryId, - lastChapterRead = max(dbTrack.lastChapterRead, track.lastChapterRead), - ) - } - .partition { it.id > 0 } + }.partition { it.id > 0 } if (newTracks.isNotEmpty()) { insertTrack.awaitAll(newTracks) @@ -419,11 +433,15 @@ class MangaRestorer( * @param manga the manga whose excluded scanlators have to be restored. * @param excludedScanlators the excluded scanlators to restore. */ - private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List) { + private suspend fun restoreExcludedScanlators( + manga: Manga, + excludedScanlators: List, + ) { if (excludedScanlators.isEmpty()) return - val existingExcludedScanlators = handler.awaitList { - excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id) - } + val existingExcludedScanlators = + handler.awaitList { + excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id) + } val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators } if (toInsert.isNotEmpty()) { handler.await { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt index 4871e3949b..fe0582a9a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt @@ -21,7 +21,6 @@ class PreferenceRestorer( private val context: Context, private val preferenceStore: PreferenceStore = Injekt.get(), ) { - fun restoreAppPreferences(preferences: List) { restorePreferences(preferences, preferenceStore) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt index bbdf85c107..41009d681f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt @@ -29,14 +29,14 @@ class ChapterCache( private val context: Context, private val json: Json, ) { - /** Cache class used for cache management. */ - private val diskCache = DiskLruCache.open( - File(context.cacheDir, "chapter_disk_cache"), - PARAMETER_APP_VERSION, - PARAMETER_VALUE_COUNT, - PARAMETER_CACHE_SIZE, - ) + private val diskCache = + DiskLruCache.open( + File(context.cacheDir, "chapter_disk_cache"), + PARAMETER_APP_VERSION, + PARAMETER_VALUE_COUNT, + PARAMETER_CACHE_SIZE, + ) /** * Returns directory of cache. @@ -77,7 +77,10 @@ class ChapterCache( * @param chapter the chapter. * @param pages list of pages. */ - fun putPageListToCache(chapter: Chapter, pages: List) { + fun putPageListToCache( + chapter: Chapter, + pages: List, + ) { // Convert list of pages to json string. val cachedValue = json.encodeToString(pages) @@ -112,13 +115,12 @@ class ChapterCache( * @param imageUrl url of image. * @return true if in cache otherwise false. */ - fun isImageInCache(imageUrl: String): Boolean { - return try { + fun isImageInCache(imageUrl: String): Boolean = + try { diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)).use { it != null } } catch (e: IOException) { false } - } /** * Get image file from url. @@ -140,7 +142,10 @@ class ChapterCache( * @throws IOException image error. */ @Throws(IOException::class) - fun putImageToCache(imageUrl: String, response: Response) { + fun putImageToCache( + imageUrl: String, + response: Response, + ) { // Initialize editor (edits the values for an entry). var editor: DiskLruCache.Editor? = null @@ -193,9 +198,7 @@ class ChapterCache( } } - private fun getKey(chapter: Chapter): String { - return "${chapter.mangaId}${chapter.url}" - } + private fun getKey(chapter: Chapter): String = "${chapter.mangaId}${chapter.url}" } /** Application cache version. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt index 73d2e0833d..36fda2dbd7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt @@ -15,8 +15,9 @@ import java.io.InputStream * @param context the application context. * @constructor creates an instance of the cover cache. */ -class CoverCache(private val context: Context) { - +class CoverCache( + private val context: Context, +) { companion object { private const val COVERS_DIR = "covers" private const val CUSTOM_COVERS_DIR = "covers/custom" @@ -35,11 +36,10 @@ class CoverCache(private val context: Context) { * @param mangaThumbnailUrl thumbnail url for the manga. * @return cover image. */ - fun getCoverFile(mangaThumbnailUrl: String?): File? { - return mangaThumbnailUrl?.let { + fun getCoverFile(mangaThumbnailUrl: String?): File? = + mangaThumbnailUrl?.let { File(cacheDir, DiskUtil.hashKeyForDisk(it)) } - } /** * Returns the custom cover from cache. @@ -47,9 +47,8 @@ class CoverCache(private val context: Context) { * @param mangaId the manga id. * @return cover image. */ - fun getCustomCoverFile(mangaId: Long?): File { - return File(customCoverCacheDir, DiskUtil.hashKeyForDisk(mangaId.toString())) - } + fun getCustomCoverFile(mangaId: Long?): File = + File(customCoverCacheDir, DiskUtil.hashKeyForDisk(mangaId.toString())) /** * Saves the given stream as the manga's custom cover to cache. @@ -59,7 +58,10 @@ class CoverCache(private val context: Context) { * @throws IOException if there's any error. */ @Throws(IOException::class) - fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) { + fun setCustomCoverToCache( + manga: Manga, + inputStream: InputStream, + ) { getCustomCoverFile(manga.id).outputStream().use { inputStream.copyTo(it) } @@ -72,7 +74,10 @@ class CoverCache(private val context: Context) { * @param deleteCustomCover whether the custom cover should be deleted. * @return number of files that were deleted. */ - fun deleteFromCache(manga: Manga, deleteCustomCover: Boolean = false): Int { + fun deleteFromCache( + manga: Manga, + deleteCustomCover: Boolean = false, + ): Int { var deleted = 0 getCoverFile(manga.thumbnailUrl)?.let { @@ -92,14 +97,12 @@ class CoverCache(private val context: Context) { * @param mangaId the manga id. * @return whether the cover was deleted. */ - fun deleteCustomCover(mangaId: Long?): Boolean { - return getCustomCoverFile(mangaId).let { + fun deleteCustomCover(mangaId: Long?): Boolean = + getCustomCoverFile(mangaId).let { it.exists() && it.delete() } - } - private fun getCacheDir(dir: String): File { - return context.getExternalFilesDir(dir) + private fun getCacheDir(dir: String): File = + context.getExternalFilesDir(dir) ?: File(context.filesDir, dir).also { it.mkdirs() } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/BufferedSourceFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/BufferedSourceFetcher.kt index 4bee925ed5..9f0208b0f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/BufferedSourceFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/BufferedSourceFetcher.kt @@ -13,26 +13,22 @@ class BufferedSourceFetcher( private val data: BufferedSource, private val options: Options, ) : Fetcher { - - override suspend fun fetch(): FetchResult { - return SourceFetchResult( - source = ImageSource( - source = data, - fileSystem = options.fileSystem, - ), + override suspend fun fetch(): FetchResult = + SourceFetchResult( + source = + ImageSource( + source = data, + fileSystem = options.fileSystem, + ), mimeType = null, dataSource = DataSource.MEMORY, ) - } class Factory : Fetcher.Factory { - override fun create( data: BufferedSource, options: Options, imageLoader: ImageLoader, - ): Fetcher { - return BufferedSourceFetcher(data, options) - } + ): Fetcher = BufferedSourceFetcher(data, options) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt index 65a1420994..c44a28b822 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt @@ -57,7 +57,6 @@ class MangaCoverFetcher( private val callFactoryLazy: Lazy, private val imageLoader: ImageLoader, ) : Fetcher { - private val diskCacheKey: String get() = diskCacheKeyLazy.value @@ -81,23 +80,25 @@ class MangaCoverFetcher( } } - private fun fileLoader(file: File): FetchResult { - return SourceFetchResult( - source = ImageSource( - file = file.toOkioPath(), - fileSystem = FileSystem.SYSTEM, - diskCacheKey = diskCacheKey - ), + private fun fileLoader(file: File): FetchResult = + SourceFetchResult( + source = + ImageSource( + file = file.toOkioPath(), + fileSystem = FileSystem.SYSTEM, + diskCacheKey = diskCacheKey, + ), mimeType = "image/*", dataSource = DataSource.DISK, ) - } private fun fileUriLoader(uri: String): FetchResult { - val source = UniFile.fromUri(options.context, uri.toUri())!! - .openInputStream() - .source() - .buffer() + val source = + UniFile + .fromUri(options.context, uri.toUri())!! + .openInputStream() + .source() + .buffer() return SourceFetchResult( source = ImageSource(source = source, fileSystem = FileSystem.SYSTEM), mimeType = "image/*", @@ -107,11 +108,12 @@ class MangaCoverFetcher( private suspend fun httpLoader(): FetchResult { // Only cache separately if it's a library item - val libraryCoverCacheFile = if (isLibraryManga) { - coverFileLazy.value ?: error("No cover specified") - } else { - null - } + val libraryCoverCacheFile = + if (isLibraryManga) { + coverFileLazy.value ?: error("No cover specified") + } else { + null + } if (libraryCoverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) { return fileLoader(libraryCoverCacheFile) } @@ -181,14 +183,15 @@ class MangaCoverFetcher( } private fun newRequest(): Request { - val request = Request.Builder().apply { - url(url!!) + val request = + Request.Builder().apply { + url(url!!) - val sourceHeaders = sourceLazy.value?.headers - if (sourceHeaders != null) { - headers(sourceHeaders) + val sourceHeaders = sourceLazy.value?.headers + if (sourceHeaders != null) { + headers(sourceHeaders) + } } - } when { options.networkCachePolicy.readEnabled -> { @@ -204,7 +207,10 @@ class MangaCoverFetcher( return request.build() } - private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? { + private fun moveSnapshotToCoverCache( + snapshot: DiskCache.Snapshot, + cacheFile: File?, + ): File? { if (cacheFile == null) return null return try { imageLoader.diskCache?.run { @@ -220,7 +226,10 @@ class MangaCoverFetcher( } } - private fun writeResponseToCoverCache(response: Response, cacheFile: File?): File? { + private fun writeResponseToCoverCache( + response: Response, + cacheFile: File?, + ): File? { if (cacheFile == null || !options.diskCachePolicy.writeEnabled) return null return try { response.peekBody(Long.MAX_VALUE).source().use { input -> @@ -233,7 +242,10 @@ class MangaCoverFetcher( } } - private fun writeSourceToCoverCache(input: Source, cacheFile: File) { + private fun writeSourceToCoverCache( + input: Source, + cacheFile: File, + ) { cacheFile.parentFile?.mkdirs() cacheFile.delete() try { @@ -246,17 +258,14 @@ class MangaCoverFetcher( } } - private fun readFromDiskCache(): DiskCache.Snapshot? { - return if (options.diskCachePolicy.readEnabled) { + private fun readFromDiskCache(): DiskCache.Snapshot? = + if (options.diskCachePolicy.readEnabled) { imageLoader.diskCache?.openSnapshot(diskCacheKey) } else { null } - } - private fun writeToDiskCache( - response: Response, - ): DiskCache.Snapshot? { + private fun writeToDiskCache(response: Response): DiskCache.Snapshot? { val diskCache = imageLoader.diskCache val editor = diskCache?.openEditor(diskCacheKey) ?: return null try { @@ -273,24 +282,22 @@ class MangaCoverFetcher( } } - private fun DiskCache.Snapshot.toImageSource(): ImageSource { - return ImageSource( + private fun DiskCache.Snapshot.toImageSource(): ImageSource = + ImageSource( file = data, fileSystem = FileSystem.SYSTEM, diskCacheKey = diskCacheKey, closeable = this, ) - } - private fun getResourceType(cover: String?): Type? { - return when { + private fun getResourceType(cover: String?): Type? = + when { cover.isNullOrEmpty() -> null cover.startsWith("http", true) || cover.startsWith("Custom-", true) -> Type.URL cover.startsWith("/") || cover.startsWith("file://") -> Type.File cover.startsWith("content") -> Type.URI else -> null } - } private enum class Type { File, @@ -301,12 +308,15 @@ class MangaCoverFetcher( class MangaFactory( private val callFactoryLazy: Lazy, ) : Fetcher.Factory { - private val coverCache: CoverCache by injectLazy() private val sourceManager: SourceManager by injectLazy() - override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher { - return MangaCoverFetcher( + override fun create( + data: Manga, + options: Options, + imageLoader: ImageLoader, + ): Fetcher = + MangaCoverFetcher( url = data.thumbnailUrl, isLibraryManga = data.favorite, options = options, @@ -317,18 +327,20 @@ class MangaCoverFetcher( callFactoryLazy = callFactoryLazy, imageLoader = imageLoader, ) - } } class MangaCoverFactory( private val callFactoryLazy: Lazy, ) : Fetcher.Factory { - private val coverCache: CoverCache by injectLazy() private val sourceManager: SourceManager by injectLazy() - override fun create(data: MangaCover, options: Options, imageLoader: ImageLoader): Fetcher { - return MangaCoverFetcher( + override fun create( + data: MangaCover, + options: Options, + imageLoader: ImageLoader, + ): Fetcher = + MangaCoverFetcher( url = data.url, isLibraryManga = data.isMangaFavorite, options = options, @@ -339,14 +351,18 @@ class MangaCoverFetcher( callFactoryLazy = callFactoryLazy, imageLoader = imageLoader, ) - } } companion object { val USE_CUSTOM_COVER_KEY = Extras.Key(true) private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build() - private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build() + private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = + CacheControl + .Builder() + .noCache() + .onlyIfCached() + .build() private const val HTTP_NOT_MODIFIED = 304 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt index 56e0291aa2..861a7f6c22 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt @@ -10,23 +10,27 @@ import uy.kohesive.injekt.api.get import tachiyomi.domain.manga.model.Manga as DomainManga class MangaKeyer : Keyer { - override fun key(data: DomainManga, options: Options): String { - return if (data.hasCustomCover()) { + override fun key( + data: DomainManga, + options: Options, + ): String = + if (data.hasCustomCover()) { "${data.id};${data.coverLastModified}" } else { "${data.thumbnailUrl};${data.coverLastModified}" } - } } class MangaCoverKeyer( private val coverCache: CoverCache = Injekt.get(), ) : Keyer { - override fun key(data: MangaCover, options: Options): String { - return if (coverCache.getCustomCoverFile(data.mangaId).exists()) { + override fun key( + data: MangaCover, + options: Options, + ): String = + if (coverCache.getCustomCoverFile(data.mangaId).exists()) { "${data.mangaId};${data.lastModified}" } else { "${data.url};${data.lastModified}" } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt index 6403f760bd..e55c0321dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt @@ -18,12 +18,15 @@ import tachiyomi.decoder.ImageDecoder /** * A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system. */ -class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder { - +class TachiyomiImageDecoder( + private val resources: ImageSource, + private val options: Options, +) : Decoder { override suspend fun decode(): DecodeResult { - val decoder = resources.sourceOrNull()?.use { - ImageDecoder.newInstance(it.inputStream(), options.cropBorders, displayProfile) - } + val decoder = + resources.sourceOrNull()?.use { + ImageDecoder.newInstance(it.inputStream(), options.cropBorders, displayProfile) + } check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" } @@ -33,13 +36,14 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti val dstWidth = options.size.widthPx(options.scale) { srcWidth } val dstHeight = options.size.heightPx(options.scale) { srcHeight } - val sampleSize = DecodeUtils.calculateInSampleSize( - srcWidth = srcWidth, - srcHeight = srcHeight, - dstWidth = dstWidth, - dstHeight = dstHeight, - scale = options.scale, - ) + val sampleSize = + DecodeUtils.calculateInSampleSize( + srcWidth = srcWidth, + srcHeight = srcHeight, + dstWidth = dstWidth, + dstHeight = dstHeight, + scale = options.scale, + ) var bitmap = decoder.decode(sampleSize = sampleSize) decoder.recycle() @@ -64,19 +68,22 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti } class Factory : Decoder.Factory { - - override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? { - return if (options.customDecoder || isApplicable(result.source.source())) { + override fun create( + result: SourceFetchResult, + options: Options, + imageLoader: ImageLoader, + ): Decoder? = + if (options.customDecoder || isApplicable(result.source.source())) { TachiyomiImageDecoder(result.source, options) } else { null } - } private fun isApplicable(source: BufferedSource): Boolean { - val type = source.peek().inputStream().use { - ImageUtil.findImageType(it) - } + val type = + source.peek().inputStream().use { + ImageUtil.findImageType(it) + } return when (type) { ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL, ImageUtil.ImageType.HEIF -> true else -> false diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt index 7a920bf398..e58597ab38 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt @@ -10,33 +10,38 @@ import coil3.size.Size import coil3.size.isOriginal import coil3.size.pxOrElse -internal inline fun Size.widthPx(scale: Scale, original: () -> Int): Int { - return if (isOriginal) original() else width.toPx(scale) -} - -internal inline fun Size.heightPx(scale: Scale, original: () -> Int): Int { - return if (isOriginal) original() else height.toPx(scale) -} - -internal fun Dimension.toPx(scale: Scale): Int = pxOrElse { - when (scale) { - Scale.FILL -> Int.MIN_VALUE - Scale.FIT -> Int.MAX_VALUE +internal inline fun Size.widthPx( + scale: Scale, + original: () -> Int, +): Int = if (isOriginal) original() else width.toPx(scale) + +internal inline fun Size.heightPx( + scale: Scale, + original: () -> Int, +): Int = if (isOriginal) original() else height.toPx(scale) + +internal fun Dimension.toPx(scale: Scale): Int = + pxOrElse { + when (scale) { + Scale.FILL -> Int.MIN_VALUE + Scale.FIT -> Int.MAX_VALUE + } } -} -fun ImageRequest.Builder.cropBorders(enable: Boolean) = apply { - extras[cropBordersKey] = enable -} +fun ImageRequest.Builder.cropBorders(enable: Boolean) = + apply { + extras[cropBordersKey] = enable + } val Options.cropBorders: Boolean get() = getExtra(cropBordersKey) private val cropBordersKey = Extras.Key(default = false) -fun ImageRequest.Builder.customDecoder(enable: Boolean) = apply { - extras[customDecoderKey] = enable -} +fun ImageRequest.Builder.customDecoder(enable: Boolean) = + apply { + extras[customDecoderKey] = enable + } val Options.customDecoder: Boolean get() = getExtra(customDecoderKey) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt index f913680846..333dfa6aba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt @@ -4,8 +4,9 @@ import eu.kanade.tachiyomi.source.model.SChapter import java.io.Serializable import tachiyomi.domain.chapter.model.Chapter as DomainChapter -interface Chapter : SChapter, Serializable { - +interface Chapter : + SChapter, + Serializable { var id: Long? var manga_id: Long? diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt index a92dd56df5..057312848e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.database.models class ChapterImpl : Chapter { - override var id: Long? = null override var manga_id: Long? = null @@ -39,7 +38,5 @@ class ChapterImpl : Chapter { return id == chapter.id } - override fun hashCode(): Int { - return url.hashCode() + id.hashCode() - } + override fun hashCode(): Int = url.hashCode() + id.hashCode() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt index aac7ec4c37..5849c53271 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.database.models import java.io.Serializable interface Track : Serializable { - var id: Long? var manga_id: Long @@ -39,8 +38,9 @@ interface Track : Serializable { } companion object { - fun create(serviceId: Long): Track = TrackImpl().apply { - tracker_id = serviceId - } + fun create(serviceId: Long): Track = + TrackImpl().apply { + tracker_id = serviceId + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt index 85868219f1..0bef901823 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.database.models class TrackImpl : Track { - override var id: Long? = null override var manga_id: Long = 0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 0fdbabe03a..5b58e0529d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -68,13 +68,14 @@ class DownloadCache( private val extensionManager: ExtensionManager = Injekt.get(), private val storageManager: StorageManager = Injekt.get(), ) { - private val scope = CoroutineScope(Dispatchers.IO) private val _changes: Channel = Channel(Channel.UNLIMITED) - val changes = _changes.receiveAsFlow() - .onStart { emit(Unit) } - .shareIn(scope, SharingStarted.Lazily, 1) + val changes = + _changes + .receiveAsFlow() + .onStart { emit(Unit) } + .shareIn(scope, SharingStarted.Lazily, 1) /** * The interval after which this cache should be invalidated. 1 hour shouldn't cause major @@ -89,9 +90,10 @@ class DownloadCache( private var renewalJob: Job? = null private val _isInitializing = MutableStateFlow(false) - val isInitializing = _isInitializing - .debounce(1000L) // Don't notify if it finishes quickly enough - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + val isInitializing = + _isInitializing + .debounce(1000L) // Don't notify if it finishes quickly enough + .stateIn(scope, SharingStarted.WhileSubscribed(), false) private val diskCacheFile: File get() = File(context.cacheDir, "dl_index_cache_v3") @@ -105,9 +107,10 @@ class DownloadCache( rootDownloadsDirLock.withLock { try { if (diskCacheFile.exists()) { - val diskCache = diskCacheFile.inputStream().use { - ProtoBuf.decodeFromByteArray(it.readBytes()) - } + val diskCache = + diskCacheFile.inputStream().use { + ProtoBuf.decodeFromByteArray(it.readBytes()) + } rootDownloadsDir = diskCache lastRenew = System.currentTimeMillis() } @@ -150,10 +153,11 @@ class DownloadCache( if (sourceDir != null) { val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(mangaTitle)] if (mangaDir != null) { - return provider.getValidChapterDirNames( - chapterName, - chapterScanlator, - ).any { it in mangaDir.chapterDirs } + return provider + .getValidChapterDirNames( + chapterName, + chapterScanlator, + ).any { it in mangaDir.chapterDirs } } } return false @@ -197,7 +201,11 @@ class DownloadCache( * @param mangaUniFile the directory of the manga. * @param manga the manga of the chapter. */ - suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) { + suspend fun addChapter( + chapterDirName: String, + mangaUniFile: UniFile, + manga: Manga, + ) { rootDownloadsDirLock.withLock { // Retrieve the cached source directory or cache a new one var sourceDir = rootDownloadsDir.sourceDirs[manga.source] @@ -229,7 +237,10 @@ class DownloadCache( * @param chapter the chapter to remove. * @param manga the manga of the chapter. */ - suspend fun removeChapter(chapter: Chapter, manga: Manga) { + suspend fun removeChapter( + chapter: Chapter, + manga: Manga, + ) { rootDownloadsDirLock.withLock { val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return @@ -249,7 +260,10 @@ class DownloadCache( * @param chapters the list of chapter to remove. * @param manga the manga of the chapter. */ - suspend fun removeChapters(chapters: List, manga: Manga) { + suspend fun removeChapters( + chapters: List, + manga: Manga, + ) { rootDownloadsDirLock.withLock { val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return @@ -306,83 +320,89 @@ class DownloadCache( return } - renewalJob = scope.launchIO { - if (lastRenew == 0L) { - _isInitializing.emit(true) - } - - // Try to wait until extensions and sources have loaded - var sources = emptyList() - withTimeoutOrNull(30.seconds) { - extensionManager.isInitialized.first { it } - sourceManager.isInitialized.first { it } - - sources = getSources() - } - - val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } + renewalJob = + scope + .launchIO { + if (lastRenew == 0L) { + _isInitializing.emit(true) + } - rootDownloadsDirLock.withLock { - rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) + // Try to wait until extensions and sources have loaded + var sources = emptyList() + withTimeoutOrNull(30.seconds) { + extensionManager.isInitialized.first { it } + sourceManager.isInitialized.first { it } - val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty() - .filter { it.isDirectory && !it.name.isNullOrBlank() } - .mapNotNull { dir -> - val sourceId = sourceMap[dir.name!!.lowercase()] - sourceId?.let { it to SourceDirectory(dir) } + sources = getSources() } - .toMap() - rootDownloadsDir.sourceDirs = sourceDirs + val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } + + rootDownloadsDirLock.withLock { + rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) - sourceDirs.values - .map { sourceDir -> - async { - sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty() + val sourceDirs = + rootDownloadsDir.dir + ?.listFiles() + .orEmpty() .filter { it.isDirectory && !it.name.isNullOrBlank() } - .associate { it.name!! to MangaDirectory(it) } - - sourceDir.mangaDirs.values.forEach { mangaDir -> - val chapterDirs = mangaDir.dir?.listFiles().orEmpty() - .mapNotNull { - when { - // Ignore incomplete downloads - it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null - // Folder of images - it.isDirectory -> it.name - // CBZ files - it.isFile && it.extension == "cbz" -> it.nameWithoutExtension - // Anything else is irrelevant - else -> null - } + .mapNotNull { dir -> + val sourceId = sourceMap[dir.name!!.lowercase()] + sourceId?.let { it to SourceDirectory(dir) } + }.toMap() + + rootDownloadsDir.sourceDirs = sourceDirs + + sourceDirs.values + .map { sourceDir -> + async { + sourceDir.mangaDirs = + sourceDir.dir + ?.listFiles() + .orEmpty() + .filter { it.isDirectory && !it.name.isNullOrBlank() } + .associate { it.name!! to MangaDirectory(it) } + + sourceDir.mangaDirs.values.forEach { mangaDir -> + val chapterDirs = + mangaDir.dir + ?.listFiles() + .orEmpty() + .mapNotNull { + when { + // Ignore incomplete downloads + it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null + // Folder of images + it.isDirectory -> it.name + // CBZ files + it.isFile && it.extension == "cbz" -> it.nameWithoutExtension + // Anything else is irrelevant + else -> null + } + }.toMutableSet() + + mangaDir.chapterDirs = chapterDirs } - .toMutableSet() + } + }.awaitAll() + } - mangaDir.chapterDirs = chapterDirs - } + _isInitializing.emit(false) + }.also { + it.invokeOnCompletion(onCancelling = true) { exception -> + if (exception != null && exception !is CancellationException) { + logcat(LogPriority.ERROR, exception) { "DownloadCache: failed to create cache" } } + lastRenew = System.currentTimeMillis() + notifyChanges() } - .awaitAll() - } - - _isInitializing.emit(false) - }.also { - it.invokeOnCompletion(onCancelling = true) { exception -> - if (exception != null && exception !is CancellationException) { - logcat(LogPriority.ERROR, exception) { "DownloadCache: failed to create cache" } } - lastRenew = System.currentTimeMillis() - notifyChanges() - } - } // Mainly to notify the indexing notifier UI notifyChanges() } - private fun getSources(): List { - return sourceManager.getOnlineSources() + sourceManager.getStubSources() - } + private fun getSources(): List = sourceManager.getOnlineSources() + sourceManager.getStubSources() private fun notifyChanges() { scope.launchNonCancellable { @@ -392,23 +412,25 @@ class DownloadCache( } private var updateDiskCacheJob: Job? = null + private fun updateDiskCache() { updateDiskCacheJob?.cancel() - updateDiskCacheJob = scope.launchIO { - delay(1000) - ensureActive() - val bytes = ProtoBuf.encodeToByteArray(rootDownloadsDir) - ensureActive() - try { - diskCacheFile.writeBytes(bytes) - } catch (e: Throwable) { - logcat( - priority = LogPriority.ERROR, - throwable = e, - message = { "Failed to write disk cache file" }, - ) + updateDiskCacheJob = + scope.launchIO { + delay(1000) + ensureActive() + val bytes = ProtoBuf.encodeToByteArray(rootDownloadsDir) + ensureActive() + try { + diskCacheFile.writeBytes(bytes) + } catch (e: Throwable) { + logcat( + priority = LogPriority.ERROR, + throwable = e, + message = { "Failed to write disk cache file" }, + ) + } } - } } } @@ -445,19 +467,19 @@ private class MangaDirectory( private object UniFileAsStringSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: UniFile?) { - return if (value == null) { - encoder.encodeNull() - } else { - encoder.encodeString(value.uri.toString()) - } + override fun serialize( + encoder: Encoder, + value: UniFile?, + ) = if (value == null) { + encoder.encodeNull() + } else { + encoder.encodeString(value.uri.toString()) } - override fun deserialize(decoder: Decoder): UniFile? { - return if (decoder.decodeNotNullMark()) { + override fun deserialize(decoder: Decoder): UniFile? = + if (decoder.decodeNotNullMark()) { UniFile.fromUri(Injekt.get(), Uri.parse(decoder.decodeString())) } else { decoder.decodeNull() } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt index 6a7b4469ee..f6c8b491f0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt @@ -32,16 +32,20 @@ import uy.kohesive.injekt.api.get * This worker is used to manage the downloader. The system can decide to stop the worker, in * which case the downloader is also stopped. It's also stopped while there's no network available. */ -class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { - +class DownloadJob( + context: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(context, workerParams) { private val downloadManager: DownloadManager = Injekt.get() private val downloadPreferences: DownloadPreferences = Injekt.get() override suspend fun getForegroundInfo(): ForegroundInfo { - val notification = applicationContext.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) { - setContentTitle(applicationContext.getString(R.string.download_notifier_downloader_title)) - setSmallIcon(android.R.drawable.stat_sys_download) - }.build() + val notification = + applicationContext + .notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) { + setContentTitle(applicationContext.getString(R.string.download_notifier_downloader_title)) + setSmallIcon(android.R.drawable.stat_sys_download) + }.build() return ForegroundInfo( Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, notification, @@ -54,10 +58,11 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW } override suspend fun doWork(): Result { - var networkCheck = checkNetworkState( - applicationContext.activeNetworkState(), - downloadPreferences.downloadOnlyOverWifi().get(), - ) + var networkCheck = + checkNetworkState( + applicationContext.activeNetworkState(), + downloadPreferences.downloadOnlyOverWifi().get(), + ) var active = networkCheck && downloadManager.downloaderStart() if (!active) { @@ -71,8 +76,7 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW applicationContext.networkStateFlow(), downloadPreferences.downloadOnlyOverWifi().changes(), transform = { a, b -> emit(checkNetworkState(a, b)) }, - ) - .onEach { networkCheck = it } + ).onEach { networkCheck = it } .launchIn(this) } @@ -84,8 +88,11 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW return Result.success() } - private fun checkNetworkState(state: NetworkState, requireWifi: Boolean): Boolean { - return if (state.isOnline) { + private fun checkNetworkState( + state: NetworkState, + requireWifi: Boolean, + ): Boolean = + if (state.isOnline) { val noWifi = requireWifi && !state.isWifi if (noWifi) { downloadManager.downloaderStop( @@ -97,36 +104,38 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW downloadManager.downloaderStop(applicationContext.getString(R.string.download_notifier_no_network)) false } - } companion object { private const val TAG = "Downloader" fun start(context: Context) { - val request = OneTimeWorkRequestBuilder() - .addTag(TAG) - .build() - WorkManager.getInstance(context) + val request = + OneTimeWorkRequestBuilder() + .addTag(TAG) + .build() + WorkManager + .getInstance(context) .enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request) } fun stop(context: Context) { - WorkManager.getInstance(context) + WorkManager + .getInstance(context) .cancelUniqueWork(TAG) } - fun isRunning(context: Context): Boolean { - return WorkManager.getInstance(context) + fun isRunning(context: Context): Boolean = + WorkManager + .getInstance(context) .getWorkInfosForUniqueWork(TAG) .get() .let { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 } - } - fun isRunningFlow(context: Context): Flow { - return WorkManager.getInstance(context) + fun isRunningFlow(context: Context): Flow = + WorkManager + .getInstance(context) .getWorkInfosForUniqueWorkLiveData(TAG) .asFlow() .map { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 } - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 01a342859d..fa42d8af60 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -40,7 +40,6 @@ class DownloadManager( private val sourceManager: SourceManager = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(), ) { - /** * Downloader whose only task is to download chapters. */ @@ -59,6 +58,7 @@ class DownloadManager( // For use by DownloadService only fun downloaderStart() = downloader.start() + fun downloaderStop(reason: String? = null) = downloader.stop(reason) val isDownloaderRunning @@ -99,9 +99,7 @@ class DownloadManager( * * @param chapterId the chapter to check. */ - fun getQueuedDownloadOrNull(chapterId: Long): Download? { - return queueState.value.find { it.chapter.id == chapterId } - } + fun getQueuedDownloadOrNull(chapterId: Long): Download? = queueState.value.find { it.chapter.id == chapterId } fun startDownloadNow(chapterId: Long) { val existingDownload = getQueuedDownloadOrNull(chapterId) @@ -131,7 +129,11 @@ class DownloadManager( * @param chapters the list of chapters to enqueue. * @param autoStart whether to start the downloader after enqueing the chapters. */ - fun downloadChapters(manga: Manga, chapters: List, autoStart: Boolean = true) { + fun downloadChapters( + manga: Manga, + chapters: List, + autoStart: Boolean = true, + ) { downloader.queueChapters(manga, chapters, autoStart) } @@ -157,16 +159,24 @@ class DownloadManager( * @param chapter the downloaded chapter. * @return the list of pages from the chapter. */ - fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List { + fun buildPageList( + source: Source, + manga: Manga, + chapter: Chapter, + ): List { val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, manga.title, source) - val files = chapterDir?.listFiles().orEmpty() - .filter { "image" in it.type.orEmpty() } + val files = + chapterDir + ?.listFiles() + .orEmpty() + .filter { "image" in it.type.orEmpty() } if (files.isEmpty()) { throw Exception(context.stringResource(MR.strings.page_list_empty_error)) } - return files.sortedBy { it.name } + return files + .sortedBy { it.name } .mapIndexed { i, file -> Page(i, uri = file.uri).apply { status = Page.State.READY } } @@ -187,25 +197,19 @@ class DownloadManager( mangaTitle: String, sourceId: Long, skipCache: Boolean = false, - ): Boolean { - return cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache) - } + ): Boolean = cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache) /** * Returns the amount of downloaded chapters. */ - fun getDownloadCount(): Int { - return cache.getTotalDownloadCount() - } + fun getDownloadCount(): Int = cache.getTotalDownloadCount() /** * Returns the amount of downloaded chapters for a manga. * * @param manga the manga to check. */ - fun getDownloadCount(manga: Manga): Int { - return cache.getDownloadCount(manga) - } + fun getDownloadCount(manga: Manga): Int = cache.getDownloadCount(manga) fun cancelQueuedDownloads(downloads: List) { removeFromDownloadQueue(downloads.map { it.chapter }) @@ -218,7 +222,11 @@ class DownloadManager( * @param manga the manga of the chapters. * @param source the source of the chapters. */ - fun deleteChapters(chapters: List, manga: Manga, source: Source) { + fun deleteChapters( + chapters: List, + manga: Manga, + source: Source, + ) { launchIO { val filteredChapters = getChaptersToDelete(chapters, manga) if (filteredChapters.isEmpty()) { @@ -245,7 +253,11 @@ class DownloadManager( * @param source the source of the manga. * @param removeQueued whether to also remove queued downloads. */ - fun deleteManga(manga: Manga, source: Source, removeQueued: Boolean = true) { + fun deleteManga( + manga: Manga, + source: Source, + removeQueued: Boolean = true, + ) { launchIO { if (removeQueued) { downloader.removeFromQueue(manga) @@ -285,7 +297,10 @@ class DownloadManager( * @param chapters the list of chapters to delete. * @param manga the manga of the chapters. */ - suspend fun enqueueChaptersToDelete(chapters: List, manga: Manga) { + suspend fun enqueueChaptersToDelete( + chapters: List, + manga: Manga, + ) { pendingDeleter.addChapters(getChaptersToDelete(chapters, manga), manga) } @@ -306,7 +321,10 @@ class DownloadManager( * @param oldSource the old source. * @param newSource the new source. */ - fun renameSource(oldSource: Source, newSource: Source) { + fun renameSource( + oldSource: Source, + newSource: Source, + ) { val oldFolder = provider.findSourceDir(oldSource) ?: return val newName = provider.getSourceDirName(newSource) @@ -334,14 +352,21 @@ class DownloadManager( * @param oldChapter the existing chapter with the old name. * @param newChapter the target chapter with the new name. */ - suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) { + suspend fun renameChapter( + source: Source, + manga: Manga, + oldChapter: Chapter, + newChapter: Chapter, + ) { val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator) val mangaDir = provider.getMangaDir(manga.title, source) // Assume there's only 1 version of the chapter name formats present - val oldDownload = oldNames.asSequence() - .mapNotNull { mangaDir.findFile(it) } - .firstOrNull() ?: return + val oldDownload = + oldNames + .asSequence() + .mapNotNull { mangaDir.findFile(it) } + .firstOrNull() ?: return var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator) if (oldDownload.isFile && oldDownload.extension == "cbz") { @@ -358,18 +383,24 @@ class DownloadManager( } } - private suspend fun getChaptersToDelete(chapters: List, manga: Manga): List { + private suspend fun getChaptersToDelete( + chapters: List, + manga: Manga, + ): List { // Retrieve the categories that are set to exclude from being deleted on read val categoriesToExclude = downloadPreferences.removeExcludeCategories().get().map(String::toLong) - val categoriesForManga = getCategories.await(manga.id) - .map { it.id } - .ifEmpty { listOf(0) } - val filteredCategoryManga = if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) { - chapters.filterNot { it.read } - } else { - chapters - } + val categoriesForManga = + getCategories + .await(manga.id) + .map { it.id } + .ifEmpty { listOf(0) } + val filteredCategoryManga = + if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) { + chapters.filterNot { it.read } + } else { + chapters + } return if (!downloadPreferences.removeBookmarkedChapters().get()) { filteredCategoryManga.filterNot { it.bookmark } @@ -378,32 +409,31 @@ class DownloadManager( } } - fun statusFlow(): Flow = queueState - .flatMapLatest { downloads -> - downloads - .map { download -> - download.statusFlow.drop(1).map { download } - } - .merge() - } - .onStart { - emitAll( - queueState.value.filter { download -> download.status == Download.State.DOWNLOADING }.asFlow(), - ) - } + fun statusFlow(): Flow = + queueState + .flatMapLatest { downloads -> + downloads + .map { download -> + download.statusFlow.drop(1).map { download } + }.merge() + }.onStart { + emitAll( + queueState.value.filter { download -> download.status == Download.State.DOWNLOADING }.asFlow(), + ) + } - fun progressFlow(): Flow = queueState - .flatMapLatest { downloads -> - downloads - .map { download -> - download.progressFlow.drop(1).map { download } - } - .merge() - } - .onStart { - emitAll( - queueState.value.filter { download -> download.status == Download.State.DOWNLOADING } - .asFlow(), - ) - } + fun progressFlow(): Flow = + queueState + .flatMapLatest { downloads -> + downloads + .map { download -> + download.progressFlow.drop(1).map { download } + }.merge() + }.onStart { + emitAll( + queueState.value + .filter { download -> download.status == Download.State.DOWNLOADING } + .asFlow(), + ) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt index 4acd8322e9..fc36409973 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt @@ -24,8 +24,9 @@ import java.util.regex.Pattern * * @param context context of application */ -internal class DownloadNotifier(private val context: Context) { - +internal class DownloadNotifier( + private val context: Context, +) { private val preferences: SecurityPreferences by injectLazy() private val progressNotificationBuilder by lazy { @@ -85,11 +86,12 @@ internal class DownloadNotifier(private val context: Context) { ) } - val downloadingProgressText = context.stringResource( - MR.strings.chapter_downloading_progress, - download.downloadedImages, - download.pages!!.size, - ) + val downloadingProgressText = + context.stringResource( + MR.strings.chapter_downloading_progress, + download.downloadedImages, + download.pages!!.size, + ) if (preferences.hideNotificationContent().get()) { setContentTitle(downloadingProgressText) @@ -97,10 +99,11 @@ internal class DownloadNotifier(private val context: Context) { } else { val title = download.manga.title.chop(15) val quotedTitle = Pattern.quote(title) - val chapter = download.chapter.name.replaceFirst( - "$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), - "", - ) + val chapter = + download.chapter.name.replaceFirst( + "$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), + "", + ) setContentTitle("$title - $chapter".chop(30)) setContentText(downloadingProgressText) } @@ -162,7 +165,11 @@ internal class DownloadNotifier(private val context: Context) { * @param timeout duration after which to automatically dismiss the notification. * Only works on Android 8+. */ - fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) { + fun onWarning( + reason: String, + timeout: Long? = null, + contentIntent: PendingIntent? = null, + ) { with(errorNotificationBuilder) { setContentTitle(context.stringResource(MR.strings.download_notifier_downloader_title)) setStyle(NotificationCompat.BigTextStyle().bigText(reason)) @@ -188,7 +195,11 @@ internal class DownloadNotifier(private val context: Context) { * @param error string containing error information. * @param chapter string containing chapter title. */ - fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null) { + fun onError( + error: String? = null, + chapter: String? = null, + mangaTitle: String? = null, + ) { // Create notification with(errorNotificationBuilder) { setContentTitle( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadPendingDeleter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadPendingDeleter.kt index 1bee154b9c..32eb15efad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadPendingDeleter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadPendingDeleter.kt @@ -19,7 +19,6 @@ class DownloadPendingDeleter( context: Context, private val json: Json = Injekt.get(), ) { - /** * Preferences used to store the list of chapters to delete. */ @@ -37,36 +36,40 @@ class DownloadPendingDeleter( * @param manga the manga of the chapters. */ @Synchronized - fun addChapters(chapters: List, manga: Manga) { + fun addChapters( + chapters: List, + manga: Manga, + ) { val lastEntry = lastAddedEntry - val newEntry = if (lastEntry != null && lastEntry.manga.id == manga.id) { - // Append new chapters - val newChapters = lastEntry.chapters.addUniqueById(chapters) - - // If no chapters were added, do nothing - if (newChapters.size == lastEntry.chapters.size) return - - // Last entry matches the manga, reuse it to avoid decoding json from preferences - lastEntry.copy(chapters = newChapters) - } else { - val existingEntry = preferences.getString(manga.id.toString(), null) - if (existingEntry != null) { - // Existing entry found on preferences, decode json and add the new chapter - val savedEntry = json.decodeFromString(existingEntry) - + val newEntry = + if (lastEntry != null && lastEntry.manga.id == manga.id) { // Append new chapters - val newChapters = savedEntry.chapters.addUniqueById(chapters) + val newChapters = lastEntry.chapters.addUniqueById(chapters) // If no chapters were added, do nothing - if (newChapters.size == savedEntry.chapters.size) return + if (newChapters.size == lastEntry.chapters.size) return - savedEntry.copy(chapters = newChapters) + // Last entry matches the manga, reuse it to avoid decoding json from preferences + lastEntry.copy(chapters = newChapters) } else { - // No entry has been found yet, create a new one - Entry(chapters.map { it.toEntry() }, manga.toEntry()) + val existingEntry = preferences.getString(manga.id.toString(), null) + if (existingEntry != null) { + // Existing entry found on preferences, decode json and add the new chapter + val savedEntry = json.decodeFromString(existingEntry) + + // Append new chapters + val newChapters = savedEntry.chapters.addUniqueById(chapters) + + // If no chapters were added, do nothing + if (newChapters.size == savedEntry.chapters.size) return + + savedEntry.copy(chapters = newChapters) + } else { + // No entry has been found yet, create a new one + Entry(chapters.map { it.toEntry() }, manga.toEntry()) + } } - } // Save current state val json = json.encodeToString(newEntry) @@ -98,15 +101,14 @@ class DownloadPendingDeleter( /** * Decodes all the chapters from preferences. */ - private fun decodeAll(): List { - return preferences.all.values.mapNotNull { rawEntry -> + private fun decodeAll(): List = + preferences.all.values.mapNotNull { rawEntry -> try { (rawEntry as? String)?.let { json.decodeFromString(it) } } catch (e: Exception) { null } } - } /** * Returns a copy of chapter entries ensuring no duplicates by chapter id. @@ -134,22 +136,24 @@ class DownloadPendingDeleter( /** * Returns a manga model from a manga entry. */ - private fun MangaEntry.toModel() = Manga.create().copy( - url = url, - title = title, - source = source, - id = id, - ) + private fun MangaEntry.toModel() = + Manga.create().copy( + url = url, + title = title, + source = source, + id = id, + ) /** * Returns a chapter model from a chapter entry. */ - private fun ChapterEntry.toModel() = Chapter.create().copy( - id = id, - url = url, - name = name, - scanlator = scanlator, - ) + private fun ChapterEntry.toModel() = + Chapter.create().copy( + id = id, + url = url, + name = name, + scanlator = scanlator, + ) /** * Class used to save an entry of chapters with their manga into preferences. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index 2ab5e55d9b..31ae0744ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -25,7 +25,6 @@ class DownloadProvider( private val context: Context, private val storageManager: StorageManager = Injekt.get(), ) { - private val downloadsDir: UniFile? get() = storageManager.getDownloadsDirectory() @@ -35,7 +34,10 @@ class DownloadProvider( * @param mangaTitle the title of the manga to query. * @param source the source of the manga. */ - internal fun getMangaDir(mangaTitle: String, source: Source): UniFile { + internal fun getMangaDir( + mangaTitle: String, + source: Source, + ): UniFile { try { return downloadsDir!! .createDirectory(getSourceDirName(source))!! @@ -56,9 +58,7 @@ class DownloadProvider( * * @param source the source to query. */ - fun findSourceDir(source: Source): UniFile? { - return downloadsDir?.findFile(getSourceDirName(source)) - } + fun findSourceDir(source: Source): UniFile? = downloadsDir?.findFile(getSourceDirName(source)) /** * Returns the download directory for a manga if it exists. @@ -66,7 +66,10 @@ class DownloadProvider( * @param mangaTitle the title of the manga to query. * @param source the source of the manga. */ - fun findMangaDir(mangaTitle: String, source: Source): UniFile? { + fun findMangaDir( + mangaTitle: String, + source: Source, + ): UniFile? { val sourceDir = findSourceDir(source) return sourceDir?.findFile(getMangaDirName(mangaTitle)) } @@ -79,9 +82,15 @@ class DownloadProvider( * @param mangaTitle the title of the manga to query. * @param source the source of the chapter. */ - fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? { + fun findChapterDir( + chapterName: String, + chapterScanlator: String?, + mangaTitle: String, + source: Source, + ): UniFile? { val mangaDir = findMangaDir(mangaTitle, source) - return getValidChapterDirNames(chapterName, chapterScanlator).asSequence() + return getValidChapterDirNames(chapterName, chapterScanlator) + .asSequence() .mapNotNull { mangaDir?.findFile(it) } .firstOrNull() } @@ -93,13 +102,19 @@ class DownloadProvider( * @param manga the manga of the chapter. * @param source the source of the chapter. */ - fun findChapterDirs(chapters: List, manga: Manga, source: Source): Pair> { + fun findChapterDirs( + chapters: List, + manga: Manga, + source: Source, + ): Pair> { val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList() - return mangaDir to chapters.mapNotNull { chapter -> - getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence() - .mapNotNull { mangaDir.findFile(it) } - .firstOrNull() - } + return mangaDir to + chapters.mapNotNull { chapter -> + getValidChapterDirNames(chapter.name, chapter.scanlator) + .asSequence() + .mapNotNull { mangaDir.findFile(it) } + .firstOrNull() + } } /** @@ -107,18 +122,14 @@ class DownloadProvider( * * @param source the source to query. */ - fun getSourceDirName(source: Source): String { - return DiskUtil.buildValidFilename(source.toString()) - } + fun getSourceDirName(source: Source): String = DiskUtil.buildValidFilename(source.toString()) /** * Returns the download directory name for a manga. * * @param mangaTitle the title of the manga to query. */ - fun getMangaDirName(mangaTitle: String): String { - return DiskUtil.buildValidFilename(mangaTitle) - } + fun getMangaDirName(mangaTitle: String): String = DiskUtil.buildValidFilename(mangaTitle) /** * Returns the chapter directory name for a chapter. @@ -126,7 +137,10 @@ class DownloadProvider( * @param chapterName the name of the chapter to query. * @param chapterScanlator scanlator of the chapter to query */ - fun getChapterDirName(chapterName: String, chapterScanlator: String?): String { + fun getChapterDirName( + chapterName: String, + chapterScanlator: String?, + ): String { val newChapterName = sanitizeChapterName(chapterName) return DiskUtil.buildValidFilename( when { @@ -141,16 +155,17 @@ class DownloadProvider( * * @param chapterName the name of the chapter */ - private fun sanitizeChapterName(chapterName: String): String { - return chapterName.ifBlank { + private fun sanitizeChapterName(chapterName: String): String = + chapterName.ifBlank { "Chapter" } - } - fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean { - return oldChapter.name != newChapter.name || + fun isChapterDirNameChanged( + oldChapter: Chapter, + newChapter: Chapter, + ): Boolean = + oldChapter.name != newChapter.name || oldChapter.scanlator?.takeIf { it.isNotBlank() } != newChapter.scanlator?.takeIf { it.isNotBlank() } - } /** * Returns valid downloaded chapter directory names. @@ -158,7 +173,10 @@ class DownloadProvider( * @param chapterName the name of the chapter to query. * @param chapterScanlator scanlator of the chapter to query */ - fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?): List { + fun getValidChapterDirNames( + chapterName: String, + chapterScanlator: String?, + ): List { val chapterDirName = getChapterDirName(chapterName, chapterScanlator) return buildList(2) { // Folder of images diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt index 3d95909bf6..9a4c93addb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt @@ -25,7 +25,6 @@ class DownloadStore( private val getManga: GetManga = Injekt.get(), private val getChapter: GetChapter = Injekt.get(), ) { - /** * Preference file where active downloads are stored. */ @@ -83,26 +82,26 @@ class DownloadStore( * * @param download the download. */ - private fun getKey(download: Download): String { - return download.chapter.id.toString() - } + private fun getKey(download: Download): String = download.chapter.id.toString() /** * Returns the list of downloads to restore. It should be called in a background thread. */ fun restore(): List { - val objs = preferences.all - .mapNotNull { it.value as? String } - .mapNotNull { deserialize(it) } - .sortedBy { it.order } + val objs = + preferences.all + .mapNotNull { it.value as? String } + .mapNotNull { deserialize(it) } + .sortedBy { it.order } val downloads = mutableListOf() if (objs.isNotEmpty()) { val cachedManga = mutableMapOf() for ((mangaId, chapterId) in objs) { - val manga = cachedManga.getOrPut(mangaId) { - runBlocking { getManga.await(mangaId) } - } ?: continue + val manga = + cachedManga.getOrPut(mangaId) { + runBlocking { getManga.await(mangaId) } + } ?: continue val source = sourceManager.get(manga.source) as? HttpSource ?: continue val chapter = runBlocking { getChapter.await(chapterId) } ?: continue downloads.add(Download(source, manga, chapter)) @@ -129,13 +128,12 @@ class DownloadStore( * * @param string the download as string. */ - private fun deserialize(string: String): DownloadObject? { - return try { + private fun deserialize(string: String): DownloadObject? = + try { json.decodeFromString(string) } catch (e: Exception) { null } - } } /** @@ -146,4 +144,8 @@ class DownloadStore( * @param order the order of the download in the queue. */ @Serializable -private data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int) +private data class DownloadObject( + val mangaId: Long, + val chapterId: Long, + val order: Int, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index ebf9df65ef..973ed5177b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -78,7 +78,6 @@ class Downloader( private val getCategories: GetCategories = Injekt.get(), private val getTracks: GetTracks = Injekt.get(), ) { - /** * Store for persisting downloads across restarts. */ @@ -190,64 +189,71 @@ class Downloader( private fun launchDownloaderJob() { if (isRunning) return - downloaderJob = scope.launch { - val activeDownloadsFlow = queueState.transformLatest { queue -> - while (true) { - val activeDownloads = queue.asSequence() - .filter { it.status.value <= Download.State.DOWNLOADING.value } // Ignore completed downloads, leave them in the queue - .groupBy { it.source } - .toList().take(5) // Concurrently download from 5 different sources - .map { (_, downloads) -> downloads.first() } - emit(activeDownloads) - - if (activeDownloads.isEmpty()) break - // Suspend until a download enters the ERROR state - val activeDownloadsErroredFlow = - combine(activeDownloads.map(Download::statusFlow)) { states -> - states.contains(Download.State.ERROR) - }.filter { it } - activeDownloadsErroredFlow.first() - } - }.distinctUntilChanged() + downloaderJob = + scope.launch { + val activeDownloadsFlow = + queueState + .transformLatest { queue -> + while (true) { + val activeDownloads = + queue + .asSequence() + .filter { it.status.value <= Download.State.DOWNLOADING.value } // Ignore completed downloads, leave them in the queue + .groupBy { it.source } + .toList() + .take(5) // Concurrently download from 5 different sources + .map { (_, downloads) -> downloads.first() } + emit(activeDownloads) + + if (activeDownloads.isEmpty()) break + // Suspend until a download enters the ERROR state + val activeDownloadsErroredFlow = + combine(activeDownloads.map(Download::statusFlow)) { states -> + states.contains(Download.State.ERROR) + }.filter { it } + activeDownloadsErroredFlow.first() + } + }.distinctUntilChanged() - // Use supervisorScope to cancel child jobs when the downloader job is cancelled - supervisorScope { - val downloadJobs = mutableMapOf() + // Use supervisorScope to cancel child jobs when the downloader job is cancelled + supervisorScope { + val downloadJobs = mutableMapOf() - activeDownloadsFlow.collectLatest { activeDownloads -> - val downloadJobsToStop = downloadJobs.filter { it.key !in activeDownloads } - downloadJobsToStop.forEach { (download, job) -> - job.cancel() - downloadJobs.remove(download) - } + activeDownloadsFlow.collectLatest { activeDownloads -> + val downloadJobsToStop = downloadJobs.filter { it.key !in activeDownloads } + downloadJobsToStop.forEach { (download, job) -> + job.cancel() + downloadJobs.remove(download) + } - val downloadsToStart = activeDownloads.filter { it !in downloadJobs } - downloadsToStart.forEach { download -> - downloadJobs[download] = launchDownloadJob(download) + val downloadsToStart = activeDownloads.filter { it !in downloadJobs } + downloadsToStart.forEach { download -> + downloadJobs[download] = launchDownloadJob(download) + } } } } - } } - private fun CoroutineScope.launchDownloadJob(download: Download) = launchIO { - try { - downloadChapter(download) + private fun CoroutineScope.launchDownloadJob(download: Download) = + launchIO { + try { + downloadChapter(download) - // Remove successful download from queue - if (download.status == Download.State.DOWNLOADED) { - removeFromQueue(download) - } - if (areAllDownloadsFinished()) { + // Remove successful download from queue + if (download.status == Download.State.DOWNLOADED) { + removeFromQueue(download) + } + if (areAllDownloadsFinished()) { + stop() + } + } catch (e: Throwable) { + if (e is CancellationException) throw e + logcat(LogPriority.ERROR, e) + notifier.onError(e.message) stop() } - } catch (e: Throwable) { - if (e is CancellationException) throw e - logcat(LogPriority.ERROR, e) - notifier.onError(e.message) - stop() } - } /** * Destroys the downloader subscriptions. @@ -264,21 +270,27 @@ class Downloader( * @param chapters the list of chapters to download. * @param autoStart whether to start the downloader after enqueing the chapters. */ - fun queueChapters(manga: Manga, chapters: List, autoStart: Boolean) { + fun queueChapters( + manga: Manga, + chapters: List, + autoStart: Boolean, + ) { if (chapters.isEmpty()) return val source = sourceManager.get(manga.source) as? HttpSource ?: return val wasEmpty = queueState.value.isEmpty() - val chaptersToQueue = chapters.asSequence() - // Filter out those already downloaded. - .filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null } - // Add chapters to queue from the start. - .sortedByDescending { it.sourceOrder } - // Filter out those already enqueued. - .filter { chapter -> queueState.value.none { it.chapter.id == chapter.id } } - // Create a download for each one. - .map { Download(source, manga, it) } - .toList() + val chaptersToQueue = + chapters + .asSequence() + // Filter out those already downloaded. + .filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null } + // Add chapters to queue from the start. + .sortedByDescending { it.sourceOrder } + // Filter out those already enqueued. + .filter { chapter -> queueState.value.none { it.chapter.id == chapter.id } } + // Create a download for each one. + .map { Download(source, manga, it) } + .toList() if (chaptersToQueue.isNotEmpty()) { addAllToQueue(chaptersToQueue) @@ -286,11 +298,12 @@ class Downloader( // Start downloader if needed if (autoStart && wasEmpty) { val queuedDownloads = queueState.value.count { it.source !is UnmeteredSource } - val maxDownloadsFromSource = queueState.value - .groupBy { it.source } - .filterKeys { it !is UnmeteredSource } - .maxOfOrNull { it.value.size } - ?: 0 + val maxDownloadsFromSource = + queueState.value + .groupBy { it.source } + .filterKeys { it !is UnmeteredSource } + .maxOfOrNull { it.value.size } + ?: 0 if ( queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD || maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD @@ -330,21 +343,29 @@ class Downloader( try { // If the page list already exists, start from the file - val pageList = download.pages ?: run { - // Otherwise, pull page list from network and add them to download object - val pages = download.source.getPageList(download.chapter.toSChapter()) + val pageList = + download.pages ?: run { + // Otherwise, pull page list from network and add them to download object + val pages = download.source.getPageList(download.chapter.toSChapter()) - if (pages.isEmpty()) { - throw Exception(context.stringResource(MR.strings.page_list_empty_error)) + if (pages.isEmpty()) { + throw Exception(context.stringResource(MR.strings.page_list_empty_error)) + } + // Don't trust index from source + val reIndexedPages = + pages.mapIndexed { + index, + page, + -> + Page(index, page.url, page.imageUrl, page.uri) + } + download.pages = reIndexedPages + reIndexedPages } - // Don't trust index from source - val reIndexedPages = pages.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) } - download.pages = reIndexedPages - reIndexedPages - } // Delete all temporary (unfinished) files - tmpDir.listFiles() + tmpDir + .listFiles() ?.filter { it.extension == "tmp" } ?.forEach { it.delete() } @@ -352,7 +373,8 @@ class Downloader( // Start downloading images, consider we can have downloaded images already // Concurrently do 2 pages at a time - pageList.asFlow() + pageList + .asFlow() .flatMapMerge(concurrency = 2) { page -> flow { // Fetch image URL if necessary @@ -368,8 +390,7 @@ class Downloader( withIOContext { getOrDownloadImage(page, download, tmpDir) } emit(page) }.flowOn(Dispatchers.IO) - } - .collect { + }.collect { // Do when page is downloaded. notifier.onProgressChange(download) } @@ -415,7 +436,11 @@ class Downloader( * @param download the download of the page. * @param tmpDir the temporary directory of the download. */ - private suspend fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile) { + private suspend fun getOrDownloadImage( + page: Page, + download: Download, + tmpDir: UniFile, + ) { // If the image URL is empty, do nothing if (page.imageUrl == null) { return @@ -429,19 +454,21 @@ class Downloader( tmpFile?.delete() // Try to find the image file - val imageFile = tmpDir.listFiles()?.firstOrNull { - it.name!!.startsWith("$filename.") || it.name!!.startsWith("${filename}__001") - } + val imageFile = + tmpDir.listFiles()?.firstOrNull { + it.name!!.startsWith("$filename.") || it.name!!.startsWith("${filename}__001") + } try { // If the image is already downloaded, do nothing. Otherwise download from network - val file = when { - imageFile != null -> imageFile - chapterCache.isImageInCache( - page.imageUrl!!, - ) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename) - else -> downloadImage(page, download.source, tmpDir, filename) - } + val file = + when { + imageFile != null -> imageFile + chapterCache.isImageInCache( + page.imageUrl!!, + ) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename) + else -> downloadImage(page, download.source, tmpDir, filename) + } // When the page is ready, set page path, progress (just in case) and status splitTallImageIfNeeded(page, tmpDir) @@ -466,7 +493,12 @@ class Downloader( * @param tmpDir the temporary directory of the download. * @param filename the filename of the image. */ - private suspend fun downloadImage(page: Page, source: HttpSource, tmpDir: UniFile, filename: String): UniFile { + private suspend fun downloadImage( + page: Page, + source: HttpSource, + tmpDir: UniFile, + filename: String, + ): UniFile { page.status = Page.State.DOWNLOAD_IMAGE page.progress = 0 return flow { @@ -491,8 +523,7 @@ class Downloader( } else { false } - } - .first() + }.first() } /** @@ -502,7 +533,11 @@ class Downloader( * @param tmpDir the temporary directory of the download. * @param filename the filename of the image. */ - private fun copyImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): UniFile { + private fun copyImageFromCache( + cacheFile: File, + tmpDir: UniFile, + filename: String, + ): UniFile { val tmpFile = tmpDir.createFile("$filename.tmp")!! cacheFile.inputStream().use { input -> tmpFile.openOutputStream().use { output -> @@ -522,24 +557,32 @@ class Downloader( * @param response the network response of the image. * @param file the file where the image is already downloaded. */ - private fun getImageExtension(response: Response, file: UniFile): String { + private fun getImageExtension( + response: Response, + file: UniFile, + ): String { // Read content type if available. - val mime = response.body.contentType()?.run { if (type == "image") "image/$subtype" else null } - // Else guess from the uri. - ?: context.contentResolver.getType(file.uri) - // Else read magic numbers. - ?: ImageUtil.findImageType { file.openInputStream() }?.mime + val mime = + response.body.contentType()?.run { if (type == "image") "image/$subtype" else null } + // Else guess from the uri. + ?: context.contentResolver.getType(file.uri) + // Else read magic numbers. + ?: ImageUtil.findImageType { file.openInputStream() }?.mime return ImageUtil.getExtensionFromMimeType(mime) } - private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile) { + private fun splitTallImageIfNeeded( + page: Page, + tmpDir: UniFile, + ) { if (!downloadPreferences.splitTallImages().get()) return try { val filenamePrefix = "%03d".format(Locale.ENGLISH, page.number) - val imageFile = tmpDir.listFiles()?.firstOrNull { it.name.orEmpty().startsWith(filenamePrefix) } - ?: error(context.stringResource(MR.strings.download_notifier_split_page_not_found, page.number)) + val imageFile = + tmpDir.listFiles()?.firstOrNull { it.name.orEmpty().startsWith(filenamePrefix) } + ?: error(context.stringResource(MR.strings.download_notifier_split_page_not_found, page.number)) // If the original page was previously split, then skip if (imageFile.name.orEmpty().startsWith("${filenamePrefix}__")) return @@ -569,16 +612,17 @@ class Downloader( } // Ensure that the chapter folder has all the pages - val downloadedImagesCount = tmpDir.listFiles().orEmpty().count { - val fileName = it.name.orEmpty() - when { - fileName in listOf(COMIC_INFO_FILE, NOMEDIA_FILE) -> false - fileName.endsWith(".tmp") -> false - // Only count the first split page and not the others - fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false - else -> true + val downloadedImagesCount = + tmpDir.listFiles().orEmpty().count { + val fileName = it.name.orEmpty() + when { + fileName in listOf(COMIC_INFO_FILE, NOMEDIA_FILE) -> false + fileName.endsWith(".tmp") -> false + // Only count the first split page and not the others + fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false + else -> true + } } - } return downloadedImagesCount == downloadPageCount } @@ -610,20 +654,22 @@ class Downloader( source: HttpSource, ) { val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() } - val urls = getTracks.await(manga.id) - .mapNotNull { track -> - track.remoteUrl.takeUnless { url -> url.isBlank() }?.trim() - } - .plus(source.getChapterUrl(chapter.toSChapter()).trim()) - .distinct() - - val comicInfo = getComicInfo( - manga, - chapter, - urls, - categories, - source.name - ) + val urls = + getTracks + .await(manga.id) + .mapNotNull { track -> + track.remoteUrl.takeUnless { url -> url.isBlank() }?.trim() + }.plus(source.getChapterUrl(chapter.toSChapter()).trim()) + .distinct() + + val comicInfo = + getComicInfo( + manga, + chapter, + urls, + categories, + source.name, + ) // Remove the old file dir.findFile(COMIC_INFO_FILE)?.delete() @@ -636,9 +682,10 @@ class Downloader( /** * Returns true if all the queued downloads are in DOWNLOADED or ERROR state. */ - private fun areAllDownloadsFinished(): Boolean { - return queueState.value.none { it.status.value <= Download.State.DOWNLOADING.value } - } + private fun areAllDownloadsFinished(): Boolean = + queueState.value.none { + it.status.value <= Download.State.DOWNLOADING.value + } private fun addAllToQueue(downloads: List) { _queueState.update { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt index 564a336336..07b746f6e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt @@ -43,19 +43,19 @@ data class Download( } @Transient - val progressFlow = flow { - if (pages == null) { - emit(0) - while (pages == null) { - delay(50) + val progressFlow = + flow { + if (pages == null) { + emit(0) + while (pages == null) { + delay(50) + } } - } - val progressFlows = pages!!.map(Page::progressFlow) - emitAll(combine(progressFlows) { it.average().toInt() }) - } - .distinctUntilChanged() - .debounce(50) + val progressFlows = pages!!.map(Page::progressFlow) + emitAll(combine(progressFlows) { it.average().toInt() }) + }.distinctUntilChanged() + .debounce(50) val progress: Int get() { @@ -63,7 +63,9 @@ data class Download( return pages.map(Page::progress).average().toInt() } - enum class State(val value: Int) { + enum class State( + val value: Int, + ) { NOT_DOWNLOADED(0), QUEUE(1), DOWNLOADING(2), 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 258b1f754c..837367ce8e 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 @@ -74,9 +74,10 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger -class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : - CoroutineWorker(context, workerParams) { - +class LibraryUpdateJob( + private val context: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(context, workerParams) { private val sourceManager: SourceManager = Injekt.get() private val downloadPreferences: DownloadPreferences = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get() @@ -153,68 +154,75 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private suspend fun addMangaToQueue(categoryId: Long) { val libraryManga = getLibraryManga.await() - val listToUpdate = if (categoryId != -1L) { - libraryManga.filter { it.category == categoryId } - } else { - val categoriesToUpdate = libraryPreferences.updateCategories().get().map { it.toLong() } - val includedManga = if (categoriesToUpdate.isNotEmpty()) { - libraryManga.filter { it.category in categoriesToUpdate } + val listToUpdate = + if (categoryId != -1L) { + libraryManga.filter { it.category == categoryId } } else { - libraryManga - } + val categoriesToUpdate = libraryPreferences.updateCategories().get().map { it.toLong() } + val includedManga = + if (categoriesToUpdate.isNotEmpty()) { + libraryManga.filter { it.category in categoriesToUpdate } + } else { + libraryManga + } - val categoriesToExclude = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() } - val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) { - libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id } - } else { - emptyList() - } + val categoriesToExclude = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() } + val excludedMangaIds = + if (categoriesToExclude.isNotEmpty()) { + libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id } + } else { + emptyList() + } - includedManga - .filterNot { it.manga.id in excludedMangaIds } - .distinctBy { it.manga.id } - } + includedManga + .filterNot { it.manga.id in excludedMangaIds } + .distinctBy { it.manga.id } + } val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get() val skippedUpdates = mutableListOf>() val (_, fetchWindowUpperBound) = fetchInterval.getWindow(ZonedDateTime.now()) - mangaToUpdate = listToUpdate - .filter { - when { - it.manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> { - skippedUpdates.add( - it.manga to context.stringResource(MR.strings.skipped_reason_not_always_update), - ) - false - } + mangaToUpdate = + listToUpdate + .filter { + when { + it.manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> { + skippedUpdates.add( + it.manga to context.stringResource(MR.strings.skipped_reason_not_always_update), + ) + false + } - MANGA_NON_COMPLETED in restrictions && it.manga.status.toInt() == SManga.COMPLETED -> { - skippedUpdates.add(it.manga to context.stringResource(MR.strings.skipped_reason_completed)) - false - } + MANGA_NON_COMPLETED in restrictions && it.manga.status.toInt() == SManga.COMPLETED -> { + skippedUpdates.add(it.manga to context.stringResource(MR.strings.skipped_reason_completed)) + false + } - MANGA_HAS_UNREAD in restrictions && it.unreadCount != 0L -> { - skippedUpdates.add(it.manga to context.stringResource(MR.strings.skipped_reason_not_caught_up)) - false - } + MANGA_HAS_UNREAD in restrictions && it.unreadCount != 0L -> { + skippedUpdates.add( + it.manga to context.stringResource(MR.strings.skipped_reason_not_caught_up), + ) + false + } - MANGA_NON_READ in restrictions && it.totalChapters > 0L && !it.hasStarted -> { - skippedUpdates.add(it.manga to context.stringResource(MR.strings.skipped_reason_not_started)) - false - } + MANGA_NON_READ in restrictions && it.totalChapters > 0L && !it.hasStarted -> { + skippedUpdates.add( + it.manga to context.stringResource(MR.strings.skipped_reason_not_started), + ) + false + } - MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> { - skippedUpdates.add( - it.manga to context.stringResource(MR.strings.skipped_reason_not_in_release_period), - ) - false - } + MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> { + skippedUpdates.add( + it.manga to context.stringResource(MR.strings.skipped_reason_not_in_release_period), + ) + false + } - else -> true - } - } - .sortedBy { it.manga.title } + else -> true + } + }.sortedBy { it.manga.title } notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate) @@ -247,7 +255,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now()) coroutineScope { - mangaToUpdate.groupBy { it.manga.source }.values + mangaToUpdate + .groupBy { it.manga.source } + .values .map { mangaInSource -> async { semaphore.withPermit { @@ -266,8 +276,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet manga, ) { try { - val newChapters = updateManga(manga, fetchWindow) - .sortedByDescending { it.sourceOrder } + val newChapters = + updateManga(manga, fetchWindow) + .sortedByDescending { it.sourceOrder } if (newChapters.isNotEmpty()) { val categoryIds = getCategories.await(manga.id).map { it.id } @@ -282,24 +293,26 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet newUpdates.add(manga to newChapters.toTypedArray()) } } catch (e: Throwable) { - val errorMessage = when (e) { - is NoChaptersException -> context.stringResource( - MR.strings.no_chapters_error, - ) - // failedUpdates will already have the source, don't need to copy it into the message - is SourceNotInstalledException -> context.stringResource( - MR.strings.loader_not_implemented_error, - ) - else -> e.message - } + val errorMessage = + when (e) { + is NoChaptersException -> + context.stringResource( + MR.strings.no_chapters_error, + ) + // failedUpdates will already have the source, don't need to copy it into the message + is SourceNotInstalledException -> + context.stringResource( + MR.strings.loader_not_implemented_error, + ) + else -> e.message + } failedUpdates.add(manga to errorMessage) } } } } } - } - .awaitAll() + }.awaitAll() } notifier.cancelProgressNotification() @@ -320,7 +333,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } } - private fun downloadChapters(manga: Manga, chapters: List) { + private fun downloadChapters( + manga: Manga, + chapters: List, + ) { // We don't want to start downloading while the library is updating, because websites // may don't like it and they could ban the user. downloadManager.downloadChapters(manga, chapters, false) @@ -332,7 +348,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet * @param manga the manga to update. * @return a pair of the inserted and removed chapters. */ - private suspend fun updateManga(manga: Manga, fetchWindow: Pair): List { + private suspend fun updateManga( + manga: Manga, + fetchWindow: Pair, + ): List { val source = sourceManager.getOrStub(manga.source) // Update manga metadata if needed @@ -404,7 +423,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } return file } - } catch (_: Exception) {} + } catch (_: Exception) { + } return File("") } @@ -434,25 +454,29 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val interval = prefInterval ?: preferences.autoUpdateInterval().get() if (interval > 0) { val restrictions = preferences.autoUpdateDeviceRestrictions().get() - val constraints = Constraints( - requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { - NetworkType.UNMETERED - } else { NetworkType.CONNECTED }, - requiresCharging = DEVICE_CHARGING in restrictions, - requiresBatteryNotLow = true, - ) - - val request = PeriodicWorkRequestBuilder( - interval.toLong(), - TimeUnit.HOURS, - 10, - TimeUnit.MINUTES, - ) - .addTag(TAG) - .addTag(WORK_NAME_AUTO) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) - .build() + val constraints = + Constraints( + requiredNetworkType = + if (DEVICE_NETWORK_NOT_METERED in restrictions) { + NetworkType.UNMETERED + } else { + NetworkType.CONNECTED + }, + requiresCharging = DEVICE_CHARGING in restrictions, + requiresBatteryNotLow = true, + ) + + val request = + PeriodicWorkRequestBuilder( + interval.toLong(), + TimeUnit.HOURS, + 10, + TimeUnit.MINUTES, + ).addTag(TAG) + .addTag(WORK_NAME_AUTO) + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) + .build() context.workManager.enqueueUniquePeriodicWork( WORK_NAME_AUTO, @@ -474,14 +498,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet return false } - val inputData = workDataOf( - KEY_CATEGORY to category?.id, - ) - val request = OneTimeWorkRequestBuilder() - .addTag(TAG) - .addTag(WORK_NAME_MANUAL) - .setInputData(inputData) - .build() + val inputData = + workDataOf( + KEY_CATEGORY to category?.id, + ) + val request = + OneTimeWorkRequestBuilder() + .addTag(TAG) + .addTag(WORK_NAME_MANUAL) + .setInputData(inputData) + .build() wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request) return true @@ -489,10 +515,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet fun stop(context: Context) { val wm = context.workManager - val workQuery = WorkQuery.Builder.fromTags(listOf(TAG)) - .addStates(listOf(WorkInfo.State.RUNNING)) - .build() - wm.getWorkInfos(workQuery).get() + val workQuery = + WorkQuery.Builder + .fromTags(listOf(TAG)) + .addStates(listOf(WorkInfo.State.RUNNING)) + .build() + wm + .getWorkInfos(workQuery) + .get() // Should only return one work but just in case .forEach { wm.cancelWorkById(it.id) 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 f7d6c5dbc0..065093dc25 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 @@ -44,15 +44,14 @@ import java.text.NumberFormat class LibraryUpdateNotifier( private val context: Context, - private val securityPreferences: SecurityPreferences = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), ) { - - private val percentFormatter = NumberFormat.getPercentInstance().apply { - roundingMode = RoundingMode.DOWN - maximumFractionDigits = 0 - } + private val percentFormatter = + NumberFormat.getPercentInstance().apply { + roundingMode = RoundingMode.DOWN + maximumFractionDigits = 0 + } /** * Pending intent of action that cancels the library update @@ -89,7 +88,11 @@ class LibraryUpdateNotifier( * @param current the current progress. * @param total the total progress. */ - fun showProgressNotification(manga: List, current: Int, total: Int) { + fun showProgressNotification( + manga: List, + current: Int, + total: Int, + ) { progressNotificationBuilder .setContentTitle( context.stringResource( @@ -115,10 +118,11 @@ class LibraryUpdateNotifier( * Warn when excessively checking any single source. */ fun showQueueSizeWarningNotificationIfNeeded(mangaToUpdate: List) { - val maxUpdatesFromSource = mangaToUpdate - .groupBy { it.manga.source } - .filterKeys { sourceManager.get(it) !is UnmeteredSource } - .maxOfOrNull { it.value.size } ?: 0 + val maxUpdatesFromSource = + mangaToUpdate + .groupBy { it.manga.source } + .filterKeys { sourceManager.get(it) !is UnmeteredSource } + .maxOfOrNull { it.value.size } ?: 0 if (maxUpdatesFromSource <= MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) { return @@ -144,7 +148,10 @@ class LibraryUpdateNotifier( * @param failed Number of entries that failed to update. * @param uri Uri for error log file containing all titles that failed. */ - fun showUpdateErrorNotification(failed: Int, uri: Uri) { + fun showUpdateErrorNotification( + failed: Int, + uri: Uri, + ) { if (failed == 0) { return } @@ -174,7 +181,12 @@ class LibraryUpdateNotifier( ) { setContentTitle(context.stringResource(MR.strings.notification_new_chapters)) if (updates.size == 1 && !securityPreferences.hideNotificationContent().get()) { - setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN)) + setContentText( + updates + .first() + .first.title + .chop(NOTIF_TITLE_MAX_LEN), + ) } else { setContentText( context.pluralStringResource( @@ -222,65 +234,69 @@ class LibraryUpdateNotifier( } } - private suspend fun createNewChaptersNotification(manga: Manga, chapters: Array): Notification { + private suspend fun createNewChaptersNotification( + manga: Manga, + chapters: Array, + ): Notification { val icon = getMangaIcon(manga) - return context.notificationBuilder(Notifications.CHANNEL_NEW_CHAPTERS) { - setContentTitle(manga.title) + return context + .notificationBuilder(Notifications.CHANNEL_NEW_CHAPTERS) { + setContentTitle(manga.title) - val description = getNewChaptersDescription(chapters) - setContentText(description) - setStyle(NotificationCompat.BigTextStyle().bigText(description)) + val description = getNewChaptersDescription(chapters) + setContentText(description) + setStyle(NotificationCompat.BigTextStyle().bigText(description)) - setSmallIcon(R.drawable.ic_mihon) + setSmallIcon(R.drawable.ic_mihon) - if (icon != null) { - setLargeIcon(icon) - } + if (icon != null) { + setLargeIcon(icon) + } - setGroup(Notifications.GROUP_NEW_CHAPTERS) - setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - priority = NotificationCompat.PRIORITY_HIGH + setGroup(Notifications.GROUP_NEW_CHAPTERS) + setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + priority = NotificationCompat.PRIORITY_HIGH - // Open first chapter on tap - setContentIntent(NotificationReceiver.openChapterPendingActivity(context, manga, chapters.first())) - setAutoCancel(true) + // Open first chapter on tap + setContentIntent(NotificationReceiver.openChapterPendingActivity(context, manga, chapters.first())) + setAutoCancel(true) - // Mark chapters as read action - addAction( - R.drawable.ic_done_24dp, - context.stringResource(MR.strings.action_mark_as_read), - NotificationReceiver.markAsReadPendingBroadcast( - context, - manga, - chapters, - Notifications.ID_NEW_CHAPTERS, - ), - ) - // View chapters action - addAction( - R.drawable.ic_book_24dp, - context.stringResource(MR.strings.action_view_chapters), - NotificationReceiver.openChapterPendingActivity( - context, - manga, - Notifications.ID_NEW_CHAPTERS, - ), - ) - // Download chapters action - // Only add the action when chapters is within threshold - if (chapters.size <= Downloader.CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) { + // Mark chapters as read action addAction( - android.R.drawable.stat_sys_download_done, - context.stringResource(MR.strings.action_download), - NotificationReceiver.downloadChaptersPendingBroadcast( + R.drawable.ic_done_24dp, + context.stringResource(MR.strings.action_mark_as_read), + NotificationReceiver.markAsReadPendingBroadcast( context, manga, chapters, Notifications.ID_NEW_CHAPTERS, ), ) - } - }.build() + // View chapters action + addAction( + R.drawable.ic_book_24dp, + context.stringResource(MR.strings.action_view_chapters), + NotificationReceiver.openChapterPendingActivity( + context, + manga, + Notifications.ID_NEW_CHAPTERS, + ), + ) + // Download chapters action + // Only add the action when chapters is within threshold + if (chapters.size <= Downloader.CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) { + addAction( + android.R.drawable.stat_sys_download_done, + context.stringResource(MR.strings.action_download), + NotificationReceiver.downloadChaptersPendingBroadcast( + context, + manga, + chapters, + Notifications.ID_NEW_CHAPTERS, + ), + ) + } + }.build() } /** @@ -291,21 +307,28 @@ class LibraryUpdateNotifier( } private suspend fun getMangaIcon(manga: Manga): Bitmap? { - val request = ImageRequest.Builder(context) - .data(manga) - .transformations(CircleCropTransformation()) - .size(NOTIF_ICON_SIZE) - .build() - val drawable = context.imageLoader.execute(request).image?.asDrawable(context.resources) + val request = + ImageRequest + .Builder(context) + .data(manga) + .transformations(CircleCropTransformation()) + .size(NOTIF_ICON_SIZE) + .build() + val drawable = + context.imageLoader + .execute(request) + .image + ?.asDrawable(context.resources) return drawable?.getBitmapOrNull() } private fun getNewChaptersDescription(chapters: Array): String { - val displayableChapterNumbers = chapters - .filter { it.isRecognizedNumber } - .sortedBy { it.chapterNumber } - .map { formatChapterNumber(it.chapterNumber) } - .toSet() + val displayableChapterNumbers = + chapters + .filter { it.isRecognizedNumber } + .sortedBy { it.chapterNumber } + .map { formatChapterNumber(it.chapterNumber) } + .toSet() return when (displayableChapterNumbers.size) { // No sensible chapter numbers to show (i.e. no chapters have parsed chapter number) @@ -341,9 +364,10 @@ class LibraryUpdateNotifier( if (shouldTruncate) { // "Chapters 1, 2.5, 3, 4, 5 and 10 more" val remaining = displayableChapterNumbers.size - NOTIF_MAX_CHAPTERS - val joinedChapterNumbers = displayableChapterNumbers - .take(NOTIF_MAX_CHAPTERS) - .joinToString(", ") + val joinedChapterNumbers = + displayableChapterNumbers + .take(NOTIF_MAX_CHAPTERS) + .joinToString(", ") context.pluralStringResource( MR.plurals.notification_chapters_multiple_and_more, remaining, @@ -365,10 +389,11 @@ class LibraryUpdateNotifier( * Returns an intent to open the main activity. */ private fun getNotificationIntent(): PendingIntent { - val intent = Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - action = Constants.SHORTCUT_UPDATES - } + val intent = + Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + action = Constants.SHORTCUT_UPDATES + } return PendingIntent.getActivity( context, 0, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt index c6401ef728..66dd8caa1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt @@ -39,9 +39,10 @@ import uy.kohesive.injekt.api.get import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicInteger -class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) : - CoroutineWorker(context, workerParams) { - +class MetadataUpdateJob( + private val context: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(context, workerParams) { private val sourceManager: SourceManager = Injekt.get() private val coverCache: CoverCache = Injekt.get() private val getLibraryManga: GetLibraryManga = Injekt.get() @@ -101,7 +102,8 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame val currentlyUpdatingManga = CopyOnWriteArrayList() coroutineScope { - mangaToUpdate.groupBy { it.manga.source } + mangaToUpdate + .groupBy { it.manga.source } .values .map { mangaInSource -> async { @@ -118,8 +120,10 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame val source = sourceManager.get(manga.source) ?: return@withUpdateNotification try { val networkManga = source.getMangaDetails(manga.toSManga()) - val updatedManga = manga.prepUpdateCover(coverCache, networkManga, true) - .copyFrom(networkManga) + val updatedManga = + manga + .prepUpdateCover(coverCache, networkManga, true) + .copyFrom(networkManga) try { updateManga.await(updatedManga.toMangaUpdate()) } catch (e: Exception) { @@ -133,8 +137,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame } } } - } - .awaitAll() + }.awaitAll() } notifier.cancelProgressNotification() @@ -180,10 +183,11 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame // Already running either as a scheduled or manual job return false } - val request = OneTimeWorkRequestBuilder() - .addTag(TAG) - .addTag(WORK_NAME_MANUAL) - .build() + val request = + OneTimeWorkRequestBuilder() + .addTag(TAG) + .addTag(WORK_NAME_MANUAL) + .build() wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request) return true @@ -191,10 +195,14 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame fun stop(context: Context) { val wm = context.workManager - val workQuery = WorkQuery.Builder.fromTags(listOf(TAG)) - .addStates(listOf(WorkInfo.State.RUNNING)) - .build() - wm.getWorkInfos(workQuery).get() + val workQuery = + WorkQuery.Builder + .fromTags(listOf(TAG)) + .addStates(listOf(WorkInfo.State.RUNNING)) + .build() + wm + .getWorkInfos(workQuery) + .get() // Should only return one work but just in case .forEach { wm.cancelWorkById(it.id) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt index f8435daecc..7342f4e468 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt @@ -19,10 +19,11 @@ object NotificationHandler { * @param context context of application */ internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent { - val intent = Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - action = Constants.SHORTCUT_DOWNLOADS - } + val intent = + Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + action = Constants.SHORTCUT_DOWNLOADS + } return PendingIntent.getActivity( context, 0, @@ -37,11 +38,15 @@ object NotificationHandler { * @param context context of application * @param file file containing image */ - internal fun openImagePendingActivity(context: Context, uri: Uri): PendingIntent { - val intent = Intent(Intent.ACTION_VIEW).apply { - setDataAndType(uri, "image/*") - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION - } + internal fun openImagePendingActivity( + context: Context, + uri: Uri, + ): PendingIntent { + val intent = + Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, "image/*") + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + } return PendingIntent.getActivity( context, 0, @@ -56,15 +61,22 @@ object NotificationHandler { * @param context context * @param uri uri of apk that is installed */ - fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent { - val intent = Intent(Intent.ACTION_VIEW).apply { - setDataAndType(uri, ExtensionInstaller.APK_MIME) - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION - } + fun installApkPendingActivity( + context: Context, + uri: Uri, + ): PendingIntent { + val intent = + Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, ExtensionInstaller.APK_MIME) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + } return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } - fun openUrl(context: Context, url: String): PendingIntent { + fun openUrl( + context: Context, + url: String, + ): PendingIntent { val notificationIntent = Intent(Intent.ACTION_VIEW, url.toUri()) return PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index de9e55803d..d421e18589 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -40,13 +40,15 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID * NOTE: Use local broadcasts if possible. */ class NotificationReceiver : BroadcastReceiver() { - private val getManga: GetManga by injectLazy() private val getChapter: GetChapter by injectLazy() private val updateChapter: UpdateChapter by injectLazy() private val downloadManager: DownloadManager by injectLazy() - override fun onReceive(context: Context, intent: Intent) { + override fun onReceive( + context: Context, + intent: Intent, + ) { when (intent.action) { // Dismiss notification ACTION_DISMISS_NOTIFICATION -> dismissNotification(context, intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) @@ -116,7 +118,10 @@ class NotificationReceiver : BroadcastReceiver() { * * @param notificationId the id of the notification */ - private fun dismissNotification(context: Context, notificationId: Int) { + private fun dismissNotification( + context: Context, + notificationId: Int, + ) { context.cancelNotification(notificationId) } @@ -126,7 +131,10 @@ class NotificationReceiver : BroadcastReceiver() { * @param context context of application * @param uri path of file */ - private fun shareImage(context: Context, uri: Uri) { + private fun shareImage( + context: Context, + uri: Uri, + ) { context.startActivity(uri.toShareIntent(context)) } @@ -136,7 +144,11 @@ class NotificationReceiver : BroadcastReceiver() { * @param context context of application * @param path path of file */ - private fun shareFile(context: Context, uri: Uri, fileMimeType: String) { + private fun shareFile( + context: Context, + uri: Uri, + fileMimeType: String, + ) { context.startActivity(uri.toShareIntent(context, fileMimeType)) } @@ -147,13 +159,18 @@ class NotificationReceiver : BroadcastReceiver() { * @param mangaId id of manga * @param chapterId id of chapter */ - private fun openChapter(context: Context, mangaId: Long, chapterId: Long) { + private fun openChapter( + context: Context, + mangaId: Long, + chapterId: Long, + ) { val manga = runBlocking { getManga.await(mangaId) } val chapter = runBlocking { getChapter.await(chapterId) } if (manga != null && chapter != null) { - val intent = ReaderActivity.newIntent(context, manga.id, chapter.id).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - } + val intent = + ReaderActivity.newIntent(context, manga.id, chapter.id).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } context.startActivity(intent) } else { context.toast(MR.strings.chapter_error) @@ -178,7 +195,10 @@ class NotificationReceiver : BroadcastReceiver() { LibraryUpdateJob.stop(context) } - private fun startDownloadAppUpdate(context: Context, intent: Intent) { + private fun startDownloadAppUpdate( + context: Context, + intent: Intent, + ) { val url = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL) ?: return AppUpdateDownloadJob.start(context, url) } @@ -193,25 +213,30 @@ class NotificationReceiver : BroadcastReceiver() { * @param chapterUrls URLs of chapter to mark as read * @param mangaId id of manga */ - private fun markAsRead(chapterUrls: Array, mangaId: Long) { + private fun markAsRead( + chapterUrls: Array, + mangaId: Long, + ) { val downloadPreferences: DownloadPreferences = Injekt.get() val sourceManager: SourceManager = Injekt.get() launchIO { - val toUpdate = chapterUrls.mapNotNull { getChapter.await(it, mangaId) } - .map { - val chapter = it.copy(read = true) - if (downloadPreferences.removeAfterMarkedAsRead().get()) { - val manga = getManga.await(mangaId) - if (manga != null) { - val source = sourceManager.get(manga.source) - if (source != null) { - downloadManager.deleteChapters(listOf(it), manga, source) + val toUpdate = + chapterUrls + .mapNotNull { getChapter.await(it, mangaId) } + .map { + val chapter = it.copy(read = true) + if (downloadPreferences.removeAfterMarkedAsRead().get()) { + val manga = getManga.await(mangaId) + if (manga != null) { + val source = sourceManager.get(manga.source) + if (source != null) { + downloadManager.deleteChapters(listOf(it), manga, source) + } } } + chapter.toChapterUpdate() } - chapter.toChapterUpdate() - } updateChapter.awaitAll(toUpdate) } } @@ -222,7 +247,10 @@ class NotificationReceiver : BroadcastReceiver() { * @param chapterUrls URLs of chapter to download * @param mangaId id of manga */ - private fun downloadChapters(chapterUrls: Array, mangaId: Long) { + private fun downloadChapters( + chapterUrls: Array, + mangaId: Long, + ) { launchIO { val manga = getManga.await(mangaId) ?: return@launchIO val chapters = chapterUrls.mapNotNull { getChapter.await(it, mangaId) } @@ -268,9 +296,10 @@ class NotificationReceiver : BroadcastReceiver() { * @return [PendingIntent] */ internal fun resumeDownloadsPendingBroadcast(context: Context): PendingIntent { - val intent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_RESUME_DOWNLOADS - } + val intent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_RESUME_DOWNLOADS + } return PendingIntent.getBroadcast( context, 0, @@ -286,9 +315,10 @@ class NotificationReceiver : BroadcastReceiver() { * @return [PendingIntent] */ internal fun pauseDownloadsPendingBroadcast(context: Context): PendingIntent { - val intent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_PAUSE_DOWNLOADS - } + val intent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_PAUSE_DOWNLOADS + } return PendingIntent.getBroadcast( context, 0, @@ -304,9 +334,10 @@ class NotificationReceiver : BroadcastReceiver() { * @return [PendingIntent] */ internal fun clearDownloadsPendingBroadcast(context: Context): PendingIntent { - val intent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_CLEAR_DOWNLOADS - } + val intent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_CLEAR_DOWNLOADS + } return PendingIntent.getBroadcast( context, 0, @@ -322,11 +353,15 @@ class NotificationReceiver : BroadcastReceiver() { * @param notificationId id of notification * @return [PendingIntent] */ - internal fun dismissNotificationPendingBroadcast(context: Context, notificationId: Int): PendingIntent { - val intent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_DISMISS_NOTIFICATION - putExtra(EXTRA_NOTIFICATION_ID, notificationId) - } + internal fun dismissNotificationPendingBroadcast( + context: Context, + notificationId: Int, + ): PendingIntent { + val intent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_DISMISS_NOTIFICATION + putExtra(EXTRA_NOTIFICATION_ID, notificationId) + } return PendingIntent.getBroadcast( context, 0, @@ -342,7 +377,11 @@ class NotificationReceiver : BroadcastReceiver() { * @param notificationId id of notification * @return [PendingIntent] */ - internal fun dismissNotification(context: Context, notificationId: Int, groupId: Int? = null) { + internal fun dismissNotification( + context: Context, + notificationId: Int, + groupId: Int? = null, + ) { /* Group notifications always have at least 2 notifications: - Group summary notification @@ -353,14 +392,17 @@ class NotificationReceiver : BroadcastReceiver() { When programmatically dismissing this notification, the group notification is not automatically dismissed. */ - val groupKey = context.notificationManager.activeNotifications.find { - it.id == notificationId - }?.groupKey + val groupKey = + context.notificationManager.activeNotifications + .find { + it.id == notificationId + }?.groupKey if (groupId != null && groupId != 0 && !groupKey.isNullOrEmpty()) { - val notifications = context.notificationManager.activeNotifications.filter { - it.groupKey == groupKey - } + val notifications = + context.notificationManager.activeNotifications.filter { + it.groupKey == groupKey + } if (notifications.size == 2) { context.cancelNotification(groupId) @@ -379,11 +421,15 @@ class NotificationReceiver : BroadcastReceiver() { * @param notificationId id of notification * @return [PendingIntent] */ - internal fun shareImagePendingBroadcast(context: Context, uri: Uri): PendingIntent { - val intent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_SHARE_IMAGE - putExtra(EXTRA_URI, uri.toString()) - } + internal fun shareImagePendingBroadcast( + context: Context, + uri: Uri, + ): PendingIntent { + val intent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_SHARE_IMAGE + putExtra(EXTRA_URI, uri.toString()) + } return PendingIntent.getBroadcast( context, 0, @@ -399,7 +445,11 @@ class NotificationReceiver : BroadcastReceiver() { * @param manga manga of chapter * @param chapter chapter that needs to be opened */ - internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter: Chapter): PendingIntent { + internal fun openChapterPendingActivity( + context: Context, + manga: Manga, + chapter: Chapter, + ): PendingIntent { val newIntent = ReaderActivity.newIntent(context, manga.id, chapter.id) return PendingIntent.getActivity( context, @@ -415,9 +465,14 @@ class NotificationReceiver : BroadcastReceiver() { * @param context context of application * @param manga manga of chapter */ - internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent { + internal fun openChapterPendingActivity( + context: Context, + manga: Manga, + groupId: Int, + ): PendingIntent { val newIntent = - Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_MANGA) + Intent(context, MainActivity::class.java) + .setAction(Constants.SHORTCUT_MANGA) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) .putExtra(Constants.MANGA_EXTRA, manga.id) .putExtra("notificationId", manga.id.hashCode()) @@ -442,13 +497,14 @@ class NotificationReceiver : BroadcastReceiver() { chapters: Array, groupId: Int, ): PendingIntent { - val newIntent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_MARK_AS_READ - putExtra(EXTRA_CHAPTER_URL, chapters.map { it.url }.toTypedArray()) - putExtra(EXTRA_MANGA_ID, manga.id) - putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode()) - putExtra(EXTRA_GROUP_ID, groupId) - } + val newIntent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_MARK_AS_READ + putExtra(EXTRA_CHAPTER_URL, chapters.map { it.url }.toTypedArray()) + putExtra(EXTRA_MANGA_ID, manga.id) + putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode()) + putExtra(EXTRA_GROUP_ID, groupId) + } return PendingIntent.getBroadcast( context, manga.id.hashCode(), @@ -469,13 +525,14 @@ class NotificationReceiver : BroadcastReceiver() { chapters: Array, groupId: Int, ): PendingIntent { - val newIntent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_DOWNLOAD_CHAPTER - putExtra(EXTRA_CHAPTER_URL, chapters.map { it.url }.toTypedArray()) - putExtra(EXTRA_MANGA_ID, manga.id) - putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode()) - putExtra(EXTRA_GROUP_ID, groupId) - } + val newIntent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_DOWNLOAD_CHAPTER + putExtra(EXTRA_CHAPTER_URL, chapters.map { it.url }.toTypedArray()) + putExtra(EXTRA_MANGA_ID, manga.id) + putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode()) + putExtra(EXTRA_GROUP_ID, groupId) + } return PendingIntent.getBroadcast( context, manga.id.hashCode(), @@ -491,9 +548,10 @@ class NotificationReceiver : BroadcastReceiver() { * @return [PendingIntent] */ internal fun cancelLibraryUpdatePendingBroadcast(context: Context): PendingIntent { - val intent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_CANCEL_LIBRARY_UPDATE - } + val intent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_CANCEL_LIBRARY_UPDATE + } return PendingIntent.getBroadcast( context, 0, @@ -512,8 +570,8 @@ class NotificationReceiver : BroadcastReceiver() { context: Context, url: String, title: String? = null, - ): PendingIntent { - return Intent(context, NotificationReceiver::class.java).run { + ): PendingIntent = + Intent(context, NotificationReceiver::class.java).run { action = ACTION_START_APP_UPDATE putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL, url) title?.let { putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_TITLE, it) } @@ -524,15 +582,15 @@ class NotificationReceiver : BroadcastReceiver() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) } - } /** * */ internal fun cancelDownloadAppUpdatePendingBroadcast(context: Context): PendingIntent { - val intent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_CANCEL_APP_UPDATE_DOWNLOAD - } + val intent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_CANCEL_APP_UPDATE_DOWNLOAD + } return PendingIntent.getBroadcast( context, 0, @@ -548,10 +606,11 @@ class NotificationReceiver : BroadcastReceiver() { * @return [PendingIntent] */ internal fun openExtensionsPendingActivity(context: Context): PendingIntent { - val intent = Intent(context, MainActivity::class.java).apply { - action = Constants.SHORTCUT_EXTENSIONS - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - } + val intent = + Intent(context, MainActivity::class.java).apply { + action = Constants.SHORTCUT_EXTENSIONS + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } return PendingIntent.getActivity( context, 0, @@ -567,11 +626,15 @@ class NotificationReceiver : BroadcastReceiver() { * @param uri uri of backup file * @return [PendingIntent] */ - internal fun shareBackupPendingBroadcast(context: Context, uri: Uri): PendingIntent { - val intent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_SHARE_BACKUP - putExtra(EXTRA_URI, uri) - } + internal fun shareBackupPendingBroadcast( + context: Context, + uri: Uri, + ): PendingIntent { + val intent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_SHARE_BACKUP + putExtra(EXTRA_URI, uri) + } return PendingIntent.getBroadcast( context, 0, @@ -587,12 +650,16 @@ class NotificationReceiver : BroadcastReceiver() { * @param uri uri of error log file * @return [PendingIntent] */ - internal fun openErrorLogPendingActivity(context: Context, uri: Uri): PendingIntent { - val intent = Intent().apply { - action = Intent.ACTION_VIEW - setDataAndType(uri, "text/plain") - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION - } + internal fun openErrorLogPendingActivity( + context: Context, + uri: Uri, + ): PendingIntent { + val intent = + Intent().apply { + action = Intent.ACTION_VIEW + setDataAndType(uri, "text/plain") + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + } return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } @@ -603,11 +670,15 @@ class NotificationReceiver : BroadcastReceiver() { * @param notificationId id of notification * @return [PendingIntent] */ - internal fun cancelRestorePendingBroadcast(context: Context, notificationId: Int): PendingIntent { - val intent = Intent(context, NotificationReceiver::class.java).apply { - action = ACTION_CANCEL_RESTORE - putExtra(EXTRA_NOTIFICATION_ID, notificationId) - } + internal fun cancelRestorePendingBroadcast( + context: Context, + notificationId: Int, + ): PendingIntent { + val intent = + Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_CANCEL_RESTORE + putExtra(EXTRA_NOTIFICATION_ID, notificationId) + } return PendingIntent.getBroadcast( context, 0, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt index 6b34d124d3..1cd232e3cf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt @@ -14,7 +14,6 @@ import tachiyomi.i18n.MR * Class to manage the basic information of all the notifications used in the app. */ object Notifications { - /** * Common notification channel and ids used anywhere. */ @@ -75,17 +74,18 @@ object Notifications { const val ID_UPDATES_TO_EXTS = -401 const val ID_EXTENSION_INSTALLER = -402 - private val deprecatedChannels = listOf( - "downloader_channel", - "downloader_complete_channel", - "backup_restore_complete_channel", - "library_channel", - "library_progress_channel", - "updates_ext_channel", - "downloader_cache_renewal", - "crash_logs_channel", - "library_skipped_channel", - ) + private val deprecatedChannels = + listOf( + "downloader_channel", + "downloader_complete_channel", + "backup_restore_complete_channel", + "library_channel", + "library_progress_channel", + "updates_ext_channel", + "downloader_cache_renewal", + "crash_logs_channel", + "library_skipped_channel", + ) /** * Creates the notification channels introduced in Android Oreo. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt index ae11e587de..3c060d93fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt @@ -4,63 +4,88 @@ import android.content.SharedPreferences import androidx.core.content.edit import androidx.preference.PreferenceDataStore -class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() { +class SharedPreferencesDataStore( + private val prefs: SharedPreferences, +) : PreferenceDataStore() { + override fun getBoolean( + key: String?, + defValue: Boolean, + ): Boolean = prefs.getBoolean(key, defValue) - override fun getBoolean(key: String?, defValue: Boolean): Boolean { - return prefs.getBoolean(key, defValue) - } - - override fun putBoolean(key: String?, value: Boolean) { + override fun putBoolean( + key: String?, + value: Boolean, + ) { prefs.edit { putBoolean(key, value) } } - override fun getInt(key: String?, defValue: Int): Int { - return prefs.getInt(key, defValue) - } + override fun getInt( + key: String?, + defValue: Int, + ): Int = prefs.getInt(key, defValue) - override fun putInt(key: String?, value: Int) { + override fun putInt( + key: String?, + value: Int, + ) { prefs.edit { putInt(key, value) } } - override fun getLong(key: String?, defValue: Long): Long { - return prefs.getLong(key, defValue) - } + override fun getLong( + key: String?, + defValue: Long, + ): Long = prefs.getLong(key, defValue) - override fun putLong(key: String?, value: Long) { + override fun putLong( + key: String?, + value: Long, + ) { prefs.edit { putLong(key, value) } } - override fun getFloat(key: String?, defValue: Float): Float { - return prefs.getFloat(key, defValue) - } + override fun getFloat( + key: String?, + defValue: Float, + ): Float = prefs.getFloat(key, defValue) - override fun putFloat(key: String?, value: Float) { + override fun putFloat( + key: String?, + value: Float, + ) { prefs.edit { putFloat(key, value) } } - override fun getString(key: String?, defValue: String?): String? { - return prefs.getString(key, defValue) - } + override fun getString( + key: String?, + defValue: String?, + ): String? = prefs.getString(key, defValue) - override fun putString(key: String?, value: String?) { + override fun putString( + key: String?, + value: String?, + ) { prefs.edit { putString(key, value) } } - override fun getStringSet(key: String?, defValues: MutableSet?): MutableSet? { - return prefs.getStringSet(key, defValues) - } + override fun getStringSet( + key: String?, + defValues: MutableSet?, + ): MutableSet? = prefs.getStringSet(key, defValues) - override fun putStringSet(key: String?, values: MutableSet?) { + override fun putStringSet( + key: String?, + values: MutableSet?, + ) { prefs.edit { putStringSet(key, values) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt index b7b53d835e..56015c50af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt @@ -28,7 +28,6 @@ import java.time.Instant class ImageSaver( val context: Context, ) { - fun save(image: Image): Uri { val data = image.data @@ -42,7 +41,11 @@ class ImageSaver( return saveApi29(image, type, filename, data) } - private fun save(inputStream: InputStream, directory: File, filename: String): Uri { + private fun save( + inputStream: InputStream, + directory: File, + filename: String, + ): Uri { directory.mkdirs() val destFile = File(directory, filename) @@ -69,25 +72,28 @@ class ImageSaver( MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val imageLocation = (image.location as Location.Pictures).relativePath - val relativePath = listOf( - Environment.DIRECTORY_PICTURES, - context.stringResource(MR.strings.app_name), - imageLocation, - ).joinToString(File.separator) - - val contentValues = contentValuesOf( - MediaStore.Images.Media.RELATIVE_PATH to relativePath, - MediaStore.Images.Media.DISPLAY_NAME to image.name, - MediaStore.Images.Media.MIME_TYPE to type.mime, - MediaStore.Images.Media.DATE_MODIFIED to Instant.now().epochSecond, - ) - - val picture = findUriOrDefault(relativePath, filename) { - context.contentResolver.insert( - pictureDir, - contentValues, - ) ?: throw IOException(context.stringResource(MR.strings.error_saving_picture)) - } + val relativePath = + listOf( + Environment.DIRECTORY_PICTURES, + context.stringResource(MR.strings.app_name), + imageLocation, + ).joinToString(File.separator) + + val contentValues = + contentValuesOf( + MediaStore.Images.Media.RELATIVE_PATH to relativePath, + MediaStore.Images.Media.DISPLAY_NAME to image.name, + MediaStore.Images.Media.MIME_TYPE to type.mime, + MediaStore.Images.Media.DATE_MODIFIED to Instant.now().epochSecond, + ) + + val picture = + findUriOrDefault(relativePath, filename) { + context.contentResolver.insert( + pictureDir, + contentValues, + ) ?: throw IOException(context.stringResource(MR.strings.error_saving_picture)) + } try { data().use { input -> @@ -106,32 +112,38 @@ class ImageSaver( } @RequiresApi(Build.VERSION_CODES.Q) - private fun findUriOrDefault(path: String, filename: String, default: () -> Uri): Uri { - val projection = arrayOf( - MediaStore.MediaColumns._ID, - MediaStore.MediaColumns.DISPLAY_NAME, - MediaStore.MediaColumns.RELATIVE_PATH, - ) + private fun findUriOrDefault( + path: String, + filename: String, + default: () -> Uri, + ): Uri { + val projection = + arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.RELATIVE_PATH, + ) val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=? AND ${MediaStore.MediaColumns.DISPLAY_NAME}=?" // Need to make sure it ends with the separator val normalizedPath = "${path.removeSuffix(File.separator)}${File.separator}" - context.contentResolver.query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - projection, - selection, - arrayOf(normalizedPath, filename), - null, - ).use { cursor -> - if (cursor != null && cursor.count >= 1) { - if (cursor.moveToFirst()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) + context.contentResolver + .query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, + selection, + arrayOf(normalizedPath, filename), + null, + ).use { cursor -> + if (cursor != null && cursor.count >= 1) { + if (cursor.moveToFirst()) { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) + return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) + } } } - } return default() } @@ -169,11 +181,11 @@ sealed class Image( } sealed interface Location { - data class Pictures private constructor(val relativePath: String) : Location { + data class Pictures private constructor( + val relativePath: String, + ) : Location { companion object { - fun create(relativePath: String = ""): Pictures { - return Pictures(relativePath) - } + fun create(relativePath: String = ""): Pictures = Pictures(relativePath) } } @@ -183,10 +195,11 @@ sealed interface Location { return when (this) { Cache -> context.cacheImageDir is Pictures -> { - val file = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), - context.stringResource(MR.strings.app_name), - ) + val file = + File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + context.stringResource(MR.strings.app_name), + ) if (relativePath.isNotEmpty()) { return File( file, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt index 8f88f1051f..322393867d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt @@ -23,7 +23,6 @@ abstract class BaseTracker( override val id: Long, override val name: String, ) : Tracker { - val trackPreferences: TrackPreferences by injectLazy() val networkService: NetworkHelper by injectLazy() private val addTracks: AddTracks by injectLazy() @@ -36,13 +35,9 @@ abstract class BaseTracker( override val supportsReadingDates: Boolean = false // TODO: Store all scores as 10 point in the future maybe? - override fun get10PointScore(track: DomainTrack): Double { - return track.score - } + override fun get10PointScore(track: DomainTrack): Double = track.score - override fun indexToScore(index: Int): Double { - return index.toDouble() - } + override fun indexToScore(index: Int): Double = index.toDouble() @CallSuper override fun logout() { @@ -50,18 +45,25 @@ abstract class BaseTracker( } override val isLoggedIn: Boolean - get() = getUsername().isNotEmpty() && - getPassword().isNotEmpty() + get() = + getUsername().isNotEmpty() && + getPassword().isNotEmpty() override fun getUsername() = trackPreferences.trackUsername(this).get() override fun getPassword() = trackPreferences.trackPassword(this).get() - override fun saveCredentials(username: String, password: String) { + override fun saveCredentials( + username: String, + password: String, + ) { trackPreferences.setCredentials(this, username, password) } - override suspend fun register(item: Track, mangaId: Long) { + override suspend fun register( + item: Track, + mangaId: Long, + ) { item.manga_id = mangaId try { addTracks.bind(this, item, mangaId) @@ -70,7 +72,10 @@ abstract class BaseTracker( } } - override suspend fun setRemoteStatus(track: Track, status: Long) { + override suspend fun setRemoteStatus( + track: Track, + status: Long, + ) { track.status = status if (track.status == getCompletionStatus() && track.total_chapters != 0L) { track.last_chapter_read = track.total_chapters.toDouble() @@ -78,7 +83,10 @@ abstract class BaseTracker( updateRemote(track) } - override suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) { + override suspend fun setRemoteLastChapterRead( + track: Track, + chapterNumber: Int, + ) { if ( track.last_chapter_read == 0.0 && track.last_chapter_read < chapterNumber && @@ -94,30 +102,40 @@ abstract class BaseTracker( updateRemote(track) } - override suspend fun setRemoteScore(track: Track, scoreString: String) { + override suspend fun setRemoteScore( + track: Track, + scoreString: String, + ) { track.score = indexToScore(getScoreList().indexOf(scoreString)) updateRemote(track) } - override suspend fun setRemoteStartDate(track: Track, epochMillis: Long) { + override suspend fun setRemoteStartDate( + track: Track, + epochMillis: Long, + ) { track.started_reading_date = epochMillis updateRemote(track) } - override suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) { + override suspend fun setRemoteFinishDate( + track: Track, + epochMillis: Long, + ) { track.finished_reading_date = epochMillis updateRemote(track) } - private suspend fun updateRemote(track: Track): Unit = withIOContext { - try { - update(track) - track.toDomainTrack(idRequired = false)?.let { - insertTrack.await(it) + private suspend fun updateRemote(track: Track): Unit = + withIOContext { + try { + update(track) + track.toDomainTrack(idRequired = false)?.let { + insertTrack.await(it) + } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" } + withUIContext { Injekt.get().toast(e.message) } } - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" } - withUIContext { Injekt.get().toast(e.message) } } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt index 9001639260..7aae3af28e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt @@ -6,6 +6,5 @@ import tachiyomi.domain.track.model.Track * Tracker that support deleting am entry from a user's list. */ interface DeletableTracker { - suspend fun delete(track: Track) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTracker.kt index a501cded88..5092c523c4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTracker.kt @@ -10,13 +10,10 @@ import tachiyomi.domain.track.model.Track * It is expected that such tracker can only work with specific sources and unique IDs. */ interface EnhancedTracker { - /** * This tracker will only work with the sources that are accepted by this filter function. */ - fun accept(source: Source): Boolean { - return source::class.qualifiedName in getAcceptedSources() - } + fun accept(source: Source): Boolean = source::class.qualifiedName in getAcceptedSources() /** * Fully qualified source classes that this tracker is compatible with. @@ -33,10 +30,18 @@ interface EnhancedTracker { /** * Checks whether the provided source/track/manga triplet is from this [Tracker] */ - fun isTrackFrom(track: Track, manga: Manga, source: Source?): Boolean + fun isTrackFrom( + track: Track, + manga: Manga, + source: Source?, + ): Boolean /** * Migrates the given track for the manga to the newSource, if possible */ - fun migrateTrack(track: Track, manga: Manga, newSource: Source): Track? + fun migrateTrack( + track: Track, + manga: Manga, + newSource: Source, + ): Track? } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt index 06644e9329..60eed95aac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt @@ -11,7 +11,6 @@ import okhttp3.OkHttpClient import tachiyomi.domain.track.model.Track as DomainTrack interface Tracker { - val id: Long val name: String @@ -46,15 +45,24 @@ interface Tracker { fun displayScore(track: DomainTrack): String - suspend fun update(track: Track, didReadChapter: Boolean = false): Track + suspend fun update( + track: Track, + didReadChapter: Boolean = false, + ): Track - suspend fun bind(track: Track, hasReadChapters: Boolean = false): Track + suspend fun bind( + track: Track, + hasReadChapters: Boolean = false, + ): Track suspend fun search(query: String): List suspend fun refresh(track: Track): Track - suspend fun login(username: String, password: String) + suspend fun login( + username: String, + password: String, + ) @CallSuper fun logout() @@ -65,18 +73,39 @@ interface Tracker { fun getPassword(): String - fun saveCredentials(username: String, password: String) + fun saveCredentials( + username: String, + password: String, + ) // TODO: move this to an interactor, and update all trackers based on common data - suspend fun register(item: Track, mangaId: Long) - - suspend fun setRemoteStatus(track: Track, status: Long) - - suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) - - suspend fun setRemoteScore(track: Track, scoreString: String) - - suspend fun setRemoteStartDate(track: Track, epochMillis: Long) - - suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) + suspend fun register( + item: Track, + mangaId: Long, + ) + + suspend fun setRemoteStatus( + track: Track, + status: Long, + ) + + suspend fun setRemoteLastChapterRead( + track: Track, + chapterNumber: Int, + ) + + suspend fun setRemoteScore( + track: Track, + scoreString: String, + ) + + suspend fun setRemoteStartDate( + track: Track, + epochMillis: Long, + ) + + suspend fun setRemoteFinishDate( + track: Track, + epochMillis: Long, + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt index 598a0c06c6..22f9a2dd6b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt @@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.track.shikimori.Shikimori import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi class TrackerManager { - companion object { const val ANILIST = 2L const val KITSU = 3L diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index abf0d702a1..f40ed50e3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -17,8 +17,10 @@ import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy import tachiyomi.domain.track.model.Track as DomainTrack -class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { - +class Anilist( + id: Long, +) : BaseTracker(id, "AniList"), + DeletableTracker { companion object { const val READING = 1L const val COMPLETED = 2L @@ -58,19 +60,18 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { override fun getLogoColor() = Color.rgb(18, 25, 35) - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING) - } - - override fun getStatus(status: Long): StringResource? = when (status) { - READING -> MR.strings.reading - PLAN_TO_READ -> MR.strings.plan_to_read - COMPLETED -> MR.strings.completed - ON_HOLD -> MR.strings.on_hold - DROPPED -> MR.strings.dropped - REREADING -> MR.strings.repeating - else -> null - } + override fun getStatusList(): List = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING) + + override fun getStatus(status: Long): StringResource? = + when (status) { + READING -> MR.strings.reading + PLAN_TO_READ -> MR.strings.plan_to_read + COMPLETED -> MR.strings.completed + ON_HOLD -> MR.strings.on_hold + DROPPED -> MR.strings.dropped + REREADING -> MR.strings.repeating + else -> null + } override fun getReadingStatus(): Long = READING @@ -78,8 +79,8 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { override fun getCompletionStatus(): Long = COMPLETED - override fun getScoreList(): ImmutableList { - return when (scorePreference.get()) { + override fun getScoreList(): ImmutableList = + when (scorePreference.get()) { // 10 point POINT_10 -> IntRange(0, 10).map(Int::toString).toImmutableList() // 100 point @@ -92,62 +93,66 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() }.toImmutableList() else -> throw Exception("Unknown score type") } - } override fun get10PointScore(track: DomainTrack): Double { // Score is stored in 100 point format return track.score / 10.0 } - override fun indexToScore(index: Int): Double { - return when (scorePreference.get()) { + override fun indexToScore(index: Int): Double = + when (scorePreference.get()) { // 10 point POINT_10 -> index * 10.0 // 100 point POINT_100 -> index.toDouble() // 5 stars - POINT_5 -> when (index) { - 0 -> 0.0 - else -> index * 20.0 - 10.0 - } + POINT_5 -> + when (index) { + 0 -> 0.0 + else -> index * 20.0 - 10.0 + } // Smiley - POINT_3 -> when (index) { - 0 -> 0.0 - else -> index * 25.0 + 10.0 - } + POINT_3 -> + when (index) { + 0 -> 0.0 + else -> index * 25.0 + 10.0 + } // 10 point decimal POINT_10_DECIMAL -> index.toDouble() else -> throw Exception("Unknown score type") } - } override fun displayScore(track: DomainTrack): String { val score = track.score return when (scorePreference.get()) { - POINT_5 -> when (score) { - 0.0 -> "0 ★" - else -> "${((score + 10) / 20).toInt()} ★" - } - POINT_3 -> when { - score == 0.0 -> "0" - score <= 35 -> "😦" - score <= 60 -> "😐" - else -> "😊" - } + POINT_5 -> + when (score) { + 0.0 -> "0 ★" + else -> "${((score + 10) / 20).toInt()} ★" + } + POINT_3 -> + when { + score == 0.0 -> "0" + score <= 35 -> "😦" + score <= 60 -> "😐" + else -> "😊" + } else -> track.toAnilistScore() } } - private suspend fun add(track: Track): Track { - return api.addLibManga(track) - } + private suspend fun add(track: Track): Track = api.addLibManga(track) - override suspend fun update(track: Track, didReadChapter: Boolean): Track { + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { // If user was using API v1 fetch library_id if (track.library_id == null || track.library_id!! == 0L) { - val libManga = api.findLibManga(track, getUsername().toInt()) - ?: throw Exception("$track not found on user library") + val libManga = + api.findLibManga(track, getUsername().toInt()) + ?: throw Exception("$track not found on user library") track.library_id = libManga.library_id } @@ -177,7 +182,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { api.deleteLibManga(track) } - override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track { val remoteTrack = api.findLibManga(track, getUsername().toInt()) return if (remoteTrack != null) { track.copyPersonalFrom(remoteTrack) @@ -197,9 +205,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { } } - override suspend fun search(query: String): List { - return api.search(query) - } + override suspend fun search(query: String): List = api.search(query) override suspend fun refresh(track: Track): Track { val remoteTrack = api.getLibManga(track, getUsername().toInt()) @@ -209,7 +215,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { return track } - override suspend fun login(username: String, password: String) = login(password) + override suspend fun login( + username: String, + password: String, + ) = login(password) suspend fun login(token: String) { try { @@ -233,11 +242,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) } - fun loadOAuth(): OAuth? { - return try { + fun loadOAuth(): OAuth? = + try { json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } - } } 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 c808d0f31c..da1f2b9f9a 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 @@ -34,18 +34,23 @@ import java.time.ZonedDateTime import kotlin.time.Duration.Companion.minutes import tachiyomi.domain.track.model.Track as DomainTrack -class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { - +class AnilistApi( + val client: OkHttpClient, + interceptor: AnilistInterceptor, +) { private val json: Json by injectLazy() - private val authClient = client.newBuilder() - .addInterceptor(interceptor) - .rateLimit(permits = 85, period = 1.minutes) - .build() + private val authClient = + client + .newBuilder() + .addInterceptor(interceptor) + .rateLimit(permits = 85, period = 1.minutes) + .build() - suspend fun addLibManga(track: Track): Track { - return withIOContext { - val query = """ + suspend fun addLibManga(track: Track): Track = + withIOContext { + val query = + """ |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { | id @@ -53,36 +58,40 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("mangaId", track.remote_id) - put("progress", track.last_chapter_read.toInt()) - put("status", track.toAnilistStatus()) + """.trimMargin() + val payload = + buildJsonObject { + put("query", query) + putJsonObject("variables") { + put("mangaId", track.remote_id) + put("progress", track.last_chapter_read.toInt()) + put("status", track.toAnilistStatus()) + } } - } with(json) { - authClient.newCall( - POST( - apiUrl, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() + authClient + .newCall( + POST( + apiUrl, + body = payload.toString().toRequestBody(jsonMime), + ), + ).awaitSuccess() .parseAs() .let { track.library_id = - it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long + it["data"]!! + .jsonObject["SaveMediaListEntry"]!! + .jsonObject["id"]!! + .jsonPrimitive.long track } } } - } - suspend fun updateLibManga(track: Track): Track { - return withIOContext { - val query = """ + suspend fun updateLibManga(track: Track): Track = + withIOContext { + val query = + """ |mutation UpdateManga( |${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, |${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput @@ -97,47 +106,53 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("listId", track.library_id) - put("progress", track.last_chapter_read.toInt()) - put("status", track.toAnilistStatus()) - put("score", track.score.toInt()) - put("startedAt", createDate(track.started_reading_date)) - put("completedAt", createDate(track.finished_reading_date)) + """.trimMargin() + val payload = + buildJsonObject { + put("query", query) + putJsonObject("variables") { + put("listId", track.library_id) + put("progress", track.last_chapter_read.toInt()) + put("status", track.toAnilistStatus()) + put("score", track.score.toInt()) + put("startedAt", createDate(track.started_reading_date)) + put("completedAt", createDate(track.finished_reading_date)) + } } - } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + authClient + .newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() track } - } suspend fun deleteLibManga(track: DomainTrack) { withIOContext { - val query = """ + val query = + """ |mutation DeleteManga(${'$'}listId: Int) { |DeleteMediaListEntry(id: ${'$'}listId) { |deleted |} |} | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("listId", track.libraryId) + """.trimMargin() + val payload = + buildJsonObject { + put("query", query) + putJsonObject("variables") { + put("listId", track.libraryId) + } } - } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + authClient + .newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() } } - suspend fun search(search: String): List { - return withIOContext { - val query = """ + + suspend fun search(search: String): List = + withIOContext { + val query = + """ |query Search(${'$'}query: String) { |Page (perPage: 50) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { @@ -162,21 +177,22 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("query", search) + """.trimMargin() + val payload = + buildJsonObject { + put("query", query) + putJsonObject("variables") { + put("query", search) + } } - } with(json) { - authClient.newCall( - POST( - apiUrl, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() + authClient + .newCall( + POST( + apiUrl, + body = payload.toString().toRequestBody(jsonMime), + ), + ).awaitSuccess() .parseAs() .let { response -> val data = response["data"]!!.jsonObject @@ -187,11 +203,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } } } - } - suspend fun findLibManga(track: Track, userid: Int): Track? { - return withIOContext { - val query = """ + suspend fun findLibManga( + track: Track, + userid: Int, + ): Track? = + withIOContext { + val query = + """ |query (${'$'}id: Int!, ${'$'}manga_id: Int!) { |Page { |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { @@ -231,22 +250,23 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("id", userid) - put("manga_id", track.remote_id) + """.trimMargin() + val payload = + buildJsonObject { + put("query", query) + putJsonObject("variables") { + put("id", userid) + put("manga_id", track.remote_id) + } } - } with(json) { - authClient.newCall( - POST( - apiUrl, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() + authClient + .newCall( + POST( + apiUrl, + body = payload.toString().toRequestBody(jsonMime), + ), + ).awaitSuccess() .parseAs() .let { response -> val data = response["data"]!!.jsonObject @@ -257,19 +277,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } } } - } - suspend fun getLibManga(track: Track, userId: Int): Track { - return findLibManga(track, userId) ?: throw Exception("Could not find manga") - } + suspend fun getLibManga( + track: Track, + userId: Int, + ): Track = findLibManga(track, userId) ?: throw Exception("Could not find manga") - fun createOAuth(token: String): OAuth { - return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) - } + fun createOAuth(token: String): OAuth = + OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) - suspend fun getCurrentUser(): Pair { - return withIOContext { - val query = """ + suspend fun getCurrentUser(): Pair = + withIOContext { + val query = + """ |query User { |Viewer { |id @@ -279,18 +299,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - } + """.trimMargin() + val payload = + buildJsonObject { + put("query", query) + } with(json) { - authClient.newCall( - POST( - apiUrl, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() + authClient + .newCall( + POST( + apiUrl, + body = payload.toString().toRequestBody(jsonMime), + ), + ).awaitSuccess() .parseAs() .let { val data = it["data"]!!.jsonObject @@ -302,10 +323,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } } } - } - private fun jsonToALManga(struct: JsonObject): ALManga { - return ALManga( + private fun jsonToALManga(struct: JsonObject): ALManga = + ALManga( struct["id"]!!.jsonPrimitive.long, struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content, struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content, @@ -316,10 +336,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { struct["chapters"]!!.jsonPrimitive.longOrNull ?: 0, struct["averageScore"]?.jsonPrimitive?.intOrNull ?: -1, ) - } - private fun jsonToALUserManga(struct: JsonObject): ALUserManga { - return ALUserManga( + private fun jsonToALUserManga(struct: JsonObject): ALUserManga = + ALUserManga( struct["id"]!!.jsonPrimitive.long, struct["status"]!!.jsonPrimitive.content, struct["scoreRaw"]!!.jsonPrimitive.int, @@ -328,17 +347,18 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { parseDate(struct, "completedAt"), jsonToALManga(struct["media"]!!.jsonObject), ) - } - private fun parseDate(struct: JsonObject, dateKey: String): Long { + private fun parseDate( + struct: JsonObject, + dateKey: String, + ): Long { return try { return LocalDate .of( struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int, struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int, struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int, - ) - .atStartOfDay(ZoneId.systemDefault()) + ).atStartOfDay(ZoneId.systemDefault()) .toInstant() .toEpochMilli() } catch (_: Exception) { @@ -369,13 +389,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { private const val baseUrl = "https://anilist.co/api/v2/" private const val baseMangaUrl = "https://anilist.co/manga/" - fun mangaUrl(mediaId: Long): String { - return baseMangaUrl + mediaId - } + fun mangaUrl(mediaId: Long): String = baseMangaUrl + mediaId - fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon() - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("response_type", "token") - .build() + fun authUrl(): Uri = + "${baseUrl}oauth/authorize" + .toUri() + .buildUpon() + .appendQueryParameter("client_id", clientId) + .appendQueryParameter("response_type", "token") + .build() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index 388b3e1b52..5bfa2fbe97 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -5,8 +5,10 @@ import okhttp3.Interceptor import okhttp3.Response import java.io.IOException -class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor { - +class AnilistInterceptor( + val anilist: Anilist, + private var token: String?, +) : Interceptor { /** * OAuth object used for authenticated requests. * @@ -39,10 +41,12 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int } // Add the authorization header to the original request. - val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") - .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") - .build() + val authRequest = + originalRequest + .newBuilder() + .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") + .build() return chain.proceed(authRequest) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt index d7c037afe6..2a154faffc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -22,26 +22,27 @@ data class ALManga( val total_chapters: Long, val average_score: Int, ) { - - fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply { - remote_id = this@ALManga.remote_id - title = title_user_pref - total_chapters = this@ALManga.total_chapters - cover_url = image_url_lge - summary = description?.htmlDecode() ?: "" - score = average_score.toDouble() - tracking_url = AnilistApi.mangaUrl(remote_id) - publishing_status = this@ALManga.publishing_status - publishing_type = format - if (start_date_fuzzy != 0L) { - start_date = try { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(start_date_fuzzy) - } catch (e: Exception) { - "" + fun toTrack() = + TrackSearch.create(TrackerManager.ANILIST).apply { + remote_id = this@ALManga.remote_id + title = title_user_pref + total_chapters = this@ALManga.total_chapters + cover_url = image_url_lge + summary = description?.htmlDecode() ?: "" + score = average_score.toDouble() + tracking_url = AnilistApi.mangaUrl(remote_id) + publishing_status = this@ALManga.publishing_status + publishing_type = format + if (start_date_fuzzy != 0L) { + start_date = + try { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(start_date_fuzzy) + } catch (e: Exception) { + "" + } } } - } } data class ALUserManga( @@ -53,28 +54,29 @@ data class ALUserManga( val completed_date_fuzzy: Long, val manga: ALManga, ) { + fun toTrack() = + Track.create(TrackerManager.ANILIST).apply { + remote_id = manga.remote_id + title = manga.title_user_pref + status = toTrackStatus() + score = score_raw.toDouble() + started_reading_date = start_date_fuzzy + finished_reading_date = completed_date_fuzzy + last_chapter_read = chapters_read.toDouble() + library_id = this@ALUserManga.library_id + total_chapters = manga.total_chapters + } - fun toTrack() = Track.create(TrackerManager.ANILIST).apply { - remote_id = manga.remote_id - title = manga.title_user_pref - status = toTrackStatus() - score = score_raw.toDouble() - started_reading_date = start_date_fuzzy - finished_reading_date = completed_date_fuzzy - last_chapter_read = chapters_read.toDouble() - library_id = this@ALUserManga.library_id - total_chapters = manga.total_chapters - } - - private fun toTrackStatus() = when (list_status) { - "CURRENT" -> Anilist.READING - "COMPLETED" -> Anilist.COMPLETED - "PAUSED" -> Anilist.ON_HOLD - "DROPPED" -> Anilist.DROPPED - "PLANNING" -> Anilist.PLAN_TO_READ - "REPEATING" -> Anilist.REREADING - else -> throw NotImplementedError("Unknown status: $list_status") - } + private fun toTrackStatus() = + when (list_status) { + "CURRENT" -> Anilist.READING + "COMPLETED" -> Anilist.COMPLETED + "PAUSED" -> Anilist.ON_HOLD + "DROPPED" -> Anilist.DROPPED + "PLANNING" -> Anilist.PLAN_TO_READ + "REPEATING" -> Anilist.REREADING + else -> throw NotImplementedError("Unknown status: $list_status") + } } @Serializable @@ -87,40 +89,44 @@ data class OAuth( fun OAuth.isExpired() = System.currentTimeMillis() > expires -fun Track.toAnilistStatus() = when (status) { - Anilist.READING -> "CURRENT" - Anilist.COMPLETED -> "COMPLETED" - Anilist.ON_HOLD -> "PAUSED" - Anilist.DROPPED -> "DROPPED" - Anilist.PLAN_TO_READ -> "PLANNING" - Anilist.REREADING -> "REPEATING" - else -> throw NotImplementedError("Unknown status: $status") -} +fun Track.toAnilistStatus() = + when (status) { + Anilist.READING -> "CURRENT" + Anilist.COMPLETED -> "COMPLETED" + Anilist.ON_HOLD -> "PAUSED" + Anilist.DROPPED -> "DROPPED" + Anilist.PLAN_TO_READ -> "PLANNING" + Anilist.REREADING -> "REPEATING" + else -> throw NotImplementedError("Unknown status: $status") + } private val preferences: TrackPreferences by injectLazy() -fun DomainTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) { - // 10 point - "POINT_10" -> (score.toInt() / 10).toString() - // 100 point - "POINT_100" -> score.toInt().toString() - // 5 stars - "POINT_5" -> when { - score == 0.0 -> "0" - score < 30 -> "1" - score < 50 -> "2" - score < 70 -> "3" - score < 90 -> "4" - else -> "5" - } - // Smiley - "POINT_3" -> when { - score == 0.0 -> "0" - score <= 35 -> ":(" - score <= 60 -> ":|" - else -> ":)" +fun DomainTrack.toAnilistScore(): String = + when (preferences.anilistScoreType().get()) { + // 10 point + "POINT_10" -> (score.toInt() / 10).toString() + // 100 point + "POINT_100" -> score.toInt().toString() + // 5 stars + "POINT_5" -> + when { + score == 0.0 -> "0" + score < 30 -> "1" + score < 50 -> "2" + score < 70 -> "3" + score < 90 -> "4" + else -> "5" + } + // Smiley + "POINT_3" -> + when { + score == 0.0 -> "0" + score <= 35 -> ":(" + score <= 60 -> ":|" + else -> ":)" + } + // 10 point decimal + "POINT_10_DECIMAL" -> (score / 10).toString() + else -> throw NotImplementedError("Unknown score type") } - // 10 point decimal - "POINT_10_DECIMAL" -> (score / 10).toString() - else -> throw NotImplementedError("Unknown score type") -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index b8e7d2acc0..6936c02464 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -14,8 +14,9 @@ import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy import tachiyomi.domain.track.model.Track as DomainTrack -class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { - +class Bangumi( + id: Long, +) : BaseTracker(id, "Bangumi") { private val json: Json by injectLazy() private val interceptor by lazy { BangumiInterceptor(this) } @@ -24,15 +25,14 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { override fun getScoreList(): ImmutableList = SCORE_LIST - override fun displayScore(track: DomainTrack): String { - return track.score.toInt().toString() - } + override fun displayScore(track: DomainTrack): String = track.score.toInt().toString() - private suspend fun add(track: Track): Track { - return api.addLibManga(track) - } + private suspend fun add(track: Track): Track = api.addLibManga(track) - override suspend fun update(track: Track, didReadChapter: Boolean): Track { + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { if (track.status != COMPLETED) { if (didReadChapter) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { @@ -46,7 +46,10 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { return api.updateLibManga(track) } - override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track { val statusTrack = api.statusLibManga(track) val remoteTrack = api.findLibManga(track) return if (remoteTrack != null && statusTrack != null) { @@ -70,9 +73,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { } } - override suspend fun search(query: String): List { - return api.search(query) - } + override suspend fun search(query: String): List = api.search(query) override suspend fun refresh(track: Track): Track { val remoteStatusTrack = api.statusLibManga(track) @@ -87,18 +88,17 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { override fun getLogoColor() = Color.rgb(240, 145, 153) - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) - } + override fun getStatusList(): List = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) - override fun getStatus(status: Long): StringResource? = when (status) { - READING -> MR.strings.reading - PLAN_TO_READ -> MR.strings.plan_to_read - COMPLETED -> MR.strings.completed - ON_HOLD -> MR.strings.on_hold - DROPPED -> MR.strings.dropped - else -> null - } + override fun getStatus(status: Long): StringResource? = + when (status) { + READING -> MR.strings.reading + PLAN_TO_READ -> MR.strings.plan_to_read + COMPLETED -> MR.strings.completed + ON_HOLD -> MR.strings.on_hold + DROPPED -> MR.strings.dropped + else -> null + } override fun getReadingStatus(): Long = READING @@ -106,7 +106,10 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { override fun getCompletionStatus(): Long = COMPLETED - override suspend fun login(username: String, password: String) = login(password) + override suspend fun login( + username: String, + password: String, + ) = login(password) suspend fun login(code: String) { try { @@ -122,13 +125,12 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { - return try { + fun restoreToken(): OAuth? = + try { json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } - } override fun logout() { super.logout() @@ -143,8 +145,9 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") { const val DROPPED = 5L const val PLAN_TO_READ = 1L - private val SCORE_LIST = IntRange(0, 10) - .map(Int::toString) - .toImmutableList() + private val SCORE_LIST = + IntRange(0, 10) + .map(Int::toString) + .toImmutableList() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index 65d2dc8058..d1283e3b9b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -31,56 +31,64 @@ class BangumiApi( private val client: OkHttpClient, interceptor: BangumiInterceptor, ) { - private val json: Json by injectLazy() private val authClient = client.newBuilder().addInterceptor(interceptor).build() - suspend fun addLibManga(track: Track): Track { - return withIOContext { - val body = FormBody.Builder() - .add("rating", track.score.toInt().toString()) - .add("status", track.toBangumiStatus()) - .build() - authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = body)) + suspend fun addLibManga(track: Track): Track = + withIOContext { + val body = + FormBody + .Builder() + .add("rating", track.score.toInt().toString()) + .add("status", track.toBangumiStatus()) + .build() + authClient + .newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = body)) .awaitSuccess() track } - } - suspend fun updateLibManga(track: Track): Track { - return withIOContext { + suspend fun updateLibManga(track: Track): Track = + withIOContext { // read status update - val sbody = FormBody.Builder() - .add("rating", track.score.toInt().toString()) - .add("status", track.toBangumiStatus()) - .build() - authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = sbody)) + val sbody = + FormBody + .Builder() + .add("rating", track.score.toInt().toString()) + .add("status", track.toBangumiStatus()) + .build() + authClient + .newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = sbody)) .awaitSuccess() // chapter update - val body = FormBody.Builder() - .add("watched_eps", track.last_chapter_read.toInt().toString()) - .build() - authClient.newCall( - POST( - "$apiUrl/subject/${track.remote_id}/update/watched_eps", - body = body, - ), - ).awaitSuccess() + val body = + FormBody + .Builder() + .add("watched_eps", track.last_chapter_read.toInt().toString()) + .build() + authClient + .newCall( + POST( + "$apiUrl/subject/${track.remote_id}/update/watched_eps", + body = body, + ), + ).awaitSuccess() track } - } - suspend fun search(search: String): List { - return withIOContext { - val url = "$apiUrl/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}" - .toUri() - .buildUpon() - .appendQueryParameter("max_results", "20") - .build() - authClient.newCall(GET(url.toString())) + suspend fun search(search: String): List = + withIOContext { + val url = + "$apiUrl/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}" + .toUri() + .buildUpon() + .appendQueryParameter("max_results", "20") + .build() + authClient + .newCall(GET(url.toString())) .awaitSuccess() .use { var responseBody = it.body.string() @@ -91,25 +99,37 @@ class BangumiApi( responseBody = "{\"results\":0,\"list\":[]}" } val response = json.decodeFromString(responseBody)["list"]?.jsonArray - response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 } - ?.map { jsonToSearch(it.jsonObject) }.orEmpty() + response + ?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 } + ?.map { jsonToSearch(it.jsonObject) } + .orEmpty() } } - } private fun jsonToSearch(obj: JsonObject): TrackSearch { - val coverUrl = if (obj["images"] is JsonObject) { - obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: "" - } else { - // Sometimes JsonNull - "" - } - val totalChapters = if (obj["eps_count"] != null) { - obj["eps_count"]!!.jsonPrimitive.long - } else { - 0 - } - val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.doubleOrNull ?: -1.0 + val coverUrl = + if (obj["images"] is JsonObject) { + obj["images"] + ?.jsonObject + ?.get("common") + ?.jsonPrimitive + ?.contentOrNull ?: "" + } else { + // Sometimes JsonNull + "" + } + val totalChapters = + if (obj["eps_count"] != null) { + obj["eps_count"]!!.jsonPrimitive.long + } else { + 0 + } + val rating = + obj["rating"] + ?.jsonObject + ?.get("score") + ?.jsonPrimitive + ?.doubleOrNull ?: -1.0 return TrackSearch.create(trackId).apply { remote_id = obj["id"]!!.jsonPrimitive.long title = obj["name_cn"]!!.jsonPrimitive.content @@ -121,25 +141,27 @@ class BangumiApi( } } - suspend fun findLibManga(track: Track): Track? { - return withIOContext { + suspend fun findLibManga(track: Track): Track? = + withIOContext { with(json) { - authClient.newCall(GET("$apiUrl/subject/${track.remote_id}")) + authClient + .newCall(GET("$apiUrl/subject/${track.remote_id}")) .awaitSuccess() .parseAs() .let { jsonToSearch(it) } } } - } - suspend fun statusLibManga(track: Track): Track? { - return withIOContext { + suspend fun statusLibManga(track: Track): Track? = + withIOContext { val urlUserRead = "$apiUrl/collection/${track.remote_id}" - val requestUserRead = Request.Builder() - .url(urlUserRead) - .cacheControl(CacheControl.FORCE_NETWORK) - .get() - .build() + val requestUserRead = + Request + .Builder() + .url(urlUserRead) + .cacheControl(CacheControl.FORCE_NETWORK) + .get() + .build() // TODO: get user readed chapter here val response = authClient.newCall(requestUserRead).awaitSuccess() @@ -158,28 +180,30 @@ class BangumiApi( } } } - } - suspend fun accessToken(code: String): OAuth { - return withIOContext { + suspend fun accessToken(code: String): OAuth = + withIOContext { with(json) { - client.newCall(accessTokenRequest(code)) + client + .newCall(accessTokenRequest(code)) .awaitSuccess() .parseAs() } } - } - private fun accessTokenRequest(code: String) = POST( - oauthUrl, - body = FormBody.Builder() - .add("grant_type", "authorization_code") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("code", code) - .add("redirect_uri", redirectUrl) - .build(), - ) + private fun accessTokenRequest(code: String) = + POST( + oauthUrl, + body = + FormBody + .Builder() + .add("grant_type", "authorization_code") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .add("code", code) + .add("redirect_uri", redirectUrl) + .build(), + ) companion object { private const val clientId = "bgm291665acbd06a4c28" @@ -192,21 +216,26 @@ class BangumiApi( private const val redirectUrl = "mihon://bangumi-auth" fun authUrl(): Uri = - loginUrl.toUri().buildUpon() + loginUrl + .toUri() + .buildUpon() .appendQueryParameter("client_id", clientId) .appendQueryParameter("response_type", "code") .appendQueryParameter("redirect_uri", redirectUrl) .build() - fun refreshTokenRequest(token: String) = POST( - oauthUrl, - body = FormBody.Builder() - .add("grant_type", "refresh_token") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("refresh_token", token) - .add("redirect_uri", redirectUrl) - .build(), - ) + fun refreshTokenRequest(token: String) = + POST( + oauthUrl, + body = + FormBody + .Builder() + .add("grant_type", "refresh_token") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .add("refresh_token", token) + .add("redirect_uri", redirectUrl) + .build(), + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt index a1822cca06..8070fafee3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt @@ -7,8 +7,9 @@ import okhttp3.Interceptor import okhttp3.Response import uy.kohesive.injekt.injectLazy -class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { - +class BangumiInterceptor( + private val bangumi: Bangumi, +) : Interceptor { private val json: Json by injectLazy() /** @@ -30,43 +31,48 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { } } - return originalRequest.newBuilder() + return originalRequest + .newBuilder() .header( "User-Agent", "antsylich/Mihon/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/mihonapp/mihon)", - ) - .apply { + ).apply { if (originalRequest.method == "GET") { - val newUrl = originalRequest.url.newBuilder() - .addQueryParameter("access_token", currAuth.access_token) - .build() + val newUrl = + originalRequest.url + .newBuilder() + .addQueryParameter("access_token", currAuth.access_token) + .build() url(newUrl) } else { post(addToken(currAuth.access_token, originalRequest.body as FormBody)) } - } - .build() + }.build() .let(chain::proceed) } fun newAuth(oauth: OAuth?) { - this.oauth = if (oauth == null) { - null - } else { - OAuth( - oauth.access_token, - oauth.token_type, - System.currentTimeMillis() / 1000, - oauth.expires_in, - oauth.refresh_token, - this.oauth?.user_id, - ) - } + this.oauth = + if (oauth == null) { + null + } else { + OAuth( + oauth.access_token, + oauth.token_type, + System.currentTimeMillis() / 1000, + oauth.expires_in, + oauth.refresh_token, + this.oauth?.user_id, + ) + } bangumi.saveToken(oauth) } - private fun addToken(token: String, oidFormBody: FormBody): FormBody { + private fun addToken( + token: String, + oidFormBody: FormBody, + ): FormBody { val newFormBody = FormBody.Builder() for (i in 0.. (created_at + expires_in - 3600) -fun Track.toBangumiStatus() = when (status) { - Bangumi.READING -> "do" - Bangumi.COMPLETED -> "collect" - Bangumi.ON_HOLD -> "on_hold" - Bangumi.DROPPED -> "dropped" - Bangumi.PLAN_TO_READ -> "wish" - else -> throw NotImplementedError("Unknown status: $status") -} +fun Track.toBangumiStatus() = + when (status) { + Bangumi.READING -> "do" + Bangumi.COMPLETED -> "collect" + Bangumi.ON_HOLD -> "on_hold" + Bangumi.DROPPED -> "dropped" + Bangumi.PLAN_TO_READ -> "wish" + else -> throw NotImplementedError("Unknown status: $status") + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt index a9aed629b0..0c444837ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt @@ -19,8 +19,10 @@ import uy.kohesive.injekt.injectLazy import java.security.MessageDigest import tachiyomi.domain.track.model.Track as DomainTrack -class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { - +class Kavita( + id: Long, +) : BaseTracker(id, "Kavita"), + EnhancedTracker { companion object { const val UNREAD = 1L const val READING = 2L @@ -40,12 +42,13 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { override fun getStatusList(): List = listOf(UNREAD, READING, COMPLETED) - override fun getStatus(status: Long): StringResource? = when (status) { - UNREAD -> MR.strings.unread - READING -> MR.strings.reading - COMPLETED -> MR.strings.completed - else -> null - } + override fun getStatus(status: Long): StringResource? = + when (status) { + UNREAD -> MR.strings.unread + READING -> MR.strings.reading + COMPLETED -> MR.strings.completed + else -> null + } override fun getReadingStatus(): Long = READING @@ -57,7 +60,10 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { override fun displayScore(track: DomainTrack): String = "" - override suspend fun update(track: Track, didReadChapter: Boolean): Track { + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { if (track.status != COMPLETED) { if (didReadChapter) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { @@ -70,9 +76,10 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { return api.updateProgress(track) } - override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { - return track - } + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track = track override suspend fun search(query: String): List { TODO("Not yet implemented: search") @@ -85,7 +92,10 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { return track } - override suspend fun login(username: String, password: String) { + override suspend fun login( + username: String, + password: String, + ) { saveCredentials("user", "pass") } @@ -104,10 +114,17 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { null } - override fun isTrackFrom(track: DomainTrack, manga: Manga, source: Source?): Boolean = - track.remoteUrl == manga.url && source?.let { accept(it) } == true - - override fun migrateTrack(track: DomainTrack, manga: Manga, newSource: Source): DomainTrack? = + override fun isTrackFrom( + track: DomainTrack, + manga: Manga, + source: Source?, + ): Boolean = track.remoteUrl == manga.url && source?.let { accept(it) } == true + + override fun migrateTrack( + track: DomainTrack, + manga: Manga, + newSource: Source, + ): DomainTrack? = if (accept(newSource)) { track.copy(remoteUrl = manga.url) } else { @@ -121,7 +138,8 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { val sourceId by lazy { val key = "kavita_$id/all/1" // Hardcoded versionID to 1 val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) - (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) } + (0..7) + .map { bytes[it].toLong() and 0xff shl 8 * (7 - it) } .reduce(Long::or) and Long.MAX_VALUE } val preferences = (sourceManager.get(sourceId) as ConfigurableSource).sourcePreferences() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt index fd1b26197f..d1d2392399 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt @@ -18,18 +18,20 @@ import uy.kohesive.injekt.injectLazy import java.io.IOException import java.net.SocketTimeoutException -class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) { - +class KavitaApi( + private val client: OkHttpClient, + interceptor: KavitaInterceptor, +) { private val json: Json by injectLazy() - private val authClient = client.newBuilder() - .dns(Dns.SYSTEM) - .addInterceptor(interceptor) - .build() + private val authClient = + client + .newBuilder() + .dns(Dns.SYSTEM) + .addInterceptor(interceptor) + .build() - fun getApiFromUrl(url: String): String { - return url.split("/api/").first() + "/api" - } + fun getApiFromUrl(url: String): String = url.split("/api/").first() + "/api" /* * Uses url to compare against each source APIURL's to get the correct custom source preference. @@ -37,11 +39,15 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor * Authenticates to get the token * Saves the token in the var jwtToken */ - fun getNewToken(apiUrl: String, apiKey: String): String? { - val request = POST( - "$apiUrl/Plugin/authenticate?apiKey=$apiKey&pluginName=Tachiyomi-Kavita", - body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()), - ) + fun getNewToken( + apiUrl: String, + apiKey: String, + ): String? { + val request = + POST( + "$apiUrl/Plugin/authenticate?apiKey=$apiKey&pluginName=Tachiyomi-Kavita", + body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()), + ) try { with(json) { client.newCall(request).execute().use { @@ -79,14 +85,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor return null } - private fun getApiVolumesUrl(url: String): String { - return "${getApiFromUrl(url)}/Series/volumes?seriesId=${getIdFromUrl(url)}" - } + private fun getApiVolumesUrl(url: String): String = + "${getApiFromUrl(url)}/Series/volumes?seriesId=${getIdFromUrl(url)}" - /* Strips serie id from URL */ - private fun getIdFromUrl(url: String): Int { - return url.substringAfterLast("/").toInt() - } + // Strips serie id from URL + private fun getIdFromUrl(url: String): Int = url.substringAfterLast("/").toInt() /* * Returns total chapters in the series. @@ -96,11 +99,13 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor private fun getTotalChapters(url: String): Long { val requestUrl = getApiVolumesUrl(url) try { - val listVolumeDto = with(json) { - authClient.newCall(GET(requestUrl)) - .execute() - .parseAs>() - } + val listVolumeDto = + with(json) { + authClient + .newCall(GET(requestUrl)) + .execute() + .parseAs>() + } var volumeNumber = 0L var maxChapterNumber = 0L for (volume in listVolumeDto) { @@ -125,7 +130,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor with(json) { authClient.newCall(GET(requestUrl)).execute().use { if (it.code == 200) { - return it.parseAs().number!!.replace(",", ".").toDouble() + return it + .parseAs() + .number!! + .replace(",", ".") + .toDouble() } if (it.code == 204) { return 0.0 @@ -142,33 +151,37 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor return 0.0 } - suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { - try { - val seriesDto: SeriesDto = with(json) { - authClient.newCall(GET(url)) - .awaitSuccess() - .parseAs() - } - - val track = seriesDto.toTrack() - track.apply { - cover_url = seriesDto.thumbnail_url.toString() - tracking_url = url - total_chapters = getTotalChapters(url) + suspend fun getTrackSearch(url: String): TrackSearch = + withIOContext { + try { + val seriesDto: SeriesDto = + with(json) { + authClient + .newCall(GET(url)) + .awaitSuccess() + .parseAs() + } - title = seriesDto.name - status = when (seriesDto.pagesRead) { - seriesDto.pages -> Kavita.COMPLETED - 0 -> Kavita.UNREAD - else -> Kavita.READING + val track = seriesDto.toTrack() + track.apply { + cover_url = seriesDto.thumbnail_url.toString() + tracking_url = url + total_chapters = getTotalChapters(url) + + title = seriesDto.name + status = + when (seriesDto.pagesRead) { + seriesDto.pages -> Kavita.COMPLETED + 0 -> Kavita.UNREAD + else -> Kavita.READING + } + last_chapter_read = getLatestChapterRead(url) } - last_chapter_read = getLatestChapterRead(url) + } catch (e: Exception) { + logcat(LogPriority.WARN, e) { "Could not get item: $url" } + throw e } - } catch (e: Exception) { - logcat(LogPriority.WARN, e) { "Could not get item: $url" } - throw e } - } suspend fun updateProgress(track: Track): Track { val requestUrl = "${getApiFromUrl( @@ -176,10 +189,10 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor )}/Tachiyomi/mark-chapter-until-as-read?seriesId=${getIdFromUrl( track.tracking_url, )}&chapterNumber=${track.last_chapter_read}" - authClient.newCall( - POST(requestUrl, body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())), - ) - .awaitSuccess() + authClient + .newCall( + POST(requestUrl, body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())), + ).awaitSuccess() return getTrackSearch(track.tracking_url) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaInterceptor.kt index 89708ccc58..56fd0944eb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaInterceptor.kt @@ -4,22 +4,26 @@ import eu.kanade.tachiyomi.BuildConfig import okhttp3.Interceptor import okhttp3.Response -class KavitaInterceptor(private val kavita: Kavita) : Interceptor { - +class KavitaInterceptor( + private val kavita: Kavita, +) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() if (kavita.authentications == null) { kavita.loadOAuth() } - val jwtToken = kavita.authentications?.getToken( - kavita.api.getApiFromUrl(originalRequest.url.toString()), - ) + val jwtToken = + kavita.authentications?.getToken( + kavita.api.getApiFromUrl(originalRequest.url.toString()), + ) // Add the authorization header to the original request. - val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer $jwtToken") - .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") - .build() + val authRequest = + originalRequest + .newBuilder() + .addHeader("Authorization", "Bearer $jwtToken") + .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") + .build() return chain.proceed(authRequest) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt index 6f42f68364..30986fd97a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt @@ -22,10 +22,11 @@ data class SeriesDto( val libraryId: Int, val libraryName: String? = "", ) { - fun toTrack(): TrackSearch = TrackSearch.create(TrackerManager.KAVITA).also { - it.title = name - it.summary = "" - } + fun toTrack(): TrackSearch = + TrackSearch.create(TrackerManager.KAVITA).also { + it.title = name + it.summary = "" + } } @Serializable @@ -63,11 +64,12 @@ data class AuthenticationDto( ) class OAuth( - val authentications: List = listOf( - SourceAuth(1), - SourceAuth(2), - SourceAuth(3), - ), + val authentications: List = + listOf( + SourceAuth(1), + SourceAuth(2), + SourceAuth(3), + ), ) { fun getToken(apiUrl: String): String? { for (authentication in authentications) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index 4b0db8bce9..7af0e410a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -16,8 +16,10 @@ import uy.kohesive.injekt.injectLazy import java.text.DecimalFormat import tachiyomi.domain.track.model.Track as DomainTrack -class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { - +class Kitsu( + id: Long, +) : BaseTracker(id, "Kitsu"), + DeletableTracker { companion object { const val READING = 1L const val COMPLETED = 2L @@ -38,18 +40,17 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { override fun getLogoColor() = Color.rgb(51, 37, 50) - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) - } + override fun getStatusList(): List = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) - override fun getStatus(status: Long): StringResource? = when (status) { - READING -> MR.strings.reading - PLAN_TO_READ -> MR.strings.plan_to_read - COMPLETED -> MR.strings.completed - ON_HOLD -> MR.strings.on_hold - DROPPED -> MR.strings.dropped - else -> null - } + override fun getStatus(status: Long): StringResource? = + when (status) { + READING -> MR.strings.reading + PLAN_TO_READ -> MR.strings.plan_to_read + COMPLETED -> MR.strings.completed + ON_HOLD -> MR.strings.on_hold + DROPPED -> MR.strings.dropped + else -> null + } override fun getReadingStatus(): Long = READING @@ -62,20 +63,19 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { return (listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }).toImmutableList() } - override fun indexToScore(index: Int): Double { - return if (index > 0) (index + 1) / 2.0 else 0.0 - } + override fun indexToScore(index: Int): Double = if (index > 0) (index + 1) / 2.0 else 0.0 override fun displayScore(track: DomainTrack): String { val df = DecimalFormat("0.#") return df.format(track.score) } - private suspend fun add(track: Track): Track { - return api.addLibManga(track, getUserId()) - } + private suspend fun add(track: Track): Track = api.addLibManga(track, getUserId()) - override suspend fun update(track: Track, didReadChapter: Boolean): Track { + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { if (track.status != COMPLETED) { if (didReadChapter) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { @@ -97,7 +97,10 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { api.removeLibManga(track) } - override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track { val remoteTrack = api.findLibManga(track, getUserId()) return if (remoteTrack != null) { track.copyPersonalFrom(remoteTrack) @@ -115,9 +118,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { } } - override suspend fun search(query: String): List { - return api.search(query) - } + override suspend fun search(query: String): List = api.search(query) override suspend fun refresh(track: Track): Track { val remoteTrack = api.getLibManga(track) @@ -126,7 +127,10 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { return track } - override suspend fun login(username: String, password: String) { + override suspend fun login( + username: String, + password: String, + ) { val token = api.login(username, password) interceptor.newAuth(token) val userId = api.getCurrentUser() @@ -138,19 +142,16 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { interceptor.newAuth(null) } - private fun getUserId(): String { - return getPassword() - } + private fun getUserId(): String = getPassword() fun saveToken(oauth: OAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { - return try { + fun restoreToken(): OAuth? = + try { json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 3c9251eafc..31fb3dc1e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -31,51 +31,60 @@ import java.net.URLEncoder import java.nio.charset.StandardCharsets import tachiyomi.domain.track.model.Track as DomainTrack -class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) { - +class KitsuApi( + private val client: OkHttpClient, + interceptor: KitsuInterceptor, +) { private val json: Json by injectLazy() private val authClient = client.newBuilder().addInterceptor(interceptor).build() - suspend fun addLibManga(track: Track, userId: String): Track { - return withIOContext { - val data = buildJsonObject { - putJsonObject("data") { - put("type", "libraryEntries") - putJsonObject("attributes") { - put("status", track.toKitsuStatus()) - put("progress", track.last_chapter_read.toInt()) - } - putJsonObject("relationships") { - putJsonObject("user") { - putJsonObject("data") { - put("id", userId) - put("type", "users") - } + suspend fun addLibManga( + track: Track, + userId: String, + ): Track = + withIOContext { + val data = + buildJsonObject { + putJsonObject("data") { + put("type", "libraryEntries") + putJsonObject("attributes") { + put("status", track.toKitsuStatus()) + put("progress", track.last_chapter_read.toInt()) } - putJsonObject("media") { - putJsonObject("data") { - put("id", track.remote_id) - put("type", "manga") + putJsonObject("relationships") { + putJsonObject("user") { + putJsonObject("data") { + put("id", userId) + put("type", "users") + } + } + putJsonObject("media") { + putJsonObject("data") { + put("id", track.remote_id) + put("type", "manga") + } } } } } - } with(json) { - authClient.newCall( - POST( - "${baseUrl}library-entries", - headers = headersOf( - "Content-Type", - "application/vnd.api+json", + authClient + .newCall( + POST( + "${baseUrl}library-entries", + headers = + headersOf( + "Content-Type", + "application/vnd.api+json", + ), + body = + data + .toString() + .toRequestBody("application/vnd.api+json".toMediaType()), ), - body = data.toString() - .toRequestBody("application/vnd.api+json".toMediaType()), - ), - ) - .awaitSuccess() + ).awaitSuccess() .parseAs() .let { track.remote_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long @@ -83,47 +92,45 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } } } - } - suspend fun updateLibManga(track: Track): Track { - return withIOContext { - val data = buildJsonObject { - putJsonObject("data") { - put("type", "libraryEntries") - put("id", track.remote_id) - putJsonObject("attributes") { - put("status", track.toKitsuStatus()) - put("progress", track.last_chapter_read.toInt()) - put("ratingTwenty", track.toKitsuScore()) - put("startedAt", KitsuDateHelper.convert(track.started_reading_date)) - put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date)) + suspend fun updateLibManga(track: Track): Track = + withIOContext { + val data = + buildJsonObject { + putJsonObject("data") { + put("type", "libraryEntries") + put("id", track.remote_id) + putJsonObject("attributes") { + put("status", track.toKitsuStatus()) + put("progress", track.last_chapter_read.toInt()) + put("ratingTwenty", track.toKitsuScore()) + put("startedAt", KitsuDateHelper.convert(track.started_reading_date)) + put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date)) + } } } - } with(json) { - authClient.newCall( - Request.Builder() - .url("${baseUrl}library-entries/${track.remote_id}") - .headers( - headersOf( - "Content-Type", - "application/vnd.api+json", - ), - ) - .patch( - data.toString().toRequestBody("application/vnd.api+json".toMediaType()), - ) - .build(), - ) - .awaitSuccess() + authClient + .newCall( + Request + .Builder() + .url("${baseUrl}library-entries/${track.remote_id}") + .headers( + headersOf( + "Content-Type", + "application/vnd.api+json", + ), + ).patch( + data.toString().toRequestBody("application/vnd.api+json".toMediaType()), + ).build(), + ).awaitSuccess() .parseAs() .let { track } } } - } suspend fun removeLibManga(track: DomainTrack) { withIOContext { @@ -131,19 +138,21 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .newCall( DELETE( "${baseUrl}library-entries/${track.remoteId}", - headers = headersOf( - "Content-Type", - "application/vnd.api+json", - ), + headers = + headersOf( + "Content-Type", + "application/vnd.api+json", + ), ), - ) - .awaitSuccess() + ).awaitSuccess() } } - suspend fun search(query: String): List { - return withIOContext { + + suspend fun search(query: String): List = + withIOContext { with(json) { - authClient.newCall(GET(algoliaKeyUrl)) + authClient + .newCall(GET(algoliaKeyUrl)) .awaitSuccess() .parseAs() .let { @@ -152,47 +161,58 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } } } - } - private suspend fun algoliaSearch(key: String, query: String): List { - return withIOContext { - val jsonObject = buildJsonObject { - put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter") - } + private suspend fun algoliaSearch( + key: String, + query: String, + ): List = + withIOContext { + val jsonObject = + buildJsonObject { + put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter") + } with(json) { - client.newCall( - POST( - algoliaUrl, - headers = headersOf( - "X-Algolia-Application-Id", - algoliaAppId, - "X-Algolia-API-Key", - key, + client + .newCall( + POST( + algoliaUrl, + headers = + headersOf( + "X-Algolia-Application-Id", + algoliaAppId, + "X-Algolia-API-Key", + key, + ), + body = jsonObject.toString().toRequestBody(jsonMime), ), - body = jsonObject.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() + ).awaitSuccess() .parseAs() .let { - it["hits"]!!.jsonArray + it["hits"]!! + .jsonArray .map { KitsuSearchManga(it.jsonObject) } .filter { it.subType != "novel" } .map { it.toTrack() } } } } - } - suspend fun findLibManga(track: Track, userId: String): Track? { - return withIOContext { - val url = "${baseUrl}library-entries".toUri().buildUpon() - .encodedQuery("filter[manga_id]=${track.remote_id}&filter[user_id]=$userId") - .appendQueryParameter("include", "manga") - .build() + suspend fun findLibManga( + track: Track, + userId: String, + ): Track? = + withIOContext { + val url = + "${baseUrl}library-entries" + .toUri() + .buildUpon() + .encodedQuery("filter[manga_id]=${track.remote_id}&filter[user_id]=$userId") + .appendQueryParameter("include", "manga") + .build() with(json) { - authClient.newCall(GET(url.toString())) + authClient + .newCall(GET(url.toString())) .awaitSuccess() .parseAs() .let { @@ -206,16 +226,19 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } } } - } - suspend fun getLibManga(track: Track): Track { - return withIOContext { - val url = "${baseUrl}library-entries".toUri().buildUpon() - .encodedQuery("filter[id]=${track.remote_id}") - .appendQueryParameter("include", "manga") - .build() + suspend fun getLibManga(track: Track): Track = + withIOContext { + val url = + "${baseUrl}library-entries" + .toUri() + .buildUpon() + .encodedQuery("filter[id]=${track.remote_id}") + .appendQueryParameter("include", "manga") + .build() with(json) { - authClient.newCall(GET(url.toString())) + authClient + .newCall(GET(url.toString())) .awaitSuccess() .parseAs() .let { @@ -229,40 +252,50 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) } } } - } - suspend fun login(username: String, password: String): OAuth { - return withIOContext { - val formBody: RequestBody = FormBody.Builder() - .add("username", username) - .add("password", password) - .add("grant_type", "password") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .build() + suspend fun login( + username: String, + password: String, + ): OAuth = + withIOContext { + val formBody: RequestBody = + FormBody + .Builder() + .add("username", username) + .add("password", password) + .add("grant_type", "password") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .build() with(json) { - client.newCall(POST(loginUrl, body = formBody)) + client + .newCall(POST(loginUrl, body = formBody)) .awaitSuccess() .parseAs() } } - } - suspend fun getCurrentUser(): String { - return withIOContext { - val url = "${baseUrl}users".toUri().buildUpon() - .encodedQuery("filter[self]=true") - .build() + suspend fun getCurrentUser(): String = + withIOContext { + val url = + "${baseUrl}users" + .toUri() + .buildUpon() + .encodedQuery("filter[self]=true") + .build() with(json) { - authClient.newCall(GET(url.toString())) + authClient + .newCall(GET(url.toString())) .awaitSuccess() .parseAs() .let { - it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content + it["data"]!! + .jsonArray[0] + .jsonObject["id"]!! + .jsonPrimitive.content } } } - } companion object { private const val clientId = @@ -283,18 +316,19 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) "%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" + "posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" - fun mangaUrl(remoteId: Long): String { - return baseMangaUrl + remoteId - } + fun mangaUrl(remoteId: Long): String = baseMangaUrl + remoteId - fun refreshTokenRequest(token: String) = POST( - loginUrl, - body = FormBody.Builder() - .add("grant_type", "refresh_token") - .add("refresh_token", token) - .add("client_id", clientId) - .add("client_secret", clientSecret) - .build(), - ) + fun refreshTokenRequest(token: String) = + POST( + loginUrl, + body = + FormBody + .Builder() + .add("grant_type", "refresh_token") + .add("refresh_token", token) + .add("client_id", clientId) + .add("client_secret", clientSecret) + .build(), + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuDateHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuDateHelper.kt index 6828e1e1a9..9f9e7d42d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuDateHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuDateHelper.kt @@ -5,7 +5,6 @@ import java.util.Date import java.util.Locale object KitsuDateHelper { - private const val pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" private val formatter = SimpleDateFormat(pattern, Locale.ENGLISH) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt index da9aff7fc4..7f16b0020e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt @@ -6,8 +6,9 @@ import okhttp3.Interceptor import okhttp3.Response import uy.kohesive.injekt.injectLazy -class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor { - +class KitsuInterceptor( + private val kitsu: Kitsu, +) : Interceptor { private val json: Json by injectLazy() /** @@ -33,12 +34,14 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor { } // Add the authorization header to the original request. - val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") - .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") - .header("Accept", "application/vnd.api+json") - .header("Content-Type", "application/vnd.api+json") - .build() + val authRequest = + originalRequest + .newBuilder() + .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") + .header("Accept", "application/vnd.api+json") + .header("Content-Type", "application/vnd.api+json") + .build() return chain.proceed(authRequest) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt index 752ef4e125..ea2a8b57d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt @@ -16,52 +16,79 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -class KitsuSearchManga(obj: JsonObject) { +class KitsuSearchManga( + obj: JsonObject, +) { val id = obj["id"]!!.jsonPrimitive.long private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.longOrNull val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull - val original = try { - obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content - } catch (e: IllegalArgumentException) { - // posterImage is sometimes a jsonNull object instead - null - } + val original = + try { + obj["posterImage"] + ?.jsonObject + ?.get("original") + ?.jsonPrimitive + ?.content + } catch (e: IllegalArgumentException) { + // posterImage is sometimes a jsonNull object instead + null + } private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull private val rating = obj["averageRating"]?.jsonPrimitive?.contentOrNull?.toDoubleOrNull() - private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(Date(it.toLong() * 1000)) - } + private var startDate = + obj["startDate"]?.jsonPrimitive?.contentOrNull?.let { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(Date(it.toLong() * 1000)) + } private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull @CallSuper - fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply { - remote_id = this@KitsuSearchManga.id - title = canonicalTitle - total_chapters = chapterCount ?: 0 - cover_url = original ?: "" - summary = synopsis ?: "" - tracking_url = KitsuApi.mangaUrl(remote_id) - score = rating ?: -1.0 - publishing_status = if (endDate == null) { - "Publishing" - } else { - "Finished" + fun toTrack() = + TrackSearch.create(TrackerManager.KITSU).apply { + remote_id = this@KitsuSearchManga.id + title = canonicalTitle + total_chapters = chapterCount ?: 0 + cover_url = original ?: "" + summary = synopsis ?: "" + tracking_url = KitsuApi.mangaUrl(remote_id) + score = rating ?: -1.0 + publishing_status = + if (endDate == null) { + "Publishing" + } else { + "Finished" + } + publishing_type = subType ?: "" + start_date = startDate ?: "" } - publishing_type = subType ?: "" - start_date = startDate ?: "" - } } -class KitsuLibManga(obj: JsonObject, manga: JsonObject) { +class KitsuLibManga( + obj: JsonObject, + manga: JsonObject, +) { val id = manga["id"]!!.jsonPrimitive.int private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.longOrNull - val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty() - val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content + val type = + manga["attributes"]!! + .jsonObject["mangaType"] + ?.jsonPrimitive + ?.contentOrNull + .orEmpty() + val original = + manga["attributes"]!! + .jsonObject["posterImage"]!! + .jsonObject["original"]!! + .jsonPrimitive.content private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content - private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty() + private val startDate = + manga["attributes"]!! + .jsonObject["startDate"] + ?.jsonPrimitive + ?.contentOrNull + .orEmpty() private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull private val libraryId = obj["id"]!!.jsonPrimitive.long @@ -69,31 +96,33 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) { private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int - fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply { - remote_id = libraryId - title = canonicalTitle - total_chapters = chapterCount ?: 0 - cover_url = original - summary = synopsis - tracking_url = KitsuApi.mangaUrl(remote_id) - publishing_status = this@KitsuLibManga.status - publishing_type = type - start_date = startDate - started_reading_date = KitsuDateHelper.parse(startedAt) - finished_reading_date = KitsuDateHelper.parse(finishedAt) - status = toTrackStatus() - score = ratingTwenty?.let { it.toInt() / 2.0 } ?: 0.0 - last_chapter_read = progress.toDouble() - } + fun toTrack() = + TrackSearch.create(TrackerManager.KITSU).apply { + remote_id = libraryId + title = canonicalTitle + total_chapters = chapterCount ?: 0 + cover_url = original + summary = synopsis + tracking_url = KitsuApi.mangaUrl(remote_id) + publishing_status = this@KitsuLibManga.status + publishing_type = type + start_date = startDate + started_reading_date = KitsuDateHelper.parse(startedAt) + finished_reading_date = KitsuDateHelper.parse(finishedAt) + status = toTrackStatus() + score = ratingTwenty?.let { it.toInt() / 2.0 } ?: 0.0 + last_chapter_read = progress.toDouble() + } - private fun toTrackStatus() = when (status) { - "current" -> Kitsu.READING - "completed" -> Kitsu.COMPLETED - "on_hold" -> Kitsu.ON_HOLD - "dropped" -> Kitsu.DROPPED - "planned" -> Kitsu.PLAN_TO_READ - else -> throw Exception("Unknown status") - } + private fun toTrackStatus() = + when (status) { + "current" -> Kitsu.READING + "completed" -> Kitsu.COMPLETED + "on_hold" -> Kitsu.ON_HOLD + "dropped" -> Kitsu.DROPPED + "planned" -> Kitsu.PLAN_TO_READ + else -> throw Exception("Unknown status") + } } @Serializable @@ -107,15 +136,14 @@ data class OAuth( fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) -fun Track.toKitsuStatus() = when (status) { - Kitsu.READING -> "current" - Kitsu.COMPLETED -> "completed" - Kitsu.ON_HOLD -> "on_hold" - Kitsu.DROPPED -> "dropped" - Kitsu.PLAN_TO_READ -> "planned" - else -> throw Exception("Unknown status") -} +fun Track.toKitsuStatus() = + when (status) { + Kitsu.READING -> "current" + Kitsu.COMPLETED -> "completed" + Kitsu.ON_HOLD -> "on_hold" + Kitsu.DROPPED -> "dropped" + Kitsu.PLAN_TO_READ -> "planned" + else -> throw Exception("Unknown status") + } -fun Track.toKitsuScore(): String? { - return if (score > 0) (score * 2).toInt().toString() else null -} +fun Track.toKitsuScore(): String? = if (score > 0) (score * 2).toInt().toString() else null diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt index eee8941a3c..7f87ac77ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt @@ -16,8 +16,10 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR import tachiyomi.domain.track.model.Track as DomainTrack -class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker { - +class Komga( + id: Long, +) : BaseTracker(id, "Komga"), + EnhancedTracker { companion object { const val UNREAD = 1L const val READING = 2L @@ -25,7 +27,8 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker { } override val client: OkHttpClient = - networkService.client.newBuilder() + networkService.client + .newBuilder() .dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing .build() @@ -37,12 +40,13 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker { override fun getStatusList(): List = listOf(UNREAD, READING, COMPLETED) - override fun getStatus(status: Long): StringResource? = when (status) { - UNREAD -> MR.strings.unread - READING -> MR.strings.reading - COMPLETED -> MR.strings.completed - else -> null - } + override fun getStatus(status: Long): StringResource? = + when (status) { + UNREAD -> MR.strings.unread + READING -> MR.strings.reading + COMPLETED -> MR.strings.completed + else -> null + } override fun getReadingStatus(): Long = READING @@ -54,7 +58,10 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker { override fun displayScore(track: DomainTrack): String = "" - override suspend fun update(track: Track, didReadChapter: Boolean): Track { + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { if (track.status != COMPLETED) { if (didReadChapter) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { @@ -68,9 +75,10 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker { return api.updateProgress(track) } - override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { - return track - } + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track = track override suspend fun search(query: String): List { TODO("Not yet implemented: search") @@ -83,7 +91,10 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker { return track } - override suspend fun login(username: String, password: String) { + override suspend fun login( + username: String, + password: String, + ) { saveCredentials("user", "pass") } @@ -102,10 +113,17 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker { null } - override fun isTrackFrom(track: DomainTrack, manga: Manga, source: Source?): Boolean = - track.remoteUrl == manga.url && source?.let { accept(it) } == true - - override fun migrateTrack(track: DomainTrack, manga: Manga, newSource: Source): DomainTrack? = + override fun isTrackFrom( + track: DomainTrack, + manga: Manga, + source: Source?, + ): Boolean = track.remoteUrl == manga.url && source?.let { accept(it) } == true + + override fun migrateTrack( + track: DomainTrack, + manga: Manga, + newSource: Source, + ): DomainTrack? = if (accept(newSource)) { track.copy(remoteUrl = manga.url) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt index a8661bf188..5374d91b7a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt @@ -24,9 +24,9 @@ class KomgaApi( private val trackId: Long, private val client: OkHttpClient, ) { - private val headers: Headers by lazy { - Headers.Builder() + Headers + .Builder() .add("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() } @@ -36,43 +36,51 @@ class KomgaApi( suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { try { - val track = with(json) { - if (url.contains(READLIST_API)) { - client.newCall(GET(url, headers)) - .awaitSuccess() - .parseAs() - .toTrack() - } else { - client.newCall(GET(url, headers)) - .awaitSuccess() - .parseAs() - .toTrack() + val track = + with(json) { + if (url.contains(READLIST_API)) { + client + .newCall(GET(url, headers)) + .awaitSuccess() + .parseAs() + .toTrack() + } else { + client + .newCall(GET(url, headers)) + .awaitSuccess() + .parseAs() + .toTrack() + } } - } - val progress = client - .newCall( - GET("${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi", headers), - ) - .awaitSuccess().let { - with(json) { - if (url.contains("/api/v1/series/")) { - it.parseAs() - } else { - it.parseAs().toV2() + val progress = + client + .newCall( + GET( + "${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi", + headers, + ), + ).awaitSuccess() + .let { + with(json) { + if (url.contains("/api/v1/series/")) { + it.parseAs() + } else { + it.parseAs().toV2() + } } } - } track.apply { cover_url = "$url/thumbnail" tracking_url = url total_chapters = progress.maxNumberSort.toLong() - status = when (progress.booksCount) { - progress.booksUnreadCount -> Komga.UNREAD - progress.booksReadCount -> Komga.COMPLETED - else -> Komga.READING - } + status = + when (progress.booksCount) { + progress.booksUnreadCount -> Komga.UNREAD + progress.booksReadCount -> Komga.COMPLETED + else -> Komga.READING + } last_chapter_read = progress.lastReadContinuousNumberSort } } catch (e: Exception) { @@ -82,29 +90,33 @@ class KomgaApi( } suspend fun updateProgress(track: Track): Track { - val payload = if (track.tracking_url.contains("/api/v1/series/")) { - json.encodeToString(ReadProgressUpdateV2Dto(track.last_chapter_read)) - } else { - json.encodeToString(ReadProgressUpdateDto(track.last_chapter_read.toInt())) - } - client.newCall( - Request.Builder() - .url("${track.tracking_url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi") - .headers(headers) - .put(payload.toRequestBody("application/json".toMediaType())) - .build(), - ) - .awaitSuccess() + val payload = + if (track.tracking_url.contains("/api/v1/series/")) { + json.encodeToString(ReadProgressUpdateV2Dto(track.last_chapter_read)) + } else { + json.encodeToString(ReadProgressUpdateDto(track.last_chapter_read.toInt())) + } + client + .newCall( + Request + .Builder() + .url("${track.tracking_url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi") + .headers(headers) + .put(payload.toRequestBody("application/json".toMediaType())) + .build(), + ).awaitSuccess() return getTrackSearch(track.tracking_url) } - private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(trackId).also { - it.title = metadata.title - it.summary = metadata.summary - it.publishing_status = metadata.status - } + private fun SeriesDto.toTrack(): TrackSearch = + TrackSearch.create(trackId).also { + it.title = metadata.title + it.summary = metadata.summary + it.publishing_status = metadata.status + } - private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(trackId).also { - it.title = name - } + private fun ReadListDto.toTrack(): TrackSearch = + TrackSearch.create(trackId).also { + it.title = name + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaModels.kt index 6d1601ac01..be1a869b82 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaModels.kt @@ -47,7 +47,6 @@ data class BookMetadataAggregationDto( val releaseDate: String?, val summary: String, val summaryNumber: String, - val created: String, val lastModified: String, ) @@ -86,14 +85,15 @@ data class ReadProgressDto( val booksInProgressCount: Int, val lastReadContinuousIndex: Int, ) { - fun toV2() = ReadProgressV2Dto( - booksCount, - booksReadCount, - booksUnreadCount, - booksInProgressCount, - lastReadContinuousIndex.toDouble(), - booksCount.toFloat(), - ) + fun toV2() = + ReadProgressV2Dto( + booksCount, + booksReadCount, + booksUnreadCount, + booksInProgressCount, + lastReadContinuousIndex.toDouble(), + booksCount.toFloat(), + ) } @Serializable diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt index 6219e728b6..3e109feaac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt @@ -16,8 +16,10 @@ import kotlinx.collections.immutable.toImmutableList import tachiyomi.i18n.MR import tachiyomi.domain.track.model.Track as DomainTrack -class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker { - +class MangaUpdates( + id: Long, +) : BaseTracker(id, "MangaUpdates"), + DeletableTracker { companion object { const val READING_LIST = 0L const val WISH_LIST = 1L @@ -25,17 +27,18 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker const val UNFINISHED_LIST = 3L const val ON_HOLD_LIST = 4L - private val SCORE_LIST = (0..10) - .flatMap { decimal -> - when (decimal) { - 0 -> listOf("-") - 10 -> listOf("10.0") - else -> (0..9).map { fraction -> - "$decimal.$fraction" + private val SCORE_LIST = + (0..10) + .flatMap { decimal -> + when (decimal) { + 0 -> listOf("-") + 10 -> listOf("10.0") + else -> + (0..9).map { fraction -> + "$decimal.$fraction" + } } - } - } - .toImmutableList() + }.toImmutableList() } private val interceptor by lazy { MangaUpdatesInterceptor(this) } @@ -46,18 +49,18 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker override fun getLogoColor(): Int = Color.rgb(146, 160, 173) - override fun getStatusList(): List { - return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST) - } - - override fun getStatus(status: Long): StringResource? = when (status) { - READING_LIST -> MR.strings.reading_list - WISH_LIST -> MR.strings.wish_list - COMPLETE_LIST -> MR.strings.complete_list - ON_HOLD_LIST -> MR.strings.on_hold_list - UNFINISHED_LIST -> MR.strings.unfinished_list - else -> null - } + override fun getStatusList(): List = + listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST) + + override fun getStatus(status: Long): StringResource? = + when (status) { + READING_LIST -> MR.strings.reading_list + WISH_LIST -> MR.strings.wish_list + COMPLETE_LIST -> MR.strings.complete_list + ON_HOLD_LIST -> MR.strings.on_hold_list + UNFINISHED_LIST -> MR.strings.unfinished_list + else -> null + } override fun getReadingStatus(): Long = READING_LIST @@ -71,7 +74,10 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker override fun displayScore(track: DomainTrack): String = track.score.toString() - override suspend fun update(track: Track, didReadChapter: Boolean): Track { + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { if (track.status != COMPLETE_LIST && didReadChapter) { track.status = READING_LIST } @@ -83,8 +89,11 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker api.deleteSeriesFromList(track) } - override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { - return try { + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track = + try { val (series, rating) = api.getSeriesListItem(track) track.copyFrom(series, rating) } catch (e: Exception) { @@ -92,32 +101,36 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker api.addSeriesToList(track, hasReadChapters) track } - } - override suspend fun search(query: String): List { - return api.search(query) + override suspend fun search(query: String): List = + api + .search(query) .map { it.toTrackSearch(id) } - } override suspend fun refresh(track: Track): Track { val (series, rating) = api.getSeriesListItem(track) return track.copyFrom(series, rating) } - private fun Track.copyFrom(item: ListItem, rating: Rating?): Track = apply { - item.copyTo(this) - score = rating?.rating ?: 0.0 - } + private fun Track.copyFrom( + item: ListItem, + rating: Rating?, + ): Track = + apply { + item.copyTo(this) + score = rating?.rating ?: 0.0 + } - override suspend fun login(username: String, password: String) { + override suspend fun login( + username: String, + password: String, + ) { val authenticated = api.authenticate(username, password) ?: throw Throwable("Unable to login") saveCredentials(authenticated.uid.toString(), authenticated.sessionToken) interceptor.newAuth(authenticated.sessionToken) } - fun restoreSession(): String? { - return trackPreferences.trackPassword(this).get().ifBlank { null } - } + fun restoreSession(): String? = trackPreferences.trackPassword(this).get().ifBlank { null } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt index 5da3b72224..889ba90c07 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt @@ -42,40 +42,47 @@ class MangaUpdatesApi( private val contentType = "application/vnd.api+json".toMediaType() private val authClient by lazy { - client.newBuilder() + client + .newBuilder() .addInterceptor(interceptor) .build() } suspend fun getSeriesListItem(track: Track): Pair { - val listItem = with(json) { - authClient.newCall(GET("$baseUrl/v1/lists/series/${track.remote_id}")) - .awaitSuccess() - .parseAs() - } + val listItem = + with(json) { + authClient + .newCall(GET("$baseUrl/v1/lists/series/${track.remote_id}")) + .awaitSuccess() + .parseAs() + } val rating = getSeriesRating(track) return listItem to rating } - suspend fun addSeriesToList(track: Track, hasReadChapters: Boolean) { + suspend fun addSeriesToList( + track: Track, + hasReadChapters: Boolean, + ) { val status = if (hasReadChapters) READING_LIST else WISH_LIST - val body = buildJsonArray { - addJsonObject { - putJsonObject("series") { - put("id", track.remote_id) + val body = + buildJsonArray { + addJsonObject { + putJsonObject("series") { + put("id", track.remote_id) + } + put("list_id", status) } - put("list_id", status) } - } - authClient.newCall( - POST( - url = "$baseUrl/v1/lists/series", - body = body.toString().toRequestBody(contentType), - ), - ) - .awaitSuccess() + authClient + .newCall( + POST( + url = "$baseUrl/v1/lists/series", + body = body.toString().toRequestBody(contentType), + ), + ).awaitSuccess() .let { if (it.code == 200) { track.status = status @@ -85,118 +92,125 @@ class MangaUpdatesApi( } suspend fun updateSeriesListItem(track: Track) { - val body = buildJsonArray { - addJsonObject { - putJsonObject("series") { - put("id", track.remote_id) - } - put("list_id", track.status) - putJsonObject("status") { - put("chapter", track.last_chapter_read.toInt()) + val body = + buildJsonArray { + addJsonObject { + putJsonObject("series") { + put("id", track.remote_id) + } + put("list_id", track.status) + putJsonObject("status") { + put("chapter", track.last_chapter_read.toInt()) + } } } - } - authClient.newCall( - POST( - url = "$baseUrl/v1/lists/series/update", - body = body.toString().toRequestBody(contentType), - ), - ) - .awaitSuccess() + authClient + .newCall( + POST( + url = "$baseUrl/v1/lists/series/update", + body = body.toString().toRequestBody(contentType), + ), + ).awaitSuccess() updateSeriesRating(track) } suspend fun deleteSeriesFromList(track: DomainTrack) { - val body = buildJsonArray { - add(track.remoteId) - } - authClient.newCall( - POST( - url = "$baseUrl/v1/lists/series/delete", - body = body.toString().toRequestBody(contentType), - ), - ) - .awaitSuccess() + val body = + buildJsonArray { + add(track.remoteId) + } + authClient + .newCall( + POST( + url = "$baseUrl/v1/lists/series/delete", + body = body.toString().toRequestBody(contentType), + ), + ).awaitSuccess() } - private suspend fun getSeriesRating(track: Track): Rating? { - return try { + private suspend fun getSeriesRating(track: Track): Rating? = + try { with(json) { - authClient.newCall(GET("$baseUrl/v1/series/${track.remote_id}/rating")) + authClient + .newCall(GET("$baseUrl/v1/series/${track.remote_id}/rating")) .awaitSuccess() .parseAs() } } catch (e: Exception) { null } - } private suspend fun updateSeriesRating(track: Track) { if (track.score < 0.0) return if (track.score != 0.0) { - val body = buildJsonObject { - put("rating", track.score) - } - authClient.newCall( - PUT( - url = "$baseUrl/v1/series/${track.remote_id}/rating", - body = body.toString().toRequestBody(contentType), - ), - ) - .awaitSuccess() + val body = + buildJsonObject { + put("rating", track.score) + } + authClient + .newCall( + PUT( + url = "$baseUrl/v1/series/${track.remote_id}/rating", + body = body.toString().toRequestBody(contentType), + ), + ).awaitSuccess() } else { - authClient.newCall( - DELETE( - url = "$baseUrl/v1/series/${track.remote_id}/rating", - ), - ) - .awaitSuccess() + authClient + .newCall( + DELETE( + url = "$baseUrl/v1/series/${track.remote_id}/rating", + ), + ).awaitSuccess() } } suspend fun search(query: String): List { - val body = buildJsonObject { - put("search", query) - put( - "filter_types", - buildJsonArray { - add("drama cd") - add("novel") - }, - ) - } + val body = + buildJsonObject { + put("search", query) + put( + "filter_types", + buildJsonArray { + add("drama cd") + add("novel") + }, + ) + } return with(json) { - client.newCall( - POST( - url = "$baseUrl/v1/series/search", - body = body.toString().toRequestBody(contentType), - ), - ) - .awaitSuccess() + client + .newCall( + POST( + url = "$baseUrl/v1/series/search", + body = body.toString().toRequestBody(contentType), + ), + ).awaitSuccess() .parseAs() .let { obj -> obj["results"]?.jsonArray?.map { element -> json.decodeFromJsonElement(element.jsonObject["record"]!!) } - } - .orEmpty() + }.orEmpty() } } - suspend fun authenticate(username: String, password: String): Context? { - val body = buildJsonObject { - put("username", username) - put("password", password) - } + suspend fun authenticate( + username: String, + password: String, + ): Context? { + val body = + buildJsonObject { + put("username", username) + put("password", password) + } return with(json) { - client.newCall( - PUT( - url = "$baseUrl/v1/account/login", - body = body.toString().toRequestBody(contentType), - ), - ) - .awaitSuccess() + client + .newCall( + PUT( + url = "$baseUrl/v1/account/login", + body = body.toString().toRequestBody(contentType), + ), + ).awaitSuccess() .parseAs() .let { obj -> try { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt index 094471b18a..73c4d0b595 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt @@ -8,7 +8,6 @@ import java.io.IOException class MangaUpdatesInterceptor( mangaUpdates: MangaUpdates, ) : Interceptor { - private var token: String? = mangaUpdates.restoreSession() override fun intercept(chain: Interceptor.Chain): Response { @@ -17,10 +16,12 @@ class MangaUpdatesInterceptor( val token = token ?: throw IOException("Not authenticated with MangaUpdates") // Add the authorization header to the original request. - val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer $token") - .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") - .build() + val authRequest = + originalRequest + .newBuilder() + .addHeader("Authorization", "Bearer $token") + .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") + .build() return chain.proceed(authRequest) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt index 15a551078b..2a465f511b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt @@ -14,9 +14,8 @@ data class ListItem( val priority: Int? = null, ) -fun ListItem.copyTo(track: Track): Track { - return track.apply { +fun ListItem.copyTo(track: Track): Track = + track.apply { this.status = listId ?: READING_LIST this.last_chapter_read = this@copyTo.status?.chapter?.toDouble() ?: 0.0 } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt index 89a55b4135..a79d401d10 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt @@ -8,8 +8,7 @@ data class Rating( val rating: Double? = null, ) -fun Rating.copyTo(track: Track): Track { - return track.apply { +fun Rating.copyTo(track: Track): Track = + track.apply { this.score = rating ?: 0.0 } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt index 4b66273e83..6da79f5835 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt @@ -23,8 +23,8 @@ data class Record( val latestChapter: Int? = null, ) -fun Record.toTrackSearch(id: Long): TrackSearch { - return TrackSearch.create(id).apply { +fun Record.toTrackSearch(id: Long): TrackSearch = + TrackSearch.create(id).apply { remote_id = this@toTrackSearch.seriesId ?: 0L title = this@toTrackSearch.title?.htmlDecode() ?: "" total_chapters = 0 @@ -35,4 +35,3 @@ fun Record.toTrackSearch(id: Long): TrackSearch { publishing_type = this@toTrackSearch.type.toString() start_date = this@toTrackSearch.year.toString() } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt index c1af76ebfe..52efcfd0d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.track.model import eu.kanade.tachiyomi.data.database.models.Track class TrackSearch : Track { - override var id: Long? = null override var manga_id: Long = 0 @@ -61,8 +60,9 @@ class TrackSearch : Track { } companion object { - fun create(serviceId: Long): TrackSearch = TrackSearch().apply { - tracker_id = serviceId - } + fun create(serviceId: Long): TrackSearch = + TrackSearch().apply { + tracker_id = serviceId + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 33daee1a95..f1d224fe3d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -15,8 +15,10 @@ import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy import tachiyomi.domain.track.model.Track as DomainTrack -class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { - +class MyAnimeList( + id: Long, +) : BaseTracker(id, "MyAnimeList"), + DeletableTracker { companion object { const val READING = 1L const val COMPLETED = 2L @@ -28,9 +30,10 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { private const val SEARCH_ID_PREFIX = "id:" private const val SEARCH_LIST_PREFIX = "my:" - private val SCORE_LIST = IntRange(0, 10) - .map(Int::toString) - .toImmutableList() + private val SCORE_LIST = + IntRange(0, 10) + .map(Int::toString) + .toImmutableList() } private val json: Json by injectLazy() @@ -44,19 +47,18 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { override fun getLogoColor() = Color.rgb(46, 81, 162) - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING) - } - - override fun getStatus(status: Long): StringResource? = when (status) { - READING -> MR.strings.reading - PLAN_TO_READ -> MR.strings.plan_to_read - COMPLETED -> MR.strings.completed - ON_HOLD -> MR.strings.on_hold - DROPPED -> MR.strings.dropped - REREADING -> MR.strings.repeating - else -> null - } + override fun getStatusList(): List = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING) + + override fun getStatus(status: Long): StringResource? = + when (status) { + READING -> MR.strings.reading + PLAN_TO_READ -> MR.strings.plan_to_read + COMPLETED -> MR.strings.completed + ON_HOLD -> MR.strings.on_hold + DROPPED -> MR.strings.dropped + REREADING -> MR.strings.repeating + else -> null + } override fun getReadingStatus(): Long = READING @@ -66,15 +68,14 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { override fun getScoreList(): ImmutableList = SCORE_LIST - override fun displayScore(track: DomainTrack): String { - return track.score.toInt().toString() - } + override fun displayScore(track: DomainTrack): String = track.score.toInt().toString() - private suspend fun add(track: Track): Track { - return api.updateItem(track) - } + private suspend fun add(track: Track): Track = api.updateItem(track) - override suspend fun update(track: Track, didReadChapter: Boolean): Track { + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { if (track.status != COMPLETED) { if (didReadChapter) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { @@ -96,7 +97,10 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { api.deleteItem(track) } - override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track { val remoteTrack = api.findListItem(track) return if (remoteTrack != null) { track.copyPersonalFrom(remoteTrack) @@ -132,11 +136,12 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { return api.search(query) } - override suspend fun refresh(track: Track): Track { - return api.findListItem(track) ?: add(track) - } + override suspend fun refresh(track: Track): Track = api.findListItem(track) ?: add(track) - override suspend fun login(username: String, password: String) = login(password) + override suspend fun login( + username: String, + password: String, + ) = login(password) suspend fun login(authCode: String) { try { @@ -155,9 +160,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { interceptor.setAuth(null) } - fun getIfAuthExpired(): Boolean { - return trackPreferences.trackAuthExpired(this).get() - } + fun getIfAuthExpired(): Boolean = trackPreferences.trackAuthExpired(this).get() fun setAuthExpired() { trackPreferences.trackAuthExpired(this).set(true) @@ -167,11 +170,10 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) } - fun loadOAuth(): OAuth? { - return try { + fun loadOAuth(): OAuth? = + try { json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } - } } 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 2aef56016b..8b9caa4954 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 @@ -39,78 +39,87 @@ class MyAnimeListApi( private val client: OkHttpClient, interceptor: MyAnimeListInterceptor, ) { - private val json: Json by injectLazy() private val authClient = client.newBuilder().addInterceptor(interceptor).build() - suspend fun getAccessToken(authCode: String): OAuth { - return withIOContext { - val formBody: RequestBody = FormBody.Builder() - .add("client_id", CLIENT_ID) - .add("code", authCode) - .add("code_verifier", codeVerifier) - .add("grant_type", "authorization_code") - .build() + suspend fun getAccessToken(authCode: String): OAuth = + withIOContext { + val formBody: RequestBody = + FormBody + .Builder() + .add("client_id", CLIENT_ID) + .add("code", authCode) + .add("code_verifier", codeVerifier) + .add("grant_type", "authorization_code") + .build() with(json) { - client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody)) + client + .newCall(POST("$BASE_OAUTH_URL/token", body = formBody)) .awaitSuccess() .parseAs() } } - } - suspend fun getCurrentUser(): String { - return withIOContext { - val request = Request.Builder() - .url("$BASE_API_URL/users/@me") - .get() - .build() + suspend fun getCurrentUser(): String = + withIOContext { + val request = + Request + .Builder() + .url("$BASE_API_URL/users/@me") + .get() + .build() with(json) { - authClient.newCall(request) + authClient + .newCall(request) .awaitSuccess() .parseAs() .let { it["name"]!!.jsonPrimitive.content } } } - } - suspend fun search(query: String): List { - return withIOContext { - val url = "$BASE_API_URL/manga".toUri().buildUpon() - // MAL API throws a 400 when the query is over 64 characters... - .appendQueryParameter("q", query.take(64)) - .appendQueryParameter("nsfw", "true") - .build() + suspend fun search(query: String): List = + withIOContext { + val url = + "$BASE_API_URL/manga" + .toUri() + .buildUpon() + // MAL API throws a 400 when the query is over 64 characters... + .appendQueryParameter("q", query.take(64)) + .appendQueryParameter("nsfw", "true") + .build() with(json) { - authClient.newCall(GET(url.toString())) + authClient + .newCall(GET(url.toString())) .awaitSuccess() .parseAs() .let { - it["data"]!!.jsonArray + it["data"]!! + .jsonArray .map { data -> data.jsonObject["node"]!!.jsonObject } .map { node -> val id = node["id"]!!.jsonPrimitive.int async { getMangaDetails(id) } - } - .awaitAll() + }.awaitAll() .filter { trackSearch -> !trackSearch.publishing_type.contains("novel") } } } } - } - suspend fun getMangaDetails(id: Int): TrackSearch { - return withIOContext { - val url = "$BASE_API_URL/manga".toUri().buildUpon() - .appendPath(id.toString()) - .appendQueryParameter( - "fields", - "id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date", - ) - .build() + suspend fun getMangaDetails(id: Int): TrackSearch = + withIOContext { + val url = + "$BASE_API_URL/manga" + .toUri() + .buildUpon() + .appendPath(id.toString()) + .appendQueryParameter( + "fields", + "id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date", + ).build() with(json) { - authClient.newCall(GET(url.toString())) + authClient + .newCall(GET(url.toString())) .awaitSuccess() .parseAs() .let { @@ -122,32 +131,38 @@ class MyAnimeListApi( total_chapters = obj["num_chapters"]!!.jsonPrimitive.long score = obj["mean"]?.jsonPrimitive?.doubleOrNull ?: -1.0 cover_url = - obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content + obj["main_picture"] + ?.jsonObject + ?.get("large") + ?.jsonPrimitive + ?.content ?: "" tracking_url = "https://myanimelist.net/manga/$remote_id" publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ") publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ") - start_date = try { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(obj["start_date"]!!) - } catch (e: Exception) { - "" - } + start_date = + try { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(obj["start_date"]!!) + } catch (e: Exception) { + "" + } } } } } - } - suspend fun updateItem(track: Track): Track { - return withIOContext { - val formBodyBuilder = FormBody.Builder() - .add("status", track.toMyAnimeListStatus() ?: "reading") - .add("is_rereading", (track.status == MyAnimeList.REREADING).toString()) - .add("score", track.score.toString()) - .add("num_chapters_read", track.last_chapter_read.toInt().toString()) + suspend fun updateItem(track: Track): Track = + withIOContext { + val formBodyBuilder = + FormBody + .Builder() + .add("status", track.toMyAnimeListStatus() ?: "reading") + .add("is_rereading", (track.status == MyAnimeList.REREADING).toString()) + .add("score", track.score.toString()) + .add("num_chapters_read", track.last_chapter_read.toInt().toString()) convertToIsoDate(track.started_reading_date)?.let { formBodyBuilder.add("start_date", it) } @@ -155,18 +170,20 @@ class MyAnimeListApi( formBodyBuilder.add("finish_date", it) } - val request = Request.Builder() - .url(mangaUrl(track.remote_id).toString()) - .put(formBodyBuilder.build()) - .build() + val request = + Request + .Builder() + .url(mangaUrl(track.remote_id).toString()) + .put(formBodyBuilder.build()) + .build() with(json) { - authClient.newCall(request) + authClient + .newCall(request) .awaitSuccess() .parseAs() .let { parseMangaItem(it, track) } } } - } suspend fun deleteItem(track: DomainTrack) { withIOContext { @@ -176,14 +193,18 @@ class MyAnimeListApi( } } - suspend fun findListItem(track: Track): Track? { - return withIOContext { - val uri = "$BASE_API_URL/manga".toUri().buildUpon() - .appendPath(track.remote_id.toString()) - .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") - .build() + suspend fun findListItem(track: Track): Track? = + withIOContext { + val uri = + "$BASE_API_URL/manga" + .toUri() + .buildUpon() + .appendPath(track.remote_id.toString()) + .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") + .build() with(json) { - authClient.newCall(GET(uri.toString())) + authClient + .newCall(GET(uri.toString())) .awaitSuccess() .parseAs() .let { obj -> @@ -194,57 +215,74 @@ class MyAnimeListApi( } } } - } - suspend fun findListItems(query: String, offset: Int = 0): List { - return withIOContext { + suspend fun findListItems( + query: String, + offset: Int = 0, + ): List = + withIOContext { val json = getListPage(offset) val obj = json.jsonObject - val matches = obj["data"]!!.jsonArray - .filter { - it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains( - query, - ignoreCase = true, - ) - } - .map { - val id = it.jsonObject["node"]!!.jsonObject["id"]!!.jsonPrimitive.int - async { getMangaDetails(id) } - } - .awaitAll() + val matches = + obj["data"]!! + .jsonArray + .filter { + it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains( + query, + ignoreCase = true, + ) + }.map { + val id = + it.jsonObject["node"]!! + .jsonObject["id"]!! + .jsonPrimitive.int + async { getMangaDetails(id) } + }.awaitAll() // Check next page if there's more - if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) { + if (!obj["paging"]!! + .jsonObject["next"] + ?.jsonPrimitive + ?.contentOrNull + .isNullOrBlank() + ) { matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT) } else { matches } } - } - private suspend fun getListPage(offset: Int): JsonObject { - return withIOContext { - val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon() - .appendQueryParameter("fields", "list_status{start_date,finish_date}") - .appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString()) + private suspend fun getListPage(offset: Int): JsonObject = + withIOContext { + val urlBuilder = + "$BASE_API_URL/users/@me/mangalist" + .toUri() + .buildUpon() + .appendQueryParameter("fields", "list_status{start_date,finish_date}") + .appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString()) if (offset > 0) { urlBuilder.appendQueryParameter("offset", offset.toString()) } - val request = Request.Builder() - .url(urlBuilder.build().toString()) - .get() - .build() + val request = + Request + .Builder() + .url(urlBuilder.build().toString()) + .get() + .build() with(json) { - authClient.newCall(request) + authClient + .newCall(request) .awaitSuccess() .parseAs() } } - } - private fun parseMangaItem(response: JsonObject, track: Track): Track { + private fun parseMangaItem( + response: JsonObject, + track: Track, + ): Track { val obj = response.jsonObject return track.apply { val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean @@ -260,9 +298,7 @@ class MyAnimeListApi( } } - private fun parseDate(isoDate: String): Long { - return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L - } + private fun parseDate(isoDate: String): Long = SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L private fun convertToIsoDate(epochTime: Long): String? { if (epochTime == 0L) { @@ -286,30 +322,40 @@ class MyAnimeListApi( private var codeVerifier: String = "" - fun authUrl(): Uri = "$BASE_OAUTH_URL/authorize".toUri().buildUpon() - .appendQueryParameter("client_id", CLIENT_ID) - .appendQueryParameter("code_challenge", getPkceChallengeCode()) - .appendQueryParameter("response_type", "code") - .build() + fun authUrl(): Uri = + "$BASE_OAUTH_URL/authorize" + .toUri() + .buildUpon() + .appendQueryParameter("client_id", CLIENT_ID) + .appendQueryParameter("code_challenge", getPkceChallengeCode()) + .appendQueryParameter("response_type", "code") + .build() - fun mangaUrl(id: Long): Uri = "$BASE_API_URL/manga".toUri().buildUpon() - .appendPath(id.toString()) - .appendPath("my_list_status") - .build() + fun mangaUrl(id: Long): Uri = + "$BASE_API_URL/manga" + .toUri() + .buildUpon() + .appendPath(id.toString()) + .appendPath("my_list_status") + .build() fun refreshTokenRequest(oauth: OAuth): Request { - val formBody: RequestBody = FormBody.Builder() - .add("client_id", CLIENT_ID) - .add("refresh_token", oauth.refresh_token) - .add("grant_type", "refresh_token") - .build() + val formBody: RequestBody = + FormBody + .Builder() + .add("client_id", CLIENT_ID) + .add("refresh_token", oauth.refresh_token) + .add("grant_type", "refresh_token") + .build() // Add the Authorization header manually as this particular // request is called by the interceptor itself so it doesn't reach // the part where the token is added automatically. - val headers = Headers.Builder() - .add("Authorization", "Bearer ${oauth.access_token}") - .build() + val headers = + Headers + .Builder() + .add("Authorization", "Bearer ${oauth.access_token}") + .build() return POST("$BASE_OAUTH_URL/token", body = formBody, headers = headers) } 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 d67c2cabec..c1714f7924 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 @@ -7,8 +7,9 @@ import okhttp3.Response import uy.kohesive.injekt.injectLazy import java.io.IOException -class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor { - +class MyAnimeListInterceptor( + private val myanimelist: MyAnimeList, +) : Interceptor { private val json: Json by injectLazy() private var oauth: OAuth? = myanimelist.loadOAuth() @@ -29,11 +30,13 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor } // Add the authorization header to the original request - val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") - // TODO(antsy): Add back custom user agent when they stop blocking us for no apparent reason - // .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") - .build() + val authRequest = + originalRequest + .newBuilder() + .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + // TODO(antsy): Add back custom user agent when they stop blocking us for no apparent reason + // .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") + .build() return chain.proceed(authRequest) } @@ -47,37 +50,39 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor myanimelist.saveOAuth(oauth) } - private fun refreshToken(chain: Interceptor.Chain): OAuth = synchronized(this) { - if (tokenExpired) throw MALTokenExpired() - oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it } - - val response = try { - chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) - } catch (_: Throwable) { - throw MALTokenRefreshFailed() - } + private fun refreshToken(chain: Interceptor.Chain): OAuth = + synchronized(this) { + if (tokenExpired) throw MALTokenExpired() + oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it } - if (response.code == 401) { - myanimelist.setAuthExpired() - throw MALTokenExpired() - } + val response = + try { + chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) + } catch (_: Throwable) { + throw MALTokenRefreshFailed() + } - return runCatching { - if (response.isSuccessful) { - with(json) { response.parseAs() } - } else { - response.close() - null + if (response.code == 401) { + myanimelist.setAuthExpired() + throw MALTokenExpired() } + + return runCatching { + if (response.isSuccessful) { + with(json) { response.parseAs() } + } else { + response.close() + null + } + }.getOrNull() + ?.also { + this.oauth = it + myanimelist.saveOAuth(it) + } + ?: throw MALTokenRefreshFailed() } - .getOrNull() - ?.also { - this.oauth = it - myanimelist.saveOAuth(it) - } - ?: throw MALTokenRefreshFailed() - } } class MALTokenRefreshFailed : IOException("MAL: Failed to refresh account token") + class MALTokenExpired : IOException("MAL: Login has expired") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt index 1ae02142f2..930b33edde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt @@ -13,24 +13,27 @@ data class OAuth( ) { // Assumes expired a minute earlier private val adjustedExpiresIn: Long = (expires_in - 60) * 1000 + fun isExpired() = created_at + adjustedExpiresIn < System.currentTimeMillis() } -fun Track.toMyAnimeListStatus() = when (status) { - MyAnimeList.READING -> "reading" - MyAnimeList.COMPLETED -> "completed" - MyAnimeList.ON_HOLD -> "on_hold" - MyAnimeList.DROPPED -> "dropped" - MyAnimeList.PLAN_TO_READ -> "plan_to_read" - MyAnimeList.REREADING -> "reading" - else -> null -} +fun Track.toMyAnimeListStatus() = + when (status) { + MyAnimeList.READING -> "reading" + MyAnimeList.COMPLETED -> "completed" + MyAnimeList.ON_HOLD -> "on_hold" + MyAnimeList.DROPPED -> "dropped" + MyAnimeList.PLAN_TO_READ -> "plan_to_read" + MyAnimeList.REREADING -> "reading" + else -> null + } -fun getStatus(status: String?) = when (status) { - "reading" -> MyAnimeList.READING - "completed" -> MyAnimeList.COMPLETED - "on_hold" -> MyAnimeList.ON_HOLD - "dropped" -> MyAnimeList.DROPPED - "plan_to_read" -> MyAnimeList.PLAN_TO_READ - else -> MyAnimeList.READING -} +fun getStatus(status: String?) = + when (status) { + "reading" -> MyAnimeList.READING + "completed" -> MyAnimeList.COMPLETED + "on_hold" -> MyAnimeList.ON_HOLD + "dropped" -> MyAnimeList.DROPPED + "plan_to_read" -> MyAnimeList.PLAN_TO_READ + else -> MyAnimeList.READING + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index 118d005c15..abd4829667 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -15,8 +15,10 @@ import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy import tachiyomi.domain.track.model.Track as DomainTrack -class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { - +class Shikimori( + id: Long, +) : BaseTracker(id, "Shikimori"), + DeletableTracker { companion object { const val READING = 1L const val COMPLETED = 2L @@ -25,9 +27,10 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { const val PLAN_TO_READ = 5L const val REREADING = 6L - private val SCORE_LIST = IntRange(0, 10) - .map(Int::toString) - .toImmutableList() + private val SCORE_LIST = + IntRange(0, 10) + .map(Int::toString) + .toImmutableList() } private val json: Json by injectLazy() @@ -38,15 +41,14 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { override fun getScoreList(): ImmutableList = SCORE_LIST - override fun displayScore(track: DomainTrack): String { - return track.score.toInt().toString() - } + override fun displayScore(track: DomainTrack): String = track.score.toInt().toString() - private suspend fun add(track: Track): Track { - return api.addLibManga(track, getUsername()) - } + private suspend fun add(track: Track): Track = api.addLibManga(track, getUsername()) - override suspend fun update(track: Track, didReadChapter: Boolean): Track { + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { if (track.status != COMPLETED) { if (didReadChapter) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { @@ -64,7 +66,10 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { api.deleteLibManga(track) } - override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track { val remoteTrack = api.findLibManga(track, getUsername()) return if (remoteTrack != null) { track.copyPersonalFrom(remoteTrack) @@ -84,9 +89,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { } } - override suspend fun search(query: String): List { - return api.search(query) - } + override suspend fun search(query: String): List = api.search(query) override suspend fun refresh(track: Track): Track { api.findLibManga(track, getUsername())?.let { remoteTrack -> @@ -101,19 +104,18 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { override fun getLogoColor() = Color.rgb(40, 40, 40) - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING) - } - - override fun getStatus(status: Long): StringResource? = when (status) { - READING -> MR.strings.reading - PLAN_TO_READ -> MR.strings.plan_to_read - COMPLETED -> MR.strings.completed - ON_HOLD -> MR.strings.on_hold - DROPPED -> MR.strings.dropped - REREADING -> MR.strings.repeating - else -> null - } + override fun getStatusList(): List = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING) + + override fun getStatus(status: Long): StringResource? = + when (status) { + READING -> MR.strings.reading + PLAN_TO_READ -> MR.strings.plan_to_read + COMPLETED -> MR.strings.completed + ON_HOLD -> MR.strings.on_hold + DROPPED -> MR.strings.dropped + REREADING -> MR.strings.repeating + else -> null + } override fun getReadingStatus(): Long = READING @@ -121,7 +123,10 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { override fun getCompletionStatus(): Long = COMPLETED - override suspend fun login(username: String, password: String) = login(password) + override suspend fun login( + username: String, + password: String, + ) = login(password) suspend fun login(code: String) { try { @@ -138,13 +143,12 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { - return try { + fun restoreToken(): OAuth? = + try { json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } - } override fun logout() { super.logout() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 6eb93a6362..c1898b6d7b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -34,30 +34,34 @@ class ShikimoriApi( private val client: OkHttpClient, interceptor: ShikimoriInterceptor, ) { - private val json: Json by injectLazy() private val authClient = client.newBuilder().addInterceptor(interceptor).build() - suspend fun addLibManga(track: Track, userId: String): Track { - return withIOContext { + suspend fun addLibManga( + track: Track, + userId: String, + ): Track = + withIOContext { with(json) { - val payload = buildJsonObject { - putJsonObject("user_rate") { - put("user_id", userId) - put("target_id", track.remote_id) - put("target_type", "Manga") - put("chapters", track.last_chapter_read.toInt()) - put("score", track.score.toInt()) - put("status", track.toShikimoriStatus()) + val payload = + buildJsonObject { + putJsonObject("user_rate") { + put("user_id", userId) + put("target_id", track.remote_id) + put("target_type", "Manga") + put("chapters", track.last_chapter_read.toInt()) + put("score", track.score.toInt()) + put("status", track.toShikimoriStatus()) + } } - } - authClient.newCall( - POST( - "$apiUrl/v2/user_rates", - body = payload.toString().toRequestBody(jsonMime), - ), - ).awaitSuccess() + authClient + .newCall( + POST( + "$apiUrl/v2/user_rates", + body = payload.toString().toRequestBody(jsonMime), + ), + ).awaitSuccess() .parseAs() .let { // save id of the entry for possible future delete request @@ -66,9 +70,11 @@ class ShikimoriApi( track } } - } - suspend fun updateLibManga(track: Track, userId: String): Track = addLibManga(track, userId) + suspend fun updateLibManga( + track: Track, + userId: String, + ): Track = addLibManga(track, userId) suspend fun deleteLibManga(track: DomainTrack) { withIOContext { @@ -78,15 +84,19 @@ class ShikimoriApi( } } - suspend fun search(search: String): List { - return withIOContext { - val url = "$apiUrl/mangas".toUri().buildUpon() - .appendQueryParameter("order", "popularity") - .appendQueryParameter("search", search) - .appendQueryParameter("limit", "20") - .build() + suspend fun search(search: String): List = + withIOContext { + val url = + "$apiUrl/mangas" + .toUri() + .buildUpon() + .appendQueryParameter("order", "popularity") + .appendQueryParameter("search", search) + .appendQueryParameter("limit", "20") + .build() with(json) { - authClient.newCall(GET(url.toString())) + authClient + .newCall(GET(url.toString())) .awaitSuccess() .parseAs() .let { response -> @@ -96,10 +106,9 @@ class ShikimoriApi( } } } - } - private fun jsonToSearch(obj: JsonObject): TrackSearch { - return TrackSearch.create(trackId).apply { + private fun jsonToSearch(obj: JsonObject): TrackSearch = + TrackSearch.create(trackId).apply { remote_id = obj["id"]!!.jsonPrimitive.long title = obj["name"]!!.jsonPrimitive.content total_chapters = obj["chapters"]!!.jsonPrimitive.long @@ -111,10 +120,12 @@ class ShikimoriApi( publishing_type = obj["kind"]!!.jsonPrimitive.content start_date = obj["aired_on"]!!.jsonPrimitive.contentOrNull ?: "" } - } - private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track { - return Track.create(trackId).apply { + private fun jsonToTrack( + obj: JsonObject, + mangas: JsonObject, + ): Track = + Track.create(trackId).apply { title = mangas["name"]!!.jsonPrimitive.content remote_id = obj["id"]!!.jsonPrimitive.long total_chapters = mangas["chapters"]!!.jsonPrimitive.long @@ -124,72 +135,86 @@ class ShikimoriApi( status = toTrackStatus(obj["status"]!!.jsonPrimitive.content) tracking_url = baseUrl + mangas["url"]!!.jsonPrimitive.content } - } - suspend fun findLibManga(track: Track, userId: String): Track? { - return withIOContext { - val urlMangas = "$apiUrl/mangas".toUri().buildUpon() - .appendPath(track.remote_id.toString()) - .build() - val mangas = with(json) { - authClient.newCall(GET(urlMangas.toString())) - .awaitSuccess() - .parseAs() - } + suspend fun findLibManga( + track: Track, + userId: String, + ): Track? = + withIOContext { + val urlMangas = + "$apiUrl/mangas" + .toUri() + .buildUpon() + .appendPath(track.remote_id.toString()) + .build() + val mangas = + with(json) { + authClient + .newCall(GET(urlMangas.toString())) + .awaitSuccess() + .parseAs() + } - val url = "$apiUrl/v2/user_rates".toUri().buildUpon() - .appendQueryParameter("user_id", userId) - .appendQueryParameter("target_id", track.remote_id.toString()) - .appendQueryParameter("target_type", "Manga") - .build() + val url = + "$apiUrl/v2/user_rates" + .toUri() + .buildUpon() + .appendQueryParameter("user_id", userId) + .appendQueryParameter("target_id", track.remote_id.toString()) + .appendQueryParameter("target_type", "Manga") + .build() with(json) { - authClient.newCall(GET(url.toString())) + authClient + .newCall(GET(url.toString())) .awaitSuccess() .parseAs() .let { response -> if (response.size > 1) { throw Exception("Too much mangas in response") } - val entry = response.map { - jsonToTrack(it.jsonObject, mangas) - } + val entry = + response.map { + jsonToTrack(it.jsonObject, mangas) + } entry.firstOrNull() } } } - } - suspend fun getCurrentUser(): Int { - return with(json) { - authClient.newCall(GET("$apiUrl/users/whoami")) + suspend fun getCurrentUser(): Int = + with(json) { + authClient + .newCall(GET("$apiUrl/users/whoami")) .awaitSuccess() .parseAs() .let { it["id"]!!.jsonPrimitive.int } } - } - suspend fun accessToken(code: String): OAuth { - return withIOContext { + suspend fun accessToken(code: String): OAuth = + withIOContext { with(json) { - client.newCall(accessTokenRequest(code)) + client + .newCall(accessTokenRequest(code)) .awaitSuccess() .parseAs() } } - } - private fun accessTokenRequest(code: String) = POST( - oauthUrl, - body = FormBody.Builder() - .add("grant_type", "authorization_code") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("code", code) - .add("redirect_uri", redirectUrl) - .build(), - ) + private fun accessTokenRequest(code: String) = + POST( + oauthUrl, + body = + FormBody + .Builder() + .add("grant_type", "authorization_code") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .add("code", code) + .add("redirect_uri", redirectUrl) + .build(), + ) companion object { private const val clientId = "PB9dq8DzI405s7wdtwTdirYqHiyVMh--djnP7lBUqSA" @@ -202,20 +227,26 @@ class ShikimoriApi( private const val redirectUrl = "mihon://shikimori-auth" - fun authUrl(): Uri = loginUrl.toUri().buildUpon() - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("redirect_uri", redirectUrl) - .appendQueryParameter("response_type", "code") - .build() + fun authUrl(): Uri = + loginUrl + .toUri() + .buildUpon() + .appendQueryParameter("client_id", clientId) + .appendQueryParameter("redirect_uri", redirectUrl) + .appendQueryParameter("response_type", "code") + .build() - fun refreshTokenRequest(token: String) = POST( - oauthUrl, - body = FormBody.Builder() - .add("grant_type", "refresh_token") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("refresh_token", token) - .build(), - ) + fun refreshTokenRequest(token: String) = + POST( + oauthUrl, + body = + FormBody + .Builder() + .add("grant_type", "refresh_token") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .add("refresh_token", token) + .build(), + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt index aa2d4247a2..d185dcf52e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt @@ -6,8 +6,9 @@ import okhttp3.Interceptor import okhttp3.Response import uy.kohesive.injekt.injectLazy -class ShikimoriInterceptor(private val shikimori: Shikimori) : Interceptor { - +class ShikimoriInterceptor( + private val shikimori: Shikimori, +) : Interceptor { private val json: Json by injectLazy() /** @@ -32,10 +33,12 @@ class ShikimoriInterceptor(private val shikimori: Shikimori) : Interceptor { } } // Add the authorization header to the original request. - val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") - .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") - .build() + val authRequest = + originalRequest + .newBuilder() + .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") + .build() return chain.proceed(authRequest) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt index 5a28eceb40..6bead8994c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt @@ -15,22 +15,24 @@ data class OAuth( // Access token lives 1 day fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) -fun Track.toShikimoriStatus() = when (status) { - Shikimori.READING -> "watching" - Shikimori.COMPLETED -> "completed" - Shikimori.ON_HOLD -> "on_hold" - Shikimori.DROPPED -> "dropped" - Shikimori.PLAN_TO_READ -> "planned" - Shikimori.REREADING -> "rewatching" - else -> throw NotImplementedError("Unknown status: $status") -} +fun Track.toShikimoriStatus() = + when (status) { + Shikimori.READING -> "watching" + Shikimori.COMPLETED -> "completed" + Shikimori.ON_HOLD -> "on_hold" + Shikimori.DROPPED -> "dropped" + Shikimori.PLAN_TO_READ -> "planned" + Shikimori.REREADING -> "rewatching" + else -> throw NotImplementedError("Unknown status: $status") + } -fun toTrackStatus(status: String) = when (status) { - "watching" -> Shikimori.READING - "completed" -> Shikimori.COMPLETED - "on_hold" -> Shikimori.ON_HOLD - "dropped" -> Shikimori.DROPPED - "planned" -> Shikimori.PLAN_TO_READ - "rewatching" -> Shikimori.REREADING - else -> throw NotImplementedError("Unknown status: $status") -} +fun toTrackStatus(status: String) = + when (status) { + "watching" -> Shikimori.READING + "completed" -> Shikimori.COMPLETED + "on_hold" -> Shikimori.ON_HOLD + "dropped" -> Shikimori.DROPPED + "planned" -> Shikimori.PLAN_TO_READ + "rewatching" -> Shikimori.REREADING + else -> throw NotImplementedError("Unknown status: $status") + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt index 6b2ca63f90..939c61f8a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt @@ -14,8 +14,10 @@ import tachiyomi.i18n.MR import tachiyomi.domain.manga.model.Manga as DomainManga import tachiyomi.domain.track.model.Track as DomainTrack -class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { - +class Suwayomi( + id: Long, +) : BaseTracker(id, "Suwayomi"), + EnhancedTracker { val api by lazy { SuwayomiApi(id) } override fun getLogo() = R.drawable.ic_tracker_suwayomi @@ -30,12 +32,13 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { override fun getStatusList(): List = listOf(UNREAD, READING, COMPLETED) - override fun getStatus(status: Long): StringResource? = when (status) { - UNREAD -> MR.strings.unread - READING -> MR.strings.reading - COMPLETED -> MR.strings.completed - else -> null - } + override fun getStatus(status: Long): StringResource? = + when (status) { + UNREAD -> MR.strings.unread + READING -> MR.strings.reading + COMPLETED -> MR.strings.completed + else -> null + } override fun getReadingStatus(): Long = READING @@ -47,7 +50,10 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { override fun displayScore(track: DomainTrack): String = "" - override suspend fun update(track: Track, didReadChapter: Boolean): Track { + override suspend fun update( + track: Track, + didReadChapter: Boolean, + ): Track { if (track.status != COMPLETED) { if (didReadChapter) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { @@ -61,9 +67,10 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { return api.updateProgress(track) } - override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { - return track - } + override suspend fun bind( + track: Track, + hasReadChapters: Boolean, + ): Track = track override suspend fun search(query: String): List { TODO("Not yet implemented") @@ -76,7 +83,10 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { return track } - override suspend fun login(username: String, password: String) { + override suspend fun login( + username: String, + password: String, + ) { saveCredentials("user", "pass") } @@ -93,11 +103,20 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { null } - override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean = source?.let { - accept(it) - } == true - - override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? = + override fun isTrackFrom( + track: DomainTrack, + manga: DomainManga, + source: Source?, + ): Boolean = + source?.let { + accept(it) + } == true + + override fun migrateTrack( + track: DomainTrack, + manga: DomainManga, + newSource: Source, + ): DomainTrack? = if (accept(newSource)) { track.copy(remoteUrl = manga.url) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt index c7cc8a188f..aa1ee9b6fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt @@ -23,22 +23,25 @@ import uy.kohesive.injekt.injectLazy import java.nio.charset.Charset import java.security.MessageDigest -class SuwayomiApi(private val trackId: Long) { - +class SuwayomiApi( + private val trackId: Long, +) { private val network: NetworkHelper by injectLazy() private val json: Json by injectLazy() private val client: OkHttpClient = - network.client.newBuilder() + network.client + .newBuilder() .dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing .build() - private fun headersBuilder(): Headers.Builder = Headers.Builder().apply { - if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) { - val credentials = Credentials.basic(baseLogin, basePassword) - add("Authorization", credentials) + private fun headersBuilder(): Headers.Builder = + Headers.Builder().apply { + if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) { + val credentials = Credentials.basic(baseLogin, basePassword) + add("Authorization", credentials) + } } - } private val headers: Headers by lazy { headersBuilder().build() } @@ -46,56 +49,65 @@ class SuwayomiApi(private val trackId: Long) { private val baseLogin by lazy { getPrefBaseLogin() } private val basePassword by lazy { getPrefBasePassword() } - suspend fun getTrackSearch(trackUrl: String): TrackSearch = withIOContext { - val url = try { - // test if getting api url or manga id - val mangaId = trackUrl.toLong() - "$baseUrl/api/v1/manga/$mangaId" - } catch (e: NumberFormatException) { - trackUrl - } - - val manga = with(json) { - client.newCall(GET("$url/full", headers)) - .awaitSuccess() - .parseAs() - } - - TrackSearch.create(trackId).apply { - title = manga.title - cover_url = "$url/thumbnail" - summary = manga.description.orEmpty() - tracking_url = url - total_chapters = manga.chapterCount - publishing_status = manga.status - last_chapter_read = manga.lastChapterRead?.chapterNumber ?: 0.0 - status = when (manga.unreadCount) { - manga.chapterCount -> Suwayomi.UNREAD - 0L -> Suwayomi.COMPLETED - else -> Suwayomi.READING + suspend fun getTrackSearch(trackUrl: String): TrackSearch = + withIOContext { + val url = + try { + // test if getting api url or manga id + val mangaId = trackUrl.toLong() + "$baseUrl/api/v1/manga/$mangaId" + } catch (e: NumberFormatException) { + trackUrl + } + + val manga = + with(json) { + client + .newCall(GET("$url/full", headers)) + .awaitSuccess() + .parseAs() + } + + TrackSearch.create(trackId).apply { + title = manga.title + cover_url = "$url/thumbnail" + summary = manga.description.orEmpty() + tracking_url = url + total_chapters = manga.chapterCount + publishing_status = manga.status + last_chapter_read = manga.lastChapterRead?.chapterNumber ?: 0.0 + status = + when (manga.unreadCount) { + manga.chapterCount -> Suwayomi.UNREAD + 0L -> Suwayomi.COMPLETED + else -> Suwayomi.READING + } } } - } suspend fun updateProgress(track: Track): Track { val url = track.tracking_url - val chapters = with(json) { - client.newCall(GET("$url/chapters", headers)) - .awaitSuccess() - .parseAs>() - } + val chapters = + with(json) { + client + .newCall(GET("$url/chapters", headers)) + .awaitSuccess() + .parseAs>() + } val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index - client.newCall( - PUT( - "$url/chapter/$lastChapterIndex", - headers, - FormBody.Builder(Charset.forName("utf8")) - .add("markPrevRead", "true") - .add("read", "true") - .build(), - ), - ).awaitSuccess() + client + .newCall( + PUT( + "$url/chapter/$lastChapterIndex", + headers, + FormBody + .Builder(Charset.forName("utf8")) + .add("markPrevRead", "true") + .add("read", "true") + .build(), + ), + ).awaitSuccess() return getTrackSearch(track.tracking_url) } @@ -111,7 +123,9 @@ class SuwayomiApi(private val trackId: Long) { } private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!! + private fun getPrefBaseLogin(): String = preferences.getString(LOGIN_TITLE, LOGIN_DEFAULT)!! + private fun getPrefBasePassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!! } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt index c3fb5023af..10a4dae0a8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt @@ -8,16 +8,12 @@ data class SourceDataClass( val name: String, val lang: String, val iconUrl: String, - /** The Source provides a latest listing */ val supportsLatest: Boolean, - /** The Source implements [ConfigurableSource] */ val isConfigurable: Boolean, - /** The Source class has a @Nsfw annotation */ val isNsfw: Boolean, - /** A nicer version of [name] */ val displayName: String, ) @@ -26,13 +22,10 @@ data class SourceDataClass( data class MangaDataClass( val id: Int, val sourceId: String, - val url: String, val title: String, val thumbnailUrl: String?, - val initialized: Boolean, - val artist: String?, val author: String?, val description: String?, @@ -41,19 +34,15 @@ data class MangaDataClass( val inLibrary: Boolean, val inLibraryAt: Long, val source: SourceDataClass?, - val meta: Map, - val realUrl: String?, val lastFetchedAt: Long?, val chaptersLastFetchedAt: Long?, - val freshData: Boolean, val unreadCount: Long?, val downloadCount: Long?, val chapterCount: Long, // actually is nullable server side, but should be set at this time val lastChapterRead: ChapterDataClass?, - val age: Long?, val chaptersAge: Long?, ) @@ -67,34 +56,24 @@ data class ChapterDataClass( val chapterNumber: Double, val scanlator: String?, val mangaId: Int, - /** chapter is read */ val read: Boolean, - /** chapter is bookmarked */ val bookmarked: Boolean, - /** last read page, zero means not read/no data */ val lastPageRead: Int, - /** last read page, zero means not read/no data */ val lastReadAt: Long, - /** this chapter's index, starts with 1 */ val index: Int, - /** the date we fist saw this chapter*/ val fetchedAt: Long, - /** is chapter downloaded */ val downloaded: Boolean, - /** used to construct pages in the front-end */ val pageCount: Int, - /** total chapter count, used to calculate if there's a next and prev chapter */ val chapterCount: Int?, - /** used to store client specific values */ val meta: Map, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt index d3632767cc..890c20faf6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt @@ -8,32 +8,36 @@ import tachiyomi.domain.release.interactor.GetApplicationRelease import uy.kohesive.injekt.injectLazy class AppUpdateChecker { - private val getApplicationRelease: GetApplicationRelease by injectLazy() - suspend fun checkForUpdate(context: Context, forceCheck: Boolean = false): GetApplicationRelease.Result { + suspend fun checkForUpdate( + context: Context, + forceCheck: Boolean = false, + ): GetApplicationRelease.Result { // Disable app update checks for older Android versions that we're going to drop support for // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { // return GetApplicationRelease.Result.OsTooOld // } return withIOContext { - val result = getApplicationRelease.await( - GetApplicationRelease.Arguments( - BuildConfig.PREVIEW, - context.isInstalledFromFDroid(), - BuildConfig.COMMIT_COUNT.toInt(), - BuildConfig.VERSION_NAME, - GITHUB_REPO, - forceCheck, - ), - ) + val result = + getApplicationRelease.await( + GetApplicationRelease.Arguments( + BuildConfig.PREVIEW, + context.isInstalledFromFDroid(), + BuildConfig.COMMIT_COUNT.toInt(), + BuildConfig.VERSION_NAME, + GITHUB_REPO, + forceCheck, + ), + ) when (result) { is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release) - is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier( - context, - ).promptFdroidUpdate() + is GetApplicationRelease.Result.ThirdPartyInstallation -> + AppUpdateNotifier( + context, + ).promptFdroidUpdate() else -> {} } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt index c1aa466263..205bdc4295 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt @@ -30,9 +30,10 @@ import uy.kohesive.injekt.injectLazy import java.io.File import kotlin.coroutines.cancellation.CancellationException -class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerParameters) : - CoroutineWorker(context, workerParams) { - +class AppUpdateDownloadJob( + private val context: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(context, workerParams) { private val notifier = AppUpdateNotifier(context) private val network: NetworkHelper by injectLazy() @@ -53,8 +54,8 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar return Result.success() } - override suspend fun getForegroundInfo(): ForegroundInfo { - return ForegroundInfo( + override suspend fun getForegroundInfo(): ForegroundInfo = + ForegroundInfo( Notifications.ID_APP_UPDATER, notifier.onDownloadStarted().build(), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -63,39 +64,48 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar 0 }, ) - } /** * Called to start downloading apk of new update * * @param url url location of file */ - private suspend fun downloadApk(title: String, url: String) { + private suspend fun downloadApk( + title: String, + url: String, + ) { // Show notification download starting. notifier.onDownloadStarted(title) - val progressListener = object : ProgressListener { - // Progress of the download - var savedProgress = 0 - - // Keep track of the last notification sent to avoid posting too many. - var lastTick = 0L - - override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { - val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt() - val currentTime = System.currentTimeMillis() - if (progress > savedProgress && currentTime - 200 > lastTick) { - savedProgress = progress - lastTick = currentTime - notifier.onProgressChange(progress) + val progressListener = + object : ProgressListener { + // Progress of the download + var savedProgress = 0 + + // Keep track of the last notification sent to avoid posting too many. + var lastTick = 0L + + override fun update( + bytesRead: Long, + contentLength: Long, + done: Boolean, + ) { + val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt() + val currentTime = System.currentTimeMillis() + if (progress > savedProgress && currentTime - 200 > lastTick) { + savedProgress = progress + lastTick = currentTime + notifier.onProgressChange(progress) + } } } - } try { // Download the new update. - val response = network.client.newCachelessCallWithProgress(GET(url), progressListener) - .await() + val response = + network.client + .newCachelessCallWithProgress(GET(url), progressListener) + .await() // File where the apk will be saved. val apkFile = File(context.externalCacheDir, "update.apk") @@ -109,8 +119,9 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar notifier.cancel() notifier.promptInstall(apkFile.getUriCompat(context)) } catch (e: Exception) { - val shouldCancel = e is CancellationException || - (e is StreamResetException && e.errorCode == ErrorCode.CANCEL) + val shouldCancel = + e is CancellationException || + (e is StreamResetException && e.errorCode == ErrorCode.CANCEL) if (shouldCancel) { notifier.cancel() } else { @@ -125,21 +136,26 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL" const val EXTRA_DOWNLOAD_TITLE = "DOWNLOAD_TITLE" - fun start(context: Context, url: String, title: String? = null) { - val constraints = Constraints( - requiredNetworkType = NetworkType.CONNECTED, - ) - - val request = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .addTag(TAG) - .setInputData( - workDataOf( - EXTRA_DOWNLOAD_URL to url, - EXTRA_DOWNLOAD_TITLE to title, - ), + fun start( + context: Context, + url: String, + title: String? = null, + ) { + val constraints = + Constraints( + requiredNetworkType = NetworkType.CONNECTED, ) - .build() + + val request = + OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .addTag(TAG) + .setInputData( + workDataOf( + EXTRA_DOWNLOAD_URL to url, + EXTRA_DOWNLOAD_TITLE to title, + ), + ).build() context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt index 35147790f3..1f2028369b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt @@ -17,8 +17,9 @@ import tachiyomi.core.common.i18n.stringResource import tachiyomi.domain.release.model.Release import tachiyomi.i18n.MR -internal class AppUpdateNotifier(private val context: Context) { - +internal class AppUpdateNotifier( + private val context: Context, +) { private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_APP_UPDATE) /** @@ -36,21 +37,23 @@ internal class AppUpdateNotifier(private val context: Context) { @SuppressLint("LaunchActivityFromNotification") fun promptUpdate(release: Release) { - val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast( - context, - release.getDownloadLink(), - release.version, - ) - - val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - PendingIntent.getActivity( + val updateIntent = + NotificationReceiver.downloadAppUpdatePendingBroadcast( context, - release.hashCode(), - this, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + release.getDownloadLink(), + release.version, ) - } + + val releaseIntent = + Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + PendingIntent.getActivity( + context, + release.hashCode(), + this, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } with(notificationBuilder) { setContentTitle(context.stringResource(MR.strings.update_check_notification_update_available)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt index 9997a07118..5b66909867 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt @@ -45,8 +45,9 @@ import uy.kohesive.injekt.api.addSingleton import uy.kohesive.injekt.api.addSingletonFactory import uy.kohesive.injekt.api.get -class AppModule(val app: Application) : InjektModule { - +class AppModule( + val app: Application, +) : InjektModule { override fun InjektRegistrar.registerInjectables() { addSingleton(app) @@ -55,37 +56,45 @@ class AppModule(val app: Application) : InjektModule { schema = Database.Schema, context = app, name = "tachiyomi.db", - factory = if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Support database inspector in Android Studio - FrameworkSQLiteOpenHelperFactory() - } else { - RequerySQLiteOpenHelperFactory() - }, - callback = object : AndroidSqliteDriver.Callback(Database.Schema) { - override fun onOpen(db: SupportSQLiteDatabase) { - super.onOpen(db) - setPragma(db, "foreign_keys = ON") - setPragma(db, "journal_mode = WAL") - setPragma(db, "synchronous = NORMAL") - } - private fun setPragma(db: SupportSQLiteDatabase, pragma: String) { - val cursor = db.query("PRAGMA $pragma") - cursor.moveToFirst() - cursor.close() - } - }, + factory = + if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Support database inspector in Android Studio + FrameworkSQLiteOpenHelperFactory() + } else { + RequerySQLiteOpenHelperFactory() + }, + callback = + object : AndroidSqliteDriver.Callback(Database.Schema) { + override fun onOpen(db: SupportSQLiteDatabase) { + super.onOpen(db) + setPragma(db, "foreign_keys = ON") + setPragma(db, "journal_mode = WAL") + setPragma(db, "synchronous = NORMAL") + } + + private fun setPragma( + db: SupportSQLiteDatabase, + pragma: String, + ) { + val cursor = db.query("PRAGMA $pragma") + cursor.moveToFirst() + cursor.close() + } + }, ) } addSingletonFactory { Database( driver = get(), - historyAdapter = History.Adapter( - last_readAdapter = DateColumnAdapter, - ), - mangasAdapter = Mangas.Adapter( - genreAdapter = StringListColumnAdapter, - update_strategyAdapter = UpdateStrategyColumnAdapter, - ), + historyAdapter = + History.Adapter( + last_readAdapter = DateColumnAdapter, + ), + mangasAdapter = + Mangas.Adapter( + genreAdapter = StringListColumnAdapter, + update_strategyAdapter = UpdateStrategyColumnAdapter, + ), ) } addSingletonFactory { AndroidDatabaseHandler(get(), get()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt index b56c16cae3..f8b0888dba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -21,8 +21,9 @@ import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addSingletonFactory import uy.kohesive.injekt.api.get -class PreferenceModule(val app: Application) : InjektModule { - +class PreferenceModule( + val app: Application, +) : InjektModule { override fun InjektRegistrar.registerInjectables() { addSingletonFactory { AndroidPreferenceStore(app) 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 968263b5dd..047b5c8461 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -44,7 +44,6 @@ class ExtensionManager( private val preferences: SourcePreferences = Injekt.get(), private val trustExtension: TrustExtension = Injekt.get(), ) { - val scope = CoroutineScope(SupervisorJob()) private val _isInitialized = MutableStateFlow(false) @@ -79,15 +78,17 @@ class ExtensionManager( private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet() fun getAppIconForSource(sourceId: Long): Drawable? { - val pkgName = _installedExtensionsMapFlow.value.values - .find { ext -> - ext.sources.any { it.id == sourceId } - } - ?.pkgName - ?: return null + val pkgName = + _installedExtensionsMapFlow.value.values + .find { ext -> + ext.sources.any { it.id == sourceId } + }?.pkgName + ?: return null return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { - ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo + ExtensionLoader + .getExtensionPackageInfoFromPkgName(context, pkgName)!! + .applicationInfo .loadIcon(context.packageManager) } } @@ -96,9 +97,10 @@ class ExtensionManager( private fun setupAvailableExtensionsSourcesDataMap(extensions: List) { if (extensions.isEmpty()) return - availableExtensionsSourcesData = extensions - .flatMap { ext -> ext.sources.map { it.toStubSource() } } - .associateBy { it.id } + availableExtensionsSourcesData = + extensions + .flatMap { ext -> ext.sources.map { it.toStubSource() } } + .associateBy { it.id } } fun getSourceData(id: Long) = availableExtensionsSourcesData[id] @@ -109,13 +111,15 @@ class ExtensionManager( private fun initExtensions() { val extensions = ExtensionLoader.loadExtensions(context) - _installedExtensionsMapFlow.value = extensions - .filterIsInstance() - .associate { it.extension.pkgName to it.extension } + _installedExtensionsMapFlow.value = + extensions + .filterIsInstance() + .associate { it.extension.pkgName to it.extension } - _untrustedExtensionsMapFlow.value = extensions - .filterIsInstance() - .associate { it.extension.pkgName to it.extension } + _untrustedExtensionsMapFlow.value = + extensions + .filterIsInstance() + .associate { it.extension.pkgName to it.extension } _isInitialized.value = true } @@ -124,13 +128,14 @@ class ExtensionManager( * Finds the available extensions in the [api] and updates [_availableExtensionsMapFlow]. */ suspend fun findAvailableExtensions() { - val extensions: List = try { - api.findExtensions() - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - withUIContext { context.toast(MR.strings.extension_api_error) } - emptyList() - } + val extensions: List = + try { + api.findExtensions() + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + withUIContext { context.toast(MR.strings.extension_api_error) } + emptyList() + } enableAdditionalSubLanguages(extensions) @@ -154,16 +159,18 @@ class ExtensionManager( } // Use the source lang as some aren't present on the extension level. - val availableLanguages = extensions - .flatMap(Extension.Available::sources) - .distinctBy(Extension.Available.Source::lang) - .map(Extension.Available.Source::lang) + val availableLanguages = + extensions + .flatMap(Extension.Available::sources) + .distinctBy(Extension.Available.Source::lang) + .map(Extension.Available.Source::lang) val deviceLanguage = Locale.getDefault().language val defaultLanguages = preferences.enabledLanguages().defaultValue() - val languagesToEnable = availableLanguages.filter { - it != deviceLanguage && it.startsWith(deviceLanguage) - } + val languagesToEnable = + availableLanguages.filter { + it != deviceLanguage && it.startsWith(deviceLanguage) + } preferences.enabledLanguages().set(defaultLanguages + languagesToEnable) subLanguagesEnabledOnFirstRun = true @@ -191,14 +198,16 @@ class ExtensionManager( } else if (availableExt != null) { val hasUpdate = extension.updateExists(availableExt) if (extension.hasUpdate != hasUpdate) { - installedExtensionsMap[pkgName] = extension.copy( - hasUpdate = hasUpdate, - repoUrl = availableExt.repoUrl, - ) + installedExtensionsMap[pkgName] = + extension.copy( + hasUpdate = hasUpdate, + repoUrl = availableExt.repoUrl, + ) } else { - installedExtensionsMap[pkgName] = extension.copy( - repoUrl = availableExt.repoUrl, - ) + installedExtensionsMap[pkgName] = + extension.copy( + repoUrl = availableExt.repoUrl, + ) } changed = true } @@ -216,9 +225,8 @@ class ExtensionManager( * * @param extension The extension to be installed. */ - fun installExtension(extension: Extension.Available): Flow { - return installer.downloadAndInstall(api.getApkUrl(extension), extension) - } + fun installExtension(extension: Extension.Available): Flow = + installer.downloadAndInstall(api.getApkUrl(extension), extension) /** * Returns a flow of the installation process for the given extension. It will complete @@ -245,7 +253,10 @@ class ExtensionManager( installer.updateInstallStep(downloadId, InstallStep.Installing) } - fun updateInstallStep(downloadId: Long, step: InstallStep) { + fun updateInstallStep( + downloadId: Long, + step: InstallStep, + ) { installer.updateInstallStep(downloadId, step) } @@ -271,7 +282,8 @@ class ExtensionManager( _untrustedExtensionsMapFlow.value -= extension.pkgName - ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) + ExtensionLoader + .loadExtensionFromPkgName(context, extension.pkgName) .let { it as? LoadResult.Success } ?.let { registerNewExtension(it.extension) } } @@ -310,7 +322,6 @@ class ExtensionManager( * Listener which receives events of the extensions being installed, updated or removed. */ private inner class InstallationListener : ExtensionInstallReceiver.Listener { - override fun onExtensionInstalled(extension: Extension.Installed) { registerNewExtension(extension.withUpdateCheck()) updatePendingUpdatesCount() @@ -337,18 +348,18 @@ class ExtensionManager( /** * Extension method to set the update field of an installed extension. */ - private fun Extension.Installed.withUpdateCheck(): Extension.Installed { - return if (updateExists()) { + private fun Extension.Installed.withUpdateCheck(): Extension.Installed = + if (updateExists()) { copy(hasUpdate = true) } else { this } - } private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean { - val availableExt = availableExtension - ?: _availableExtensionsMapFlow.value[pkgName] - ?: return false + val availableExt = + availableExtension + ?: _availableExtensionsMapFlow.value[pkgName] + ?: return false return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) } @@ -363,7 +374,8 @@ class ExtensionManager( private operator fun Map.plus(extension: T) = plus(extension.pkgName to extension) - private fun StateFlow>.mapExtensions(scope: CoroutineScope): StateFlow> { - return map { it.values.toList() }.stateIn(scope, SharingStarted.Lazily, value.values.toList()) - } + private fun StateFlow>.mapExtensions(scope: CoroutineScope): StateFlow> = + map { + it.values.toList() + }.stateIn(scope, SharingStarted.Lazily, value.values.toList()) } 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 71e6251dbf..2fcac52242 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 @@ -26,7 +26,6 @@ import java.time.Instant import kotlin.time.Duration.Companion.days internal class ExtensionApi { - private val networkService: NetworkHelper by injectLazy() private val preferenceStore: PreferenceStore by injectLazy() private val getExtensionRepo: GetExtensionRepo by injectLazy() @@ -38,21 +37,22 @@ internal class ExtensionApi { preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0) } - suspend fun findExtensions(): List { - return withIOContext { - getExtensionRepo.getAll() + suspend fun findExtensions(): List = + withIOContext { + getExtensionRepo + .getAll() .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() + val response = + networkService.client + .newCall(GET("$repoBaseUrl/index.min.json")) + .awaitSuccess() with(json) { response @@ -79,15 +79,18 @@ internal class ExtensionApi { // Update extension repo details updateExtensionRepo.awaitAll() - val extensions = if (fromAvailableExtensionList) { - extensionManager.availableExtensionsFlow.value - } else { - findExtensions().also { lastExtCheck.set(Instant.now().toEpochMilli()) } - } + val extensions = + if (fromAvailableExtensionList) { + extensionManager.availableExtensionsFlow.value + } else { + findExtensions().also { lastExtCheck.set(Instant.now().toEpochMilli()) } + } - val installedExtensions = ExtensionLoader.loadExtensions(context) - .filterIsInstance() - .map { it.extension } + val installedExtensions = + ExtensionLoader + .loadExtensions(context) + .filterIsInstance() + .map { it.extension } val extensionsWithUpdate = mutableListOf() for (installedExt in installedExtensions) { @@ -108,13 +111,12 @@ internal class ExtensionApi { return extensionsWithUpdate } - private fun List.toExtensions(repoUrl: String): List { - return this + private fun List.toExtensions(repoUrl: String): List = + this .filter { val libVersion = it.extractLibVersion() libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX - } - .map { + }.map { Extension.Available( name = it.name.substringAfter("Tachiyomi: "), pkgName = it.pkg, @@ -129,15 +131,10 @@ internal class ExtensionApi { repoUrl = repoUrl, ) } - } - fun getApkUrl(extension: Extension.Available): String { - return "${extension.repoUrl}/apk/${extension.apkName}" - } + fun getApkUrl(extension: Extension.Available): String = "${extension.repoUrl}/apk/${extension.apkName}" - private fun ExtensionJsonObject.extractLibVersion(): Double { - return version.substringBeforeLast('.').toDouble() - } + private fun ExtensionJsonObject.extractLibVersion(): Double = version.substringBeforeLast('.').toDouble() } @Serializable diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionUpdateNotifier.kt index ff764271ee..71bcf57808 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionUpdateNotifier.kt @@ -10,8 +10,9 @@ import eu.kanade.tachiyomi.util.system.notify import tachiyomi.core.common.i18n.pluralStringResource import tachiyomi.i18n.MR -class ExtensionUpdateNotifier(private val context: Context) { - +class ExtensionUpdateNotifier( + private val context: Context, +) { fun promptUpdates(names: List) { context.notify( Notifications.ID_UPDATES_TO_EXTS, 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 0e1ee3684e..813dbf73d4 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 @@ -17,19 +17,24 @@ import java.util.concurrent.atomic.AtomicReference /** * Base implementation class for extension installer. To be used inside a foreground [Service]. */ -abstract class Installer(private val service: Service) { - +abstract class Installer( + private val service: Service, +) { private val extensionManager: ExtensionManager by injectLazy() private var waitingInstall = AtomicReference(null) private val queue = Collections.synchronizedList(mutableListOf()) - private val cancelReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return - cancelQueue(downloadId) + private val cancelReceiver = + object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return + cancelQueue(downloadId) + } } - } /** * Installer readiness. If false, queue check will not run. @@ -44,7 +49,10 @@ abstract class Installer(private val service: Service) { * @param downloadId Download ID as known by [ExtensionManager] * @param uri Uri of APK to install */ - fun addToQueue(downloadId: Long, uri: Uri) { + fun addToQueue( + downloadId: Long, + uri: Uri, + ) { queue.add(Entry(downloadId, uri)) checkQueue() } @@ -67,9 +75,7 @@ abstract class Installer(private val service: Service) { * * @return true if this entry can be removed from queue. */ - open fun cancelEntry(entry: Entry): Boolean { - return true - } + open fun cancelEntry(entry: Entry): Boolean = true /** * Tells the queue to continue processing the next entry and updates the install step @@ -145,7 +151,10 @@ abstract class Installer(private val service: Service) { * @param downloadId Download ID as known by [ExtensionManager] * @param uri Uri of APK to install */ - data class Entry(val downloadId: Long, val uri: Uri) + data class Entry( + val downloadId: Long, + val uri: Uri, + ) init { val filter = IntentFilter(ACTION_CANCEL_QUEUE) @@ -161,7 +170,10 @@ abstract class Installer(private val service: Service) { * * @param downloadId Download ID as known by [ExtensionManager] */ - fun cancelInstallQueue(context: Context, downloadId: Long) { + fun cancelInstallQueue( + context: Context, + downloadId: Long, + ) { val intent = Intent(ACTION_CANCEL_QUEUE) intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId) LocalBroadcastManager.getInstance(context).sendBroadcast(intent) 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 d3aef3b180..2d5c50d13e 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 @@ -17,45 +17,54 @@ import eu.kanade.tachiyomi.util.system.getUriSize import logcat.LogPriority import tachiyomi.core.common.util.system.logcat -class PackageInstallerInstaller(private val service: Service) : Installer(service) { - +class PackageInstallerInstaller( + private val service: Service, +) : Installer(service) { private val packageInstaller = service.packageManager.packageInstaller - private val packageActionReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) { - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userAction = intent.getParcelableExtraCompat(Intent.EXTRA_INTENT) - ?.run { - // Doesn't actually needed as the receiver is actually not exported - // But the warnings can't be suppressed without this - IntentSanitizer.Builder() - .allowAction(this.action!!) - .allowExtra(PackageInstaller.EXTRA_SESSION_ID) { id -> id == activeSession?.second } - .allowAnyComponent() - .allowPackage { - // There is no way to check the actual installer name so allow all. - true + private val packageActionReceiver = + object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) { + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + val userAction = + intent + .getParcelableExtraCompat(Intent.EXTRA_INTENT) + ?.run { + // Doesn't actually needed as the receiver is actually not exported + // But the warnings can't be suppressed without this + IntentSanitizer + .Builder() + .allowAction(this.action!!) + .allowExtra(PackageInstaller.EXTRA_SESSION_ID) { id -> + id == + activeSession?.second + }.allowAnyComponent() + .allowPackage { + // There is no way to check the actual installer name so allow all. + true + }.build() + .sanitizeByFiltering(this) } - .build() - .sanitizeByFiltering(this) + if (userAction == null) { + logcat(LogPriority.ERROR) { "Fatal error for $intent" } + continueQueue(InstallStep.Error) + return } - if (userAction == null) { - logcat(LogPriority.ERROR) { "Fatal error for $intent" } - continueQueue(InstallStep.Error) - return + userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + service.startActivity(userAction) } - userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - service.startActivity(userAction) - } - PackageInstaller.STATUS_FAILURE_ABORTED -> { - continueQueue(InstallStep.Idle) + PackageInstaller.STATUS_FAILURE_ABORTED -> { + continueQueue(InstallStep.Idle) + } + PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed) + else -> continueQueue(InstallStep.Error) } - PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed) - else -> continueQueue(InstallStep.Error) } } - } private var activeSession: Pair? = null @@ -83,12 +92,14 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic session.fsync(outputStream) } - val intentSender = PendingIntent.getBroadcast( - service, - activeSession!!.second, - Intent(INSTALL_ACTION).setPackage(service.packageName), - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0, - ).intentSender + val intentSender = + PendingIntent + .getBroadcast( + service, + activeSession!!.second, + Intent(INSTALL_ACTION).setPackage(service.packageName), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0, + ).intentSender 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 24a8cb377c..bd3c08d4bd 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 @@ -18,28 +18,34 @@ import tachiyomi.i18n.MR import java.io.BufferedReader import java.io.InputStream -class ShizukuInstaller(private val service: Service) : Installer(service) { - +class ShizukuInstaller( + private val service: Service, +) : Installer(service) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val shizukuDeadListener = Shizuku.OnBinderDeadListener { - logcat { "Shizuku was killed prematurely" } - service.stopSelf() - } + private val shizukuDeadListener = + Shizuku.OnBinderDeadListener { + logcat { "Shizuku was killed prematurely" } + service.stopSelf() + } - private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener { - override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { - if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) { - if (grantResult == PackageManager.PERMISSION_GRANTED) { - ready = true - checkQueue() - } else { - service.stopSelf() + private val shizukuPermissionListener = + object : Shizuku.OnRequestPermissionResultListener { + override fun onRequestPermissionResult( + requestCode: Int, + grantResult: Int, + ) { + if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) { + if (grantResult == PackageManager.PERMISSION_GRANTED) { + ready = true + checkQueue() + } else { + service.stopSelf() + } + Shizuku.removeRequestPermissionResultListener(this) } - Shizuku.removeRequestPermissionResultListener(this) } } - } override var ready = false @@ -88,7 +94,10 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { super.onDestroy() } - private fun exec(command: String, stdin: InputStream? = null): ShellResult { + private fun exec( + command: String, + stdin: InputStream? = null, + ): ShellResult { @Suppress("DEPRECATION") val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null) if (stdin != null) { @@ -99,24 +108,28 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { return ShellResult(resultCode, output) } - private data class ShellResult(val resultCode: Int, val out: String) + private data class ShellResult( + val resultCode: Int, + val out: String, + ) init { Shizuku.addBinderDeadListener(shizukuDeadListener) - ready = if (Shizuku.pingBinder()) { - if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { - true + ready = + if (Shizuku.pingBinder()) { + if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { + true + } else { + Shizuku.addRequestPermissionResultListener(shizukuPermissionListener) + Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE) + false + } } else { - Shizuku.addRequestPermissionResultListener(shizukuPermissionListener) - Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE) + logcat(LogPriority.ERROR) { "Shizuku is not ready to use" } + service.toast(MR.strings.ext_installer_shizuku_stopped) + service.stopSelf() false } - } else { - logcat(LogPriority.ERROR) { "Shizuku is not ready to use" } - service.toast(MR.strings.ext_installer_shizuku_stopped) - service.stopSelf() - false - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index a8e80d0a52..310f556ac5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.source.Source import tachiyomi.domain.source.model.StubSource sealed class Extension { - abstract val name: String abstract val pkgName: String abstract val versionName: String @@ -44,20 +43,18 @@ sealed class Extension { val iconUrl: String, val repoUrl: String, ) : Extension() { - data class Source( val id: Long, val lang: String, val name: String, val baseUrl: String, ) { - fun toStubSource(): StubSource { - return StubSource( + fun toStubSource(): StubSource = + StubSource( id = this.id, lang = this.lang, name = this.name, ) - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt index d1049689e2..46cca6a574 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt @@ -1,9 +1,13 @@ package eu.kanade.tachiyomi.extension.model enum class InstallStep { - Idle, Pending, Downloading, Installing, Installed, Error; + Idle, + Pending, + Downloading, + Installing, + Installed, + Error, + ; - fun isCompleted(): Boolean { - return this == Installed || this == Error || this == Idle - } + fun isCompleted(): Boolean = this == Installed || this == Error || this == Idle } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt index 98da1710ef..1a87371042 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt @@ -1,7 +1,13 @@ package eu.kanade.tachiyomi.extension.model sealed interface LoadResult { - data class Success(val extension: Extension.Installed) : LoadResult - data class Untrusted(val extension: Extension.Untrusted) : LoadResult + data class Success( + val extension: Extension.Installed, + ) : LoadResult + + data class Untrusted( + val extension: Extension.Untrusted, + ) : LoadResult + data object Error : LoadResult } 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 37ac84e44c..4d58b7fb91 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 @@ -16,7 +16,6 @@ import kotlin.time.Duration.Companion.seconds * with [startActivityForResult], which we need to update the UI. */ class ExtensionInstallActivity : Activity() { - // MIUI package installer bug workaround private var ignoreUntil = 0L private var ignoreResult = false @@ -25,10 +24,11 @@ class ExtensionInstallActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE) - .setDataAndType(intent.data, intent.type) - .putExtra(Intent.EXTRA_RETURN_RESULT, true) - .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val installIntent = + Intent(Intent.ACTION_INSTALL_PACKAGE) + .setDataAndType(intent.data, intent.type) + .putExtra(Intent.EXTRA_RETURN_RESULT, true) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) if (hasMiuiPackageInstaller) { ignoreResult = true @@ -44,7 +44,11 @@ class ExtensionInstallActivity : Activity() { } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + override fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent?, + ) { if (ignoreResult && System.nanoTime() < ignoreUntil) { hasIgnoredResult = true return @@ -66,11 +70,12 @@ class ExtensionInstallActivity : Activity() { private fun checkInstallationResult(resultCode: Int) { val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID) val extensionManager = Injekt.get() - val newStep = when (resultCode) { - RESULT_OK -> InstallStep.Installed - RESULT_CANCELED -> InstallStep.Idle - else -> InstallStep.Error - } + val newStep = + when (resultCode) { + RESULT_OK -> InstallStep.Installed + RESULT_CANCELED -> InstallStep.Idle + else -> InstallStep.Error + } extensionManager.updateInstallStep(downloadId, newStep) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index a0ccb23fb2..cd5c1c92f7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -21,29 +21,34 @@ import tachiyomi.core.common.util.system.logcat * * @param listener The listener that should be notified of extension installation events. */ -internal class ExtensionInstallReceiver(private val listener: Listener) : BroadcastReceiver() { - +internal class ExtensionInstallReceiver( + private val listener: Listener, +) : BroadcastReceiver() { val scope = CoroutineScope(SupervisorJob()) fun register(context: Context) { ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED) } - private val filter = IntentFilter().apply { - addAction(Intent.ACTION_PACKAGE_ADDED) - addAction(Intent.ACTION_PACKAGE_REPLACED) - addAction(Intent.ACTION_PACKAGE_REMOVED) - addAction(ACTION_EXTENSION_ADDED) - addAction(ACTION_EXTENSION_REPLACED) - addAction(ACTION_EXTENSION_REMOVED) - addDataScheme("package") - } + private val filter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REPLACED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addAction(ACTION_EXTENSION_ADDED) + addAction(ACTION_EXTENSION_REPLACED) + addAction(ACTION_EXTENSION_REMOVED) + addDataScheme("package") + } /** * Called when one of the events of the [filter] is received. When the package is an extension, * it's loaded in background and it notifies the [listener] when finished. */ - override fun onReceive(context: Context, intent: Intent?) { + override fun onReceive( + context: Context, + intent: Intent?, + ) { if (intent == null) return when (intent.action) { @@ -83,9 +88,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc * * @param intent The intent that triggered the event. */ - private fun isReplacing(intent: Intent): Boolean { - return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) - } + private fun isReplacing(intent: Intent): Boolean = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) /** * Returns the extension triggered by the given intent. @@ -93,7 +96,10 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc * @param context The application context. * @param intent The intent containing the package name of the extension. */ - private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult { + private suspend fun getExtensionFromIntent( + context: Context, + intent: Intent?, + ): LoadResult { val pkgName = getPackageNameFromIntent(intent) if (pkgName == null) { logcat(LogPriority.WARN) { "Package name not found" } @@ -114,8 +120,11 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc */ interface Listener { fun onExtensionInstalled(extension: Extension.Installed) + fun onExtensionUpdated(extension: Extension.Installed) + fun onExtensionUntrusted(extension: Extension.Untrusted) + fun onPackageUninstalled(pkgName: String) } @@ -124,19 +133,32 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc private const val ACTION_EXTENSION_REPLACED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REPLACED" private const val ACTION_EXTENSION_REMOVED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REMOVED" - fun notifyAdded(context: Context, pkgName: String) { + fun notifyAdded( + context: Context, + pkgName: String, + ) { notify(context, pkgName, ACTION_EXTENSION_ADDED) } - fun notifyReplaced(context: Context, pkgName: String) { + fun notifyReplaced( + context: Context, + pkgName: String, + ) { notify(context, pkgName, ACTION_EXTENSION_REPLACED) } - fun notifyRemoved(context: Context, pkgName: String) { + fun notifyRemoved( + context: Context, + pkgName: String, + ) { notify(context, pkgName, ACTION_EXTENSION_REMOVED) } - private fun notify(context: Context, pkgName: String, action: String) { + private fun notify( + context: Context, + pkgName: String, + action: String, + ) { Intent(action).apply { data = Uri.parse("package:$pkgName") `package` = context.packageName 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 12bdb61d0a..803a558c3e 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 @@ -20,22 +20,26 @@ import tachiyomi.core.common.util.system.logcat import tachiyomi.i18n.MR class ExtensionInstallService : Service() { - private var installer: Installer? = null override fun onCreate() { - val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) { - setSmallIcon(R.drawable.ic_mihon) - setAutoCancel(false) - setOngoing(true) - setShowWhen(false) - setContentTitle(stringResource(MR.strings.ext_install_service_notif)) - setProgress(100, 100, true) - }.build() + val notification = + notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) { + setSmallIcon(R.drawable.ic_mihon) + setAutoCancel(false) + setOngoing(true) + setShowWhen(false) + setContentTitle(stringResource(MR.strings.ext_install_service_notif)) + setProgress(100, 100, true) + }.build() startForeground(Notifications.ID_EXTENSION_INSTALLER, notification) } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + override fun onStartCommand( + intent: Intent?, + flags: Int, + startId: Int, + ): Int { val uri = intent?.data val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L } val installerUsed = intent?.getSerializableExtraCompat(EXTRA_INSTALLER) @@ -45,15 +49,16 @@ class ExtensionInstallService : Service() { } if (installer == null) { - installer = when (installerUsed) { - BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this) - BasePreferences.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this) - else -> { - logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" } - stopSelf() - return START_NOT_STICKY + installer = + when (installerUsed) { + BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this) + BasePreferences.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this) + else -> { + logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" } + stopSelf() + return START_NOT_STICKY + } } - } } installer!!.addToQueue(id, uri) return START_NOT_STICKY @@ -74,11 +79,10 @@ class ExtensionInstallService : Service() { downloadId: Long, uri: Uri, installer: BasePreferences.ExtensionInstaller, - ): Intent { - return Intent(context, ExtensionInstallService::class.java) + ): Intent = + Intent(context, ExtensionInstallService::class.java) .setDataAndType(uri, ExtensionInstaller.APK_MIME) .putExtra(EXTRA_DOWNLOAD_ID, downloadId) .putExtra(EXTRA_INSTALLER, installer) - } } } 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 b71974a301..69de967135 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 @@ -39,8 +39,9 @@ import kotlin.time.Duration.Companion.seconds * * @param context The application context. */ -internal class ExtensionInstaller(private val context: Context) { - +internal class ExtensionInstaller( + private val context: Context, +) { /** * The system's download manager */ @@ -68,7 +69,10 @@ internal class ExtensionInstaller(private val context: Context) { * @param url The url of the apk. * @param extension The extension to install. */ - fun downloadAndInstall(url: String, extension: Extension): Flow { + fun downloadAndInstall( + url: String, + extension: Extension, + ): Flow { val pkgName = extension.pkgName val oldDownload = activeDownloads[pkgName] @@ -80,11 +84,13 @@ internal class ExtensionInstaller(private val context: Context) { downloadReceiver.register() 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) + 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) val id = downloadManager.enqueue(request) activeDownloads[pkgName] = id @@ -93,26 +99,28 @@ internal class ExtensionInstaller(private val context: Context) { 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 + val pollStatusFlow = + downloadStatusFlow(id).mapNotNull { downloadStatus -> + // Map to our model + when (downloadStatus) { + DownloadManager.STATUS_PENDING -> InstallStep.Pending + DownloadManager.STATUS_RUNNING -> InstallStep.Downloading + else -> null + } } - } - 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) + 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) + } } - } } /** @@ -121,43 +129,49 @@ internal class ExtensionInstaller(private val context: Context) { * * @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)) - } + 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) + emit(downloadStatus) - // Stop polling when the download fails or finishes - if ( - downloadStatus == DownloadManager.STATUS_SUCCESSFUL || - downloadStatus == DownloadManager.STATUS_FAILED - ) { - return@flow - } + // Stop polling when the download fails or finishes + if ( + downloadStatus == DownloadManager.STATUS_SUCCESSFUL || + downloadStatus == DownloadManager.STATUS_FAILED + ) { + return@flow + } - delay(1.seconds) + delay(1.seconds) + } } - } - // Ignore duplicate results - .distinctUntilChanged() + // Ignore duplicate results + .distinctUntilChanged() /** * Starts an intent to install the extension at the given uri. * * @param uri The uri of the extension to install. */ - fun installApk(downloadId: Long, uri: Uri) { + fun installApk( + downloadId: Long, + uri: Uri, + ) { when (val installer = extensionInstaller.get()) { BasePreferences.ExtensionInstaller.LEGACY -> { - val intent = Intent(context, ExtensionInstallActivity::class.java) - .setDataAndType(uri, APK_MIME) - .putExtra(EXTRA_DOWNLOAD_ID, downloadId) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) + val intent = + Intent(context, ExtensionInstallActivity::class.java) + .setDataAndType(uri, APK_MIME) + .putExtra(EXTRA_DOWNLOAD_ID, downloadId) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) context.startActivity(intent) } @@ -214,8 +228,9 @@ internal class ExtensionInstaller(private val context: Context) { fun uninstallApk(pkgName: String) { if (context.isPackageInstalled(pkgName)) { @Suppress("DEPRECATION") - val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri()) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val intent = + Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri()) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } else { ExtensionLoader.uninstallPrivateExtension(context, pkgName) @@ -229,7 +244,10 @@ internal class ExtensionInstaller(private val context: Context) { * @param downloadId The id of the download. * @param step New install step. */ - fun updateInstallStep(downloadId: Long, step: InstallStep) { + fun updateInstallStep( + downloadId: Long, + step: InstallStep, + ) { downloadsStateFlows[downloadId]?.let { it.value = step } } @@ -253,7 +271,6 @@ internal class ExtensionInstaller(private val context: Context) { * Receiver that listens to download status events. */ private inner class DownloadCompletionReceiver : BroadcastReceiver() { - /** * Whether this receiver is currently registered. */ @@ -284,7 +301,10 @@ internal class ExtensionInstaller(private val context: Context) { * 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?) { + 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 @@ -302,9 +322,11 @@ internal class ExtensionInstaller(private val context: Context) { 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) + val localUri = + cursor + .getString( + cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI), + ).removePrefix(FILE_SCHEME) installApk(id, File(localUri).getUriCompat(context)) } 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 50ab94279b..6283d20e5f 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 @@ -39,7 +39,6 @@ import java.io.File * one with higher version code will be used. */ internal object ExtensionLoader { - private val preferences: SourcePreferences by injectLazy() private val trustExtension: TrustExtension by injectLazy() private val loadNsfwSource by lazy { @@ -54,18 +53,24 @@ internal object ExtensionLoader { const val LIB_VERSION_MAX = 1.5 @Suppress("DEPRECATION") - private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or - PackageManager.GET_META_DATA or - PackageManager.GET_SIGNATURES or - (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0) + private val PACKAGE_FLAGS = + PackageManager.GET_CONFIGURATIONS or + PackageManager.GET_META_DATA or + PackageManager.GET_SIGNATURES or + (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0) private const val PRIVATE_EXTENSION_EXTENSION = "ext" private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts") - fun installPrivateExtensionFile(context: Context, file: File): Boolean { - val extension = context.packageManager.getPackageArchiveInfo(file.absolutePath, PACKAGE_FLAGS) - ?.takeIf { isPackageAnExtension(it) } ?: return false + fun installPrivateExtensionFile( + context: Context, + file: File, + ): Boolean { + val extension = + context.packageManager + .getPackageArchiveInfo(file.absolutePath, PACKAGE_FLAGS) + ?.takeIf { isPackageAnExtension(it) } ?: return false val currentExtension = getExtensionPackageInfoFromPkgName(context, extension.packageName) if (currentExtension != null) { @@ -105,7 +110,10 @@ internal object ExtensionLoader { } } - fun uninstallPrivateExtension(context: Context, pkgName: String) { + fun uninstallPrivateExtension( + context: Context, + pkgName: String, + ) { File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION").delete() } @@ -117,53 +125,58 @@ internal object ExtensionLoader { fun loadExtensions(context: Context): List { val pkgManager = context.packageManager - val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) - } else { - pkgManager.getInstalledPackages(PACKAGE_FLAGS) - } + val installedPkgs = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) + } else { + pkgManager.getInstalledPackages(PACKAGE_FLAGS) + } - val sharedExtPkgs = installedPkgs - .asSequence() - .filter { isPackageAnExtension(it) } - .map { ExtensionInfo(packageInfo = it, isShared = true) } - - val privateExtPkgs = getPrivateExtensionDir(context) - .listFiles() - ?.asSequence() - ?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION } - ?.mapNotNull { - // Just in case, since Android 14+ requires them to be read-only - if (it.canWrite()) { - it.setReadOnly() - } + val sharedExtPkgs = + installedPkgs + .asSequence() + .filter { isPackageAnExtension(it) } + .map { ExtensionInfo(packageInfo = it, isShared = true) } + + val privateExtPkgs = + getPrivateExtensionDir(context) + .listFiles() + ?.asSequence() + ?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION } + ?.mapNotNull { + // Just in case, since Android 14+ requires them to be read-only + if (it.canWrite()) { + it.setReadOnly() + } - val path = it.absolutePath - pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS) - ?.apply { applicationInfo.fixBasePaths(path) } - } - ?.filter { isPackageAnExtension(it) } - ?.map { ExtensionInfo(packageInfo = it, isShared = false) } - ?: emptySequence() - - val extPkgs = (sharedExtPkgs + privateExtPkgs) - // Remove duplicates. Shared takes priority than private by default - .distinctBy { it.packageInfo.packageName } - // Compare version number - .mapNotNull { sharedPkg -> - val privatePkg = privateExtPkgs - .singleOrNull { it.packageInfo.packageName == sharedPkg.packageInfo.packageName } - selectExtensionPackage(sharedPkg, privatePkg) - } - .toList() + val path = it.absolutePath + pkgManager + .getPackageArchiveInfo(path, PACKAGE_FLAGS) + ?.apply { applicationInfo.fixBasePaths(path) } + }?.filter { isPackageAnExtension(it) } + ?.map { ExtensionInfo(packageInfo = it, isShared = false) } + ?: emptySequence() + + val extPkgs = + (sharedExtPkgs + privateExtPkgs) + // Remove duplicates. Shared takes priority than private by default + .distinctBy { it.packageInfo.packageName } + // Compare version number + .mapNotNull { sharedPkg -> + val privatePkg = + privateExtPkgs + .singleOrNull { it.packageInfo.packageName == sharedPkg.packageInfo.packageName } + selectExtensionPackage(sharedPkg, privatePkg) + }.toList() if (extPkgs.isEmpty()) return emptyList() // Load each extension concurrently and wait for completion return runBlocking { - val deferred = extPkgs.map { - async { loadExtension(context, it) } - } + val deferred = + extPkgs.map { + async { loadExtension(context, it) } + } deferred.awaitAll() } } @@ -172,7 +185,10 @@ internal object ExtensionLoader { * Attempts to load an extension from the given package name. It checks if the extension * contains the required feature flag before trying to load it. */ - suspend fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult { + suspend fun loadExtensionFromPkgName( + context: Context, + pkgName: String, + ): LoadResult { val extensionPackage = getExtensionInfoFromPkgName(context, pkgName) if (extensionPackage == null) { logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" } @@ -181,38 +197,46 @@ internal object ExtensionLoader { return loadExtension(context, extensionPackage) } - fun getExtensionPackageInfoFromPkgName(context: Context, pkgName: String): PackageInfo? { - return getExtensionInfoFromPkgName(context, pkgName)?.packageInfo - } + fun getExtensionPackageInfoFromPkgName( + context: Context, + pkgName: String, + ): PackageInfo? = getExtensionInfoFromPkgName(context, pkgName)?.packageInfo - private fun getExtensionInfoFromPkgName(context: Context, pkgName: String): ExtensionInfo? { + private fun getExtensionInfoFromPkgName( + context: Context, + pkgName: String, + ): ExtensionInfo? { val privateExtensionFile = File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION") - val privatePkg = if (privateExtensionFile.isFile) { - context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS) - ?.takeIf { isPackageAnExtension(it) } - ?.let { - it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath) - ExtensionInfo( - packageInfo = it, - isShared = false, - ) - } - } else { - null - } + val privatePkg = + if (privateExtensionFile.isFile) { + context.packageManager + .getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS) + ?.takeIf { isPackageAnExtension(it) } + ?.let { + it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath) + ExtensionInfo( + packageInfo = it, + isShared = false, + ) + } + } else { + null + } - val sharedPkg = try { - context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS) - .takeIf { isPackageAnExtension(it) } - ?.let { - ExtensionInfo( - packageInfo = it, - isShared = true, - ) - } - } catch (error: PackageManager.NameNotFoundException) { - null - } + val sharedPkg = + try { + context.packageManager + .getPackageInfo(pkgName, PACKAGE_FLAGS) + .takeIf { isPackageAnExtension(it) } + ?.let { + ExtensionInfo( + packageInfo = it, + isShared = true, + ) + } + } catch (error: PackageManager.NameNotFoundException) { + null + } return selectExtensionPackage(sharedPkg, privatePkg) } @@ -224,7 +248,10 @@ internal object ExtensionLoader { * @param extensionInfo The extension to load. */ @Suppress("LongMethod", "CyclomaticComplexMethod", "ReturnCount") - private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult { + private suspend fun loadExtension( + context: Context, + extensionInfo: ExtensionInfo, + ): LoadResult { val pkgManager = context.packageManager val pkgInfo = extensionInfo.packageInfo val appInfo = pkgInfo.applicationInfo @@ -254,14 +281,15 @@ internal object ExtensionLoader { logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } return LoadResult.Error } else if (!trustExtension.isTrusted(pkgInfo, signatures)) { - val extension = Extension.Untrusted( - extName, - pkgName, - versionName, - versionCode, - libVersion, - signatures.last(), - ) + val extension = + Extension.Untrusted( + extName, + pkgName, + versionName, + versionCode, + libVersion, + signatures.last(), + ) logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" } return LoadResult.Untrusted(extension) } @@ -272,58 +300,64 @@ internal object ExtensionLoader { return LoadResult.Error } - val classLoader = try { - ChildFirstPathClassLoader(appInfo.sourceDir, null, context.classLoader) - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" } - return LoadResult.Error - } - - val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!! - .split(";") - .map { - val sourceClass = it.trim() - if (sourceClass.startsWith(".")) { - pkgInfo.packageName + sourceClass - } else { - sourceClass - } + val classLoader = + try { + ChildFirstPathClassLoader(appInfo.sourceDir, null, context.classLoader) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" } + return LoadResult.Error } - .flatMap { - try { - when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) { - is Source -> listOf(obj) - is SourceFactory -> obj.createSources() - else -> throw Exception("Unknown source class type: ${obj.javaClass}") + + val sources = + appInfo.metaData + .getString(METADATA_SOURCE_CLASS)!! + .split(";") + .map { + val sourceClass = it.trim() + if (sourceClass.startsWith(".")) { + pkgInfo.packageName + sourceClass + } else { + sourceClass + } + }.flatMap { + try { + when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) { + is Source -> listOf(obj) + is SourceFactory -> obj.createSources() + else -> throw Exception("Unknown source class type: ${obj.javaClass}") + } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" } + return LoadResult.Error } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" } - return LoadResult.Error } - } - val langs = sources.filterIsInstance() - .map { it.lang } - .toSet() - val lang = when (langs.size) { - 0 -> "" - 1 -> langs.first() - else -> "all" - } + val langs = + sources + .filterIsInstance() + .map { it.lang } + .toSet() + val lang = + when (langs.size) { + 0 -> "" + 1 -> langs.first() + else -> "all" + } - val extension = Extension.Installed( - name = extName, - pkgName = pkgName, - versionName = versionName, - versionCode = versionCode, - libVersion = libVersion, - lang = lang, - isNsfw = isNsfw, - sources = sources, - pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY), - icon = appInfo.loadIcon(pkgManager), - isShared = extensionInfo.isShared, - ) + val extension = + Extension.Installed( + name = extName, + pkgName = pkgName, + versionName = versionName, + versionCode = versionCode, + libVersion = libVersion, + lang = lang, + isNsfw = isNsfw, + sources = sources, + pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY), + icon = appInfo.loadIcon(pkgManager), + isShared = extensionInfo.isShared, + ) return LoadResult.Success(extension) } @@ -333,7 +367,10 @@ internal object ExtensionLoader { * @param shared extension installed to system * @param private extension installed to data directory */ - private fun selectExtensionPackage(shared: ExtensionInfo?, private: ExtensionInfo?): ExtensionInfo? { + private fun selectExtensionPackage( + shared: ExtensionInfo?, + private: ExtensionInfo?, + ): ExtensionInfo? { when { private == null && shared != null -> return shared shared == null && private != null -> return private @@ -354,9 +391,8 @@ internal object ExtensionLoader { * * @param pkgInfo The package info of the application. */ - private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean { - return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE } - } + private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean = + pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE } /** * Returns the signatures of the package or null if it's not signed. @@ -364,8 +400,8 @@ internal object ExtensionLoader { * @param pkgInfo The package info of the application. * @return List SHA256 digest of the signatures */ - private fun getSignatures(pkgInfo: PackageInfo): List? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + private fun getSignatures(pkgInfo: PackageInfo): List? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val signingInfo = pkgInfo.signingInfo if (signingInfo.hasMultipleSigners()) { signingInfo.apkContentsSigners @@ -375,10 +411,8 @@ internal object ExtensionLoader { } else { @Suppress("DEPRECATION") pkgInfo.signatures - } - ?.map { Hash.sha256(it.toByteArray()) } + }?.map { Hash.sha256(it.toByteArray()) } ?.toList() - } /** * On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't 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 0fa40ba9d6..cbcd285942 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -29,7 +29,6 @@ class AndroidSourceManager( private val extensionManager: ExtensionManager, private val sourceRepository: StubSourceRepository, ) : SourceManager { - private val _isInitialized = MutableStateFlow(false) override val isInitialized: StateFlow = _isInitialized.asStateFlow() @@ -41,23 +40,26 @@ class AndroidSourceManager( private val stubSourcesMap = ConcurrentHashMap() - override val catalogueSources: Flow> = sourcesMapFlow.map { - it.values.filterIsInstance() - } + override val catalogueSources: Flow> = + sourcesMapFlow.map { + it.values.filterIsInstance() + } init { scope.launch { extensionManager.installedExtensionsFlow .collectLatest { extensions -> - val mutableMap = ConcurrentHashMap( - mapOf( - LocalSource.ID to LocalSource( - context, - Injekt.get(), - Injekt.get(), + val mutableMap = + ConcurrentHashMap( + mapOf( + LocalSource.ID to + LocalSource( + context, + Injekt.get(), + Injekt.get(), + ), ), - ), - ) + ) extensions.forEach { extension -> extension.sources.forEach { mutableMap[it.id] = it @@ -70,7 +72,8 @@ class AndroidSourceManager( } scope.launch { - sourceRepository.subscribeAll() + sourceRepository + .subscribeAll() .collectLatest { sources -> val mutableMap = stubSourcesMap.toMutableMap() sources.forEach { @@ -80,15 +83,12 @@ class AndroidSourceManager( } } - override fun get(sourceKey: Long): Source? { - return sourcesMapFlow.value[sourceKey] - } + override fun get(sourceKey: Long): Source? = sourcesMapFlow.value[sourceKey] - override fun getOrStub(sourceKey: Long): Source { - return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { + override fun getOrStub(sourceKey: Long): Source = + sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { runBlocking { createStubSource(sourceKey) } } - } override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance() diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt index bac6ed5cd4..52d56cf49d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt @@ -8,8 +8,11 @@ import uy.kohesive.injekt.api.get fun Source.getNameForMangaInfo(): String { val preferences = Injekt.get() - val enabledLanguages = preferences.enabledLanguages().get() - .filterNot { it in listOf("all", "other") } + val enabledLanguages = + preferences + .enabledLanguages() + .get() + .filterNot { it in listOf("all", "other") } val hasOneActiveLanguages = enabledLanguages.size == 1 val isInEnabledLanguages = lang in enabledLanguages return when { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt index a99fdab4c1..d5aba55634 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt @@ -13,7 +13,6 @@ open class BaseActivity : AppCompatActivity(), SecureActivityDelegate by SecureActivityDelegateImpl(), ThemingDelegate by ThemingDelegateImpl() { - override fun attachBaseContext(newBase: Context) { super.attachBaseContext(newBase.prepareTabletUiContext()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt index c3b94d6395..83cc06229b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt @@ -56,11 +56,12 @@ interface SecureActivityDelegate { // `requireUnlock` can be true on process start or if app was closed in locked state if (!AuthenticatorUtil.isAuthenticating && !requireUnlock) { - requireUnlock = when (val lockDelay = preferences.lockAppAfter().get()) { - -1 -> false // Never - 0 -> true // Always - else -> lastClosedPref.get() + lockDelay * 60_000 <= System.currentTimeMillis() - } + requireUnlock = + when (val lockDelay = preferences.lockAppAfter().get()) { + -1 -> false // Never + 0 -> true // Always + else -> lastClosedPref.get() + lockDelay * 60_000 <= System.currentTimeMillis() + } } lastClosedPref.delete() @@ -72,8 +73,9 @@ interface SecureActivityDelegate { } } -class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObserver { - +class SecureActivityDelegateImpl : + SecureActivityDelegate, + DefaultLifecycleObserver { private lateinit var activity: AppCompatActivity private val preferences: BasePreferences by injectLazy() @@ -97,9 +99,9 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser val incognitoModeFlow = preferences.incognitoMode().changes() combine(secureScreenFlow, incognitoModeFlow) { secureScreen, incognitoMode -> secureScreen == SecurityPreferences.SecureScreenMode.ALWAYS || - secureScreen == SecurityPreferences.SecureScreenMode.INCOGNITO && incognitoMode - } - .onEach(activity.window::setSecureScreen) + secureScreen == SecurityPreferences.SecureScreenMode.INCOGNITO && + incognitoMode + }.onEach(activity.window::setSecureScreen) .launchIn(activity.lifecycleScope) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt index 8b3572f08b..49cc564a15 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt @@ -11,33 +11,37 @@ interface ThemingDelegate { fun applyAppTheme(activity: Activity) companion object { - fun getThemeResIds(appTheme: AppTheme, isAmoled: Boolean): List { - return buildList(2) { + fun getThemeResIds( + appTheme: AppTheme, + isAmoled: Boolean, + ): List = + buildList(2) { add(themeResources.getOrDefault(appTheme, R.style.Theme_Tachiyomi)) if (isAmoled) add(R.style.ThemeOverlay_Tachiyomi_Amoled) } - } } } class ThemingDelegateImpl : ThemingDelegate { override fun applyAppTheme(activity: Activity) { val uiPreferences = Injekt.get() - ThemingDelegate.getThemeResIds(uiPreferences.appTheme().get(), uiPreferences.themeDarkAmoled().get()) + ThemingDelegate + .getThemeResIds(uiPreferences.appTheme().get(), uiPreferences.themeDarkAmoled().get()) .forEach(activity::setTheme) } } -private val themeResources: Map = mapOf( - AppTheme.MONET to R.style.Theme_Tachiyomi_Monet, - AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple, - AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender, - AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk, - AppTheme.NORD to R.style.Theme_Tachiyomi_Nord, - AppTheme.STRAWBERRY_DAIQUIRI to R.style.Theme_Tachiyomi_StrawberryDaiquiri, - AppTheme.TAKO to R.style.Theme_Tachiyomi_Tako, - AppTheme.TEALTURQUOISE to R.style.Theme_Tachiyomi_TealTurquoise, - AppTheme.YINYANG to R.style.Theme_Tachiyomi_YinYang, - AppTheme.YOTSUBA to R.style.Theme_Tachiyomi_Yotsuba, - AppTheme.TIDAL_WAVE to R.style.Theme_Tachiyomi_TidalWave, -) +private val themeResources: Map = + mapOf( + AppTheme.MONET to R.style.Theme_Tachiyomi_Monet, + AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple, + AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender, + AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk, + AppTheme.NORD to R.style.Theme_Tachiyomi_Nord, + AppTheme.STRAWBERRY_DAIQUIRI to R.style.Theme_Tachiyomi_StrawberryDaiquiri, + AppTheme.TAKO to R.style.Theme_Tachiyomi_Tako, + AppTheme.TEALTURQUOISE to R.style.Theme_Tachiyomi_TealTurquoise, + AppTheme.YINYANG to R.style.Theme_Tachiyomi_YinYang, + AppTheme.YOTSUBA to R.style.Theme_Tachiyomi_Yotsuba, + AppTheme.TIDAL_WAVE to R.style.Theme_Tachiyomi_TidalWave, + ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 2ce4685bb2..70962c6880 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -28,7 +28,6 @@ import tachiyomi.presentation.core.i18n.stringResource data class BrowseTab( private val toExtensions: Boolean = false, ) : Tab { - override val options: TabOptions @Composable get() { @@ -55,11 +54,12 @@ data class BrowseTab( TabbedScreen( titleRes = MR.strings.browse, - tabs = persistentListOf( - sourcesTab(), - extensionsTab(extensionsScreenModel), - migrateSourceTab(), - ), + tabs = + persistentListOf( + sourcesTab(), + extensionsTab(extensionsScreenModel), + migrateSourceTab(), + ), startIndex = 1.takeIf { toExtensions }, searchQuery = extensionsState.searchQuery, onChangeSearchQuery = extensionsScreenModel::search, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt index 278c10d367..988f6f6d75 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreen.kt @@ -16,7 +16,6 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.screens.LoadingScreen class ExtensionFilterScreen : Screen() { - @Composable override fun Content() { val context = LocalContext.current diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreenModel.kt index 7cd83923fa..c61160d49e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterScreenModel.kt @@ -29,7 +29,6 @@ class ExtensionFilterScreenModel( private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(), private val toggleLanguage: ToggleLanguage = Injekt.get(), ) : StateScreenModel(ExtensionFilterState.Loading) { - private val _events: Channel = Channel() val events: Flow = _events.receiveAsFlow() @@ -42,8 +41,7 @@ class ExtensionFilterScreenModel( .catch { throwable -> logcat(LogPriority.ERROR, throwable) _events.send(ExtensionFilterEvent.FailedFetchingLanguages) - } - .collectLatest { (extensionLanguages, enabledLanguages) -> + }.collectLatest { (extensionLanguages, enabledLanguages) -> mutableState.update { ExtensionFilterState.Success( languages = extensionLanguages.toImmutableList(), @@ -64,7 +62,6 @@ sealed interface ExtensionFilterEvent { } sealed interface ExtensionFilterState { - @Immutable data object Loading : ExtensionFilterState @@ -73,7 +70,6 @@ sealed interface ExtensionFilterState { val languages: ImmutableList, val enabledLanguages: ImmutableSet = persistentSetOf(), ) : ExtensionFilterState { - val isEmpty: Boolean get() = languages.isEmpty() } 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 feb69c5ce9..7dc8a8aa2b 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 @@ -40,7 +40,6 @@ class ExtensionsScreenModel( private val extensionManager: ExtensionManager = Injekt.get(), private val getExtensions: GetExtensionsByType = Injekt.get(), ) : StateScreenModel(State()) { - private var _currentDownloads = MutableStateFlow>(hashMapOf()) init { @@ -62,14 +61,20 @@ class ExtensionsScreenModel( it.name.contains(input, ignoreCase = true) || it.baseUrl.contains(input, ignoreCase = true) || it.id == input.toLongOrNull() - } || extension.name.contains(input, ignoreCase = true) + } || + extension.name.contains(input, ignoreCase = true) } is Extension.Installed -> { extension.sources.any { it.name.contains(input, ignoreCase = true) || it.id == input.toLongOrNull() || - if (it is HttpSource) { it.baseUrl.contains(input, ignoreCase = true) } else false - } || extension.name.contains(input, ignoreCase = true) + if (it is HttpSource) { + it.baseUrl.contains(input, ignoreCase = true) + } else { + false + } + } || + extension.name.contains(input, ignoreCase = true) } is Extension.Untrusted -> extension.name.contains(input, ignoreCase = true) } @@ -98,37 +103,41 @@ class ExtensionsScreenModel( itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_installed)] = installed + untrusted } - val languagesWithExtensions = _available - .filter(queryFilter(searchQuery)) - .groupBy { it.lang } - .toSortedMap(LocaleHelper.comparator) - .map { (lang, exts) -> - ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to - exts.map(extensionMapper(downloads)) - } + val languagesWithExtensions = + _available + .filter(queryFilter(searchQuery)) + .groupBy { it.lang } + .toSortedMap(LocaleHelper.comparator) + .map { (lang, exts) -> + ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to + exts.map(extensionMapper(downloads)) + } if (languagesWithExtensions.isNotEmpty()) { itemsGroups.putAll(languagesWithExtensions) } itemsGroups - } - .collectLatest { - mutableState.update { state -> - state.copy( - isLoading = false, - items = it, - ) - } + }.collectLatest { + mutableState.update { state -> + state.copy( + isLoading = false, + items = it, + ) } + } } screenModelScope.launchIO { findAvailableExtensions() } - preferences.extensionUpdatesCount().changes() + preferences + .extensionUpdatesCount() + .changes() .onEach { mutableState.update { state -> state.copy(updates = it) } } .launchIn(screenModelScope) - basePreferences.extensionInstaller().changes() + basePreferences + .extensionInstaller() + .changes() .onEach { mutableState.update { state -> state.copy(installer = it) } } .launchIn(screenModelScope) } @@ -141,7 +150,8 @@ class ExtensionsScreenModel( fun updateAllExtensions() { screenModelScope.launchIO { - state.value.items.values.flatten() + state.value.items.values + .flatten() .map { it.extension } .filterIsInstance() .filter { it.hasUpdate } @@ -165,7 +175,10 @@ class ExtensionsScreenModel( extensionManager.cancelInstallUpdateExtension(extension) } - private fun addDownloadState(extension: Extension, installStep: InstallStep) { + private fun addDownloadState( + extension: Extension, + installStep: InstallStep, + ) { _currentDownloads.update { it + Pair(extension.pkgName, installStep) } } @@ -219,8 +232,13 @@ typealias ItemGroups = MutableMap 0 }, searchEnabled = true, - actions = persistentListOf( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_filter), - onClick = { navigator.push(ExtensionFilterScreen()) }, + actions = + persistentListOf( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_filter), + onClick = { navigator.push(ExtensionFilterScreen()) }, + ), + AppBar.OverflowAction( + title = stringResource(MR.strings.label_extension_repos), + onClick = { navigator.push(ExtensionReposScreen()) }, + ), ), - AppBar.OverflowAction( - title = stringResource(MR.strings.label_extension_repos), - onClick = { navigator.push(ExtensionReposScreen()) }, - ), - ), content = { contentPadding, _ -> ExtensionScreen( state = state, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt index 4a4a78cdea..12a796b244 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt @@ -16,7 +16,6 @@ import tachiyomi.presentation.core.screens.LoadingScreen data class ExtensionDetailsScreen( private val pkgName: String, ) : Screen() { - @Composable override fun Content() { val context = LocalContext.current diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt index 4266c4730f..24b8acb6db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt @@ -37,7 +37,6 @@ class ExtensionDetailsScreenModel( private val getExtensionSources: GetExtensionSources = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(), ) : StateScreenModel(State()) { - private val _events: Channel = Channel() val events: Flow = _events.receiveAsFlow() @@ -59,7 +58,8 @@ class ExtensionDetailsScreenModel( launch { state.collectLatest { state -> if (state.extension == null) return@collectLatest - getExtensionSources.subscribe(state.extension) + getExtensionSources + .subscribe(state.extension) .map { it.sortedWith( compareBy( @@ -70,12 +70,10 @@ class ExtensionDetailsScreenModel( }, ), ) - } - .catch { throwable -> + }.catch { throwable -> logcat(LogPriority.ERROR, throwable) mutableState.update { it.copy(_sources = persistentListOf()) } - } - .collectLatest { sources -> + }.collectLatest { sources -> mutableState.update { it.copy(_sources = sources.toImmutableList()) } } } @@ -86,19 +84,21 @@ class ExtensionDetailsScreenModel( fun clearCookies() { val extension = state.value.extension ?: return - val urls = extension.sources - .filterIsInstance() - .mapNotNull { it.baseUrl.takeUnless { url -> url.isEmpty() } } - .distinct() + val urls = + extension.sources + .filterIsInstance() + .mapNotNull { it.baseUrl.takeUnless { url -> url.isEmpty() } } + .distinct() - val cleared = urls.sumOf { - try { - network.cookieJar.remove(it.toHttpUrl()) - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to clear cookies for $it" } - 0 + val cleared = + urls.sumOf { + try { + network.cookieJar.remove(it.toHttpUrl()) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to clear cookies for $it" } + 0 + } } - } logcat { "Cleared $cleared cookies for: ${urls.joinToString()}" } } @@ -113,7 +113,8 @@ class ExtensionDetailsScreenModel( } fun toggleSources(enable: Boolean) { - state.value.extension?.sources + state.value.extension + ?.sources ?.map { it.id } ?.let { toggleSource.await(it, enable) } } @@ -123,7 +124,6 @@ class ExtensionDetailsScreenModel( val extension: Extension.Installed? = null, private val _sources: ImmutableList? = null, ) { - val sources: ImmutableList get() = _sources ?: persistentListOf() 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 c144caa2a4..294838fd27 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 @@ -45,8 +45,9 @@ import tachiyomi.presentation.core.screens.LoadingScreen import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class SourcePreferencesScreen(val sourceId: Long) : Screen() { - +class SourcePreferencesScreen( + val sourceId: Long, +) : Screen() { @Composable override fun Content() { if (!ifSourcesLoaded()) { @@ -68,9 +69,10 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() { ) { contentPadding -> FragmentContainer( fragmentManager = (context as FragmentActivity).supportFragmentManager, - modifier = Modifier - .fillMaxSize() - .padding(contentPadding), + modifier = + Modifier + .fillMaxSize() + .padding(contentPadding), ) { add(it, SourcePreferencesFragment.getInstance(sourceId), null) } @@ -109,17 +111,17 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() { /** Access to package-private method in FragmentManager through reflection */ private fun FragmentManager.onContainerAvailable(view: FragmentContainerView) { - val method = FragmentManager::class.java.getDeclaredMethod( - "onContainerAvailable", - FragmentContainerView::class.java, - ) + val method = + FragmentManager::class.java.getDeclaredMethod( + "onContainerAvailable", + FragmentContainerView::class.java, + ) method.isAccessible = true method.invoke(this, view) } } class SourcePreferencesFragment : PreferenceFragmentCompat() { - override fun getContext(): Context? { val superCtx = super.getContext() ?: return null val tv = TypedValue() @@ -127,7 +129,10 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { return ContextThemeWrapper(superCtx, tv.resourceId) } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + override fun onCreatePreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { preferenceScreen = populateScreen() } @@ -165,10 +170,9 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { companion object { private const val SOURCE_ID = "source_id" - fun getInstance(sourceId: Long): SourcePreferencesFragment { - return SourcePreferencesFragment().apply { + fun getInstance(sourceId: Long): SourcePreferencesFragment = + SourcePreferencesFragment().apply { arguments = bundleOf(SOURCE_ID to sourceId) } - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt index 3064ad60f7..f6b39b4a1f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt @@ -14,18 +14,20 @@ data class MigrationFlag( val titleId: StringResource, ) { companion object { - fun create(flag: Int, defaultSelectionMap: Int, titleId: StringResource): MigrationFlag { - return MigrationFlag( + fun create( + flag: Int, + defaultSelectionMap: Int, + titleId: StringResource, + ): MigrationFlag = + MigrationFlag( flag = flag, isDefaultSelected = defaultSelectionMap and flag != 0, titleId = titleId, ) - } } } object MigrationFlags { - private const val CHAPTERS = 0b00001 private const val CATEGORIES = 0b00010 private const val CUSTOM_COVER = 0b01000 @@ -34,24 +36,19 @@ object MigrationFlags { private val coverCache: CoverCache by injectLazy() private val downloadCache: DownloadCache by injectLazy() - fun hasChapters(value: Int): Boolean { - return value and CHAPTERS != 0 - } + fun hasChapters(value: Int): Boolean = value and CHAPTERS != 0 - fun hasCategories(value: Int): Boolean { - return value and CATEGORIES != 0 - } + fun hasCategories(value: Int): Boolean = value and CATEGORIES != 0 - fun hasCustomCover(value: Int): Boolean { - return value and CUSTOM_COVER != 0 - } + fun hasCustomCover(value: Int): Boolean = value and CUSTOM_COVER != 0 - fun hasDeleteDownloaded(value: Int): Boolean { - return value and DELETE_DOWNLOADED != 0 - } + fun hasDeleteDownloaded(value: Int): Boolean = value and DELETE_DOWNLOADED != 0 /** Returns information about applicable flags with default selections. */ - fun getFlags(manga: Manga?, defaultSelectedBitMap: Int): List { + fun getFlags( + manga: Manga?, + defaultSelectedBitMap: Int, + ): List { val flags = mutableListOf() flags += MigrationFlag.create(CHAPTERS, defaultSelectedBitMap, MR.strings.chapters) flags += MigrationFlag.create(CATEGORIES, defaultSelectedBitMap, MR.strings.categories) @@ -71,11 +68,10 @@ object MigrationFlags { fun getSelectedFlagsBitMap( selectedFlags: List, flags: List, - ): Int { - return selectedFlags + ): Int = + selectedFlags .zip(flags) .filter { (isSelected, _) -> isSelected } .map { (_, flag) -> flag.flag } .reduceOrNull { acc, mask -> acc or mask } ?: 0 - } } 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 6f26c559b3..c37dd82fe6 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 @@ -20,7 +20,6 @@ import tachiyomi.presentation.core.screens.LoadingScreen data class MigrateMangaScreen( private val sourceId: Long, ) : Screen() { - @Composable override fun Content() { val context = LocalContext.current 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 8faf1c320e..b3c7b1d49e 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 @@ -28,7 +28,6 @@ class MigrateMangaScreenModel( private val sourceManager: SourceManager = Injekt.get(), private val getFavorites: GetFavorites = Injekt.get(), ) : StateScreenModel(State()) { - private val _events: Channel = Channel() val events: Flow = _events.receiveAsFlow() @@ -38,20 +37,19 @@ class MigrateMangaScreenModel( state.copy(source = sourceManager.getOrStub(sourceId)) } - getFavorites.subscribe(sourceId) + getFavorites + .subscribe(sourceId) .catch { logcat(LogPriority.ERROR, it) _events.send(MigrationMangaEvent.FailedFetchingFavorites) mutableState.update { state -> state.copy(titleList = persistentListOf()) } - } - .map { manga -> + }.map { manga -> manga .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title }) .toImmutableList() - } - .collectLatest { list -> + }.collectLatest { list -> mutableState.update { it.copy(titleList = list) } } } @@ -62,7 +60,6 @@ class MigrateMangaScreenModel( val source: Source? = null, private val titleList: ImmutableList? = null, ) { - val titles: ImmutableList get() = titleList ?: persistentListOf() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt index d3b35bcf42..ad0d1e7a13 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt @@ -72,8 +72,9 @@ internal fun MigrateDialog( if (state.isMigrating) { LoadingScreen( - modifier = Modifier - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)), + modifier = + Modifier + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)), ) } else { AlertDialog( @@ -160,7 +161,6 @@ internal class MigrateDialogScreenModel( private val coverCache: CoverCache = Injekt.get(), private val preferenceStore: PreferenceStore = Injekt.get(), ) : StateScreenModel(State()) { - val migrateFlags: Preference by lazy { preferenceStore.getInt("migrate_flags", Int.MAX_VALUE) } @@ -225,31 +225,35 @@ internal class MigrateDialogScreenModel( val prevMangaChapters = getChaptersByMangaId.await(oldManga.id) val mangaChapters = getChaptersByMangaId.await(newManga.id) - val maxChapterRead = prevMangaChapters - .filter { it.read } - .maxOfOrNull { it.chapterNumber } - - val updatedMangaChapters = mangaChapters.map { mangaChapter -> - var updatedChapter = mangaChapter - if (updatedChapter.isRecognizedNumber) { - val prevChapter = prevMangaChapters - .find { it.isRecognizedNumber && it.chapterNumber == updatedChapter.chapterNumber } + val maxChapterRead = + prevMangaChapters + .filter { it.read } + .maxOfOrNull { it.chapterNumber } + + val updatedMangaChapters = + mangaChapters.map { mangaChapter -> + var updatedChapter = mangaChapter + if (updatedChapter.isRecognizedNumber) { + val prevChapter = + prevMangaChapters + .find { it.isRecognizedNumber && it.chapterNumber == updatedChapter.chapterNumber } + + if (prevChapter != null) { + updatedChapter = + updatedChapter.copy( + dateFetch = prevChapter.dateFetch, + bookmark = prevChapter.bookmark, + ) + } - if (prevChapter != null) { - updatedChapter = updatedChapter.copy( - dateFetch = prevChapter.dateFetch, - bookmark = prevChapter.bookmark, - ) + if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) { + updatedChapter = updatedChapter.copy(read = true) + } } - if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) { - updatedChapter = updatedChapter.copy(read = true) - } + updatedChapter } - updatedChapter - } - val chapterUpdates = updatedMangaChapters.map { it.toChapterUpdate() } updateChapter.awaitAll(chapterUpdates) } @@ -261,19 +265,21 @@ internal class MigrateDialogScreenModel( } // Update track - getTracks.await(oldManga.id).mapNotNull { track -> - val updatedTrack = track.copy(mangaId = newManga.id) - - val service = enhancedServices - .firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) } - - if (service != null) { - service.migrateTrack(updatedTrack, newManga, newSource) - } else { - updatedTrack - } - } - .takeIf { it.isNotEmpty() } + getTracks + .await(oldManga.id) + .mapNotNull { track -> + val updatedTrack = track.copy(mangaId = newManga.id) + + val service = + enhancedServices + .firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) } + + if (service != null) { + service.migrateTrack(updatedTrack, newManga, newSource) + } else { + updatedTrack + } + }.takeIf { it.isNotEmpty() } ?.let { insertTrack.awaitAll(it) } // Delete downloaded diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index fd0d4a0e84..b2acccbd76 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -10,8 +10,9 @@ import eu.kanade.presentation.browse.MigrateSearchScreen import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.ui.manga.MangaScreen -class MigrateSearchScreen(private val mangaId: Long) : Screen() { - +class MigrateSearchScreen( + private val mangaId: Long, +) : Screen() { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenDialogScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenDialogScreenModel.kt index a053d45baa..91fdb88f2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenDialogScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenDialogScreenModel.kt @@ -14,7 +14,6 @@ class MigrateSearchScreenDialogScreenModel( val mangaId: Long, getManga: GetManga = Injekt.get(), ) : StateScreenModel(State()) { - init { screenModelScope.launch { val manga = getManga.await(mangaId)!! @@ -38,6 +37,8 @@ class MigrateSearchScreenDialogScreenModel( ) sealed interface Dialog { - data class Migrate(val manga: Manga) : Dialog + data class Migrate( + val manga: Manga, + ) : Dialog } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt index d22756c780..f977b3c3bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt @@ -14,7 +14,6 @@ class MigrateSearchScreenModel( val mangaId: Long, getManga: GetManga = Injekt.get(), ) : SearchScreenModel() { - init { screenModelScope.launch { val manga = getManga.await(mangaId)!! @@ -28,8 +27,9 @@ class MigrateSearchScreenModel( } } - override fun getEnabledSources(): List { - return super.getEnabledSources() + override fun getEnabledSources(): List = + super + .getEnabledSources() .filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } .sortedWith( compareBy( @@ -38,5 +38,4 @@ class MigrateSearchScreenModel( { "${it.name.lowercase()} (${it.lang})" }, ), ) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index 533dbd3af3..e910d058db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -43,7 +43,6 @@ data class SourceSearchScreen( private val sourceId: Long, private val query: String?, ) : Screen() { - @Composable override fun Content() { if (!ifSourcesLoaded()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceScreenModel.kt index 8968d62b82..e0e1dfd996 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceScreenModel.kt @@ -28,18 +28,17 @@ class MigrateSourceScreenModel( private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(), private val setMigrateSorting: SetMigrateSorting = Injekt.get(), ) : StateScreenModel(State()) { - private val _channel = Channel(Int.MAX_VALUE) val channel = _channel.receiveAsFlow() init { screenModelScope.launchIO { - getSourcesWithFavoriteCount.subscribe() + getSourcesWithFavoriteCount + .subscribe() .catch { logcat(LogPriority.ERROR, it) _channel.send(Event.FailedFetchingSourcesWithCount) - } - .collectLatest { sources -> + }.collectLatest { sources -> mutableState.update { it.copy( isLoading = false, @@ -49,21 +48,26 @@ class MigrateSourceScreenModel( } } - preferences.migrationSortingDirection().changes() + preferences + .migrationSortingDirection() + .changes() .onEach { mutableState.update { state -> state.copy(sortingDirection = it) } } .launchIn(screenModelScope) - preferences.migrationSortingMode().changes() + preferences + .migrationSortingMode() + .changes() .onEach { mutableState.update { state -> state.copy(sortingMode = it) } } .launchIn(screenModelScope) } fun toggleSortingMode() { with(state.value) { - val newMode = when (sortingMode) { - SetMigrateSorting.Mode.ALPHABETICAL -> SetMigrateSorting.Mode.TOTAL - SetMigrateSorting.Mode.TOTAL -> SetMigrateSorting.Mode.ALPHABETICAL - } + val newMode = + when (sortingMode) { + SetMigrateSorting.Mode.ALPHABETICAL -> SetMigrateSorting.Mode.TOTAL + SetMigrateSorting.Mode.TOTAL -> SetMigrateSorting.Mode.ALPHABETICAL + } setMigrateSorting.await(newMode, sortingDirection) } @@ -71,10 +75,11 @@ class MigrateSourceScreenModel( fun toggleSortingDirection() { with(state.value) { - val newDirection = when (sortingDirection) { - SetMigrateSorting.Direction.ASCENDING -> SetMigrateSorting.Direction.DESCENDING - SetMigrateSorting.Direction.DESCENDING -> SetMigrateSorting.Direction.ASCENDING - } + val newDirection = + when (sortingDirection) { + SetMigrateSorting.Direction.ASCENDING -> SetMigrateSorting.Direction.DESCENDING + SetMigrateSorting.Direction.DESCENDING -> SetMigrateSorting.Direction.ASCENDING + } setMigrateSorting.await(sortingMode, newDirection) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt index a3ebea28fb..6cf943dda2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt @@ -27,15 +27,16 @@ fun Screen.migrateSourceTab(): TabContent { return TabContent( titleRes = MR.strings.label_migration, - actions = persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.migration_help_guide), - icon = Icons.AutoMirrored.Outlined.HelpOutline, - onClick = { - uriHandler.openUri("https://mihon.app/docs/guides/source-migration") - }, + actions = + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.migration_help_guide), + icon = Icons.AutoMirrored.Outlined.HelpOutline, + onClick = { + uriHandler.openUri("https://mihon.app/docs/guides/source-migration") + }, + ), ), - ), content = { contentPadding, _ -> MigrateSourceScreen( state = state, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt index f225b397a4..c71764c301 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt @@ -15,7 +15,6 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.screens.LoadingScreen class SourcesFilterScreen : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreenModel.kt index 4f4ff301ce..758ec8b208 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreenModel.kt @@ -23,7 +23,6 @@ class SourcesFilterScreenModel( private val toggleSource: ToggleSource = Injekt.get(), private val toggleLanguage: ToggleLanguage = Injekt.get(), ) : StateScreenModel(State.Loading) { - init { screenModelScope.launch { combine( @@ -37,8 +36,7 @@ class SourcesFilterScreenModel( throwable = throwable, ) } - } - .collectLatest { (languagesWithSources, enabledLanguages, disabledSources) -> + }.collectLatest { (languagesWithSources, enabledLanguages, disabledSources) -> mutableState.update { State.Success( items = languagesWithSources, @@ -59,7 +57,6 @@ class SourcesFilterScreenModel( } sealed interface State { - @Immutable data object Loading : State @@ -74,7 +71,6 @@ class SourcesFilterScreenModel( val enabledLanguages: Set, val disabledSources: Set, ) : State { - val isEmpty: Boolean get() = items.isEmpty() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesScreenModel.kt index 0f777f7e58..4614672d0b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesScreenModel.kt @@ -29,55 +29,57 @@ class SourcesScreenModel( private val toggleSource: ToggleSource = Injekt.get(), private val toggleSourcePin: ToggleSourcePin = Injekt.get(), ) : StateScreenModel(State()) { - private val _events = Channel(Int.MAX_VALUE) val events = _events.receiveAsFlow() init { screenModelScope.launchIO { - getEnabledSources.subscribe() + getEnabledSources + .subscribe() .catch { logcat(LogPriority.ERROR, it) _events.send(Event.FailedFetchingSources) - } - .collectLatest(::collectLatestSources) + }.collectLatest(::collectLatestSources) } } private fun collectLatestSources(sources: List) { mutableState.update { state -> - val map = TreeMap> { d1, d2 -> - // Sources without a lang defined will be placed at the end - when { - d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1 - d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1 - d1 == PINNED_KEY && d2 != PINNED_KEY -> -1 - d2 == PINNED_KEY && d1 != PINNED_KEY -> 1 - d1 == "" && d2 != "" -> 1 - d2 == "" && d1 != "" -> -1 - else -> d1.compareTo(d2) + val map = + TreeMap> { d1, d2 -> + // Sources without a lang defined will be placed at the end + when { + d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1 + d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1 + d1 == PINNED_KEY && d2 != PINNED_KEY -> -1 + d2 == PINNED_KEY && d1 != PINNED_KEY -> 1 + d1 == "" && d2 != "" -> 1 + d2 == "" && d1 != "" -> -1 + else -> d1.compareTo(d2) + } } - } - val byLang = sources.groupByTo(map) { - when { - it.isUsedLast -> LAST_USED_KEY - Pin.Actual in it.pin -> PINNED_KEY - else -> it.lang + val byLang = + sources.groupByTo(map) { + when { + it.isUsedLast -> LAST_USED_KEY + Pin.Actual in it.pin -> PINNED_KEY + else -> it.lang + } } - } state.copy( isLoading = false, - items = byLang - .flatMap { - listOf( - SourceUiModel.Header(it.key), - *it.value.map { source -> - SourceUiModel.Item(source) - }.toTypedArray(), - ) - } - .toImmutableList(), + items = + byLang + .flatMap { + listOf( + SourceUiModel.Header(it.key), + *it.value + .map { source -> + SourceUiModel.Item(source) + }.toTypedArray(), + ) + }.toImmutableList(), ) } } @@ -102,7 +104,9 @@ class SourcesScreenModel( data object FailedFetchingSources : Event } - data class Dialog(val source: Source) + data class Dialog( + val source: Source, + ) @Immutable data class State( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt index f644f8c8da..47fc72cbcb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt @@ -31,18 +31,19 @@ fun Screen.sourcesTab(): TabContent { return TabContent( titleRes = MR.strings.label_sources, - actions = persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_global_search), - icon = Icons.Outlined.TravelExplore, - onClick = { navigator.push(GlobalSearchScreen()) }, + actions = + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_global_search), + icon = Icons.Outlined.TravelExplore, + onClick = { navigator.push(GlobalSearchScreen()) }, + ), + AppBar.Action( + title = stringResource(MR.strings.action_filter), + icon = Icons.Outlined.FilterList, + onClick = { navigator.push(SourcesFilterScreen()) }, + ), ), - AppBar.Action( - title = stringResource(MR.strings.action_filter), - icon = Icons.Outlined.FilterList, - onClick = { navigator.push(SourcesFilterScreen()) }, - ), - ), content = { contentPadding, snackbarHostState -> SourcesScreen( state = state, 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 152b2adfee..7c1b77e907 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 @@ -69,8 +69,8 @@ import tachiyomi.source.local.LocalSource data class BrowseSourceScreen( private val sourceId: Long, private val listingQuery: String?, -) : Screen(), AssistContentScreen { - +) : Screen(), + AssistContentScreen { private var assistUrl: String? = null override fun onProvideAssistUrl() = assistUrl @@ -139,9 +139,10 @@ data class BrowseSourceScreen( ) Row( - modifier = Modifier - .horizontalScroll(rememberScrollState()) - .padding(horizontal = MaterialTheme.padding.small), + modifier = + Modifier + .horizontalScroll(rememberScrollState()) + .padding(horizontal = MaterialTheme.padding.small), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { FilterChip( @@ -154,8 +155,9 @@ data class BrowseSourceScreen( Icon( imageVector = Icons.Outlined.Favorite, contentDescription = null, - modifier = Modifier - .size(FilterChipDefaults.IconSize), + modifier = + Modifier + .size(FilterChipDefaults.IconSize), ) }, label = { @@ -173,8 +175,9 @@ data class BrowseSourceScreen( Icon( imageVector = Icons.Outlined.NewReleases, contentDescription = null, - modifier = Modifier - .size(FilterChipDefaults.IconSize), + modifier = + Modifier + .size(FilterChipDefaults.IconSize), ) }, label = { @@ -190,8 +193,9 @@ data class BrowseSourceScreen( Icon( imageVector = Icons.Outlined.FilterList, contentDescription = null, - modifier = Modifier - .size(FilterChipDefaults.IconSize), + modifier = + Modifier + .size(FilterChipDefaults.IconSize), ) }, label = { @@ -224,12 +228,13 @@ data class BrowseSourceScreen( val duplicateManga = screenModel.getDuplicateLibraryManga(manga) when { manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) - duplicateManga != null -> screenModel.setDialog( - BrowseSourceScreenModel.Dialog.AddDuplicateManga( - manga, - duplicateManga, - ), - ) + duplicateManga != null -> + screenModel.setDialog( + BrowseSourceScreenModel.Dialog.AddDuplicateManga( + manga, + duplicateManga, + ), + ) else -> screenModel.addFavorite(manga) } haptic.performHapticFeedback(HapticFeedbackType.LongPress) @@ -296,7 +301,8 @@ data class BrowseSourceScreen( } LaunchedEffect(Unit) { - queryEvent.receiveAsFlow() + queryEvent + .receiveAsFlow() .collectLatest { when (it) { is SearchType.Genre -> screenModel.searchGenre(it.txt) @@ -307,14 +313,22 @@ data class BrowseSourceScreen( } suspend fun search(query: String) = queryEvent.send(SearchType.Text(query)) + suspend fun searchGenre(name: String) = queryEvent.send(SearchType.Genre(name)) companion object { private val queryEvent = Channel() } - sealed class SearchType(val txt: String) { - class Text(txt: String) : SearchType(txt) - class Genre(txt: String) : SearchType(txt) + sealed class SearchType( + val txt: String, + ) { + class Text( + txt: String, + ) : SearchType(txt) + + class Genre( + txt: String, + ) : SearchType(txt) } } 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 dff062503a..1492781c56 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 @@ -73,7 +73,6 @@ class BrowseSourceScreenModel( private val updateManga: UpdateManga = Injekt.get(), private val addTracks: AddTracks = Injekt.get(), ) : StateScreenModel(State(Listing.valueOf(listingQuery))) { - var displayMode by sourcePreferences.sourceDisplayMode().asState(screenModelScope) val source = sourceManager.getOrStub(sourceId) @@ -106,31 +105,34 @@ class BrowseSourceScreenModel( * Flow of Pager flow tied to [State.listing] */ private val hideInLibraryItems = sourcePreferences.hideInLibraryItems().get() - val mangaPagerFlowFlow = state.map { it.listing } - .distinctUntilChanged() - .map { listing -> - Pager(PagingConfig(pageSize = 25)) { - getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters) - }.flow.map { pagingData -> - pagingData.map { - networkToLocalManga.await(it.toDomainManga(sourceId)) - .let { localManga -> getManga.subscribe(localManga.url, localManga.source) } - .filterNotNull() - .stateIn(ioCoroutineScope) - } - .filter { !hideInLibraryItems || !it.value.favorite } - } - .cachedIn(ioCoroutineScope) - } - .stateIn(ioCoroutineScope, SharingStarted.Lazily, emptyFlow()) + val mangaPagerFlowFlow = + state + .map { it.listing } + .distinctUntilChanged() + .map { listing -> + Pager(PagingConfig(pageSize = 25)) { + getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters) + }.flow + .map { pagingData -> + pagingData + .map { + networkToLocalManga + .await(it.toDomainManga(sourceId)) + .let { localManga -> getManga.subscribe(localManga.url, localManga.source) } + .filterNotNull() + .stateIn(ioCoroutineScope) + }.filter { !hideInLibraryItems || !it.value.favorite } + }.cachedIn(ioCoroutineScope) + }.stateIn(ioCoroutineScope, SharingStarted.Lazily, emptyFlow()) fun getColumnsPreference(orientation: Int): GridCells { val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE - val columns = if (isLandscape) { - libraryPreferences.landscapeColumns() - } else { - libraryPreferences.portraitColumns() - }.get() + val columns = + if (isLandscape) { + libraryPreferences.landscapeColumns() + } else { + libraryPreferences.portraitColumns() + }.get() return if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns) } @@ -154,18 +156,23 @@ class BrowseSourceScreenModel( } } - fun search(query: String? = null, filters: FilterList? = null) { + fun search( + query: String? = null, + filters: FilterList? = null, + ) { if (source !is CatalogueSource) return - val input = state.value.listing as? Listing.Search - ?: Listing.Search(query = null, filters = source.getFilterList()) + val input = + state.value.listing as? Listing.Search + ?: Listing.Search(query = null, filters = source.getFilterList()) mutableState.update { it.copy( - listing = input.copy( - query = query ?: input.query, - filters = filters ?: input.filters, - ), + listing = + input.copy( + query = query ?: input.query, + filters = filters ?: input.filters, + ), toolbarQuery = query ?: input.query, ) } @@ -191,8 +198,10 @@ class BrowseSourceScreenModel( } } } else if (sourceFilter is SourceModelFilter.Select<*>) { - val index = sourceFilter.values.filterIsInstance() - .indexOfFirst { it.equals(genreName, true) } + val index = + sourceFilter.values + .filterIsInstance() + .indexOfFirst { it.equals(genreName, true) } if (index != -1) { sourceFilter.state = index @@ -203,11 +212,12 @@ class BrowseSourceScreenModel( } mutableState.update { - val listing = if (genreExists) { - Listing.Search(query = null, filters = defaultFilters) - } else { - Listing.Search(query = genreName, filters = defaultFilters) - } + val listing = + if (genreExists) { + Listing.Search(query = null, filters = defaultFilters) + } else { + Listing.Search(query = genreName, filters = defaultFilters) + } it.copy( filters = defaultFilters, listing = listing, @@ -223,13 +233,15 @@ class BrowseSourceScreenModel( */ fun changeMangaFavorite(manga: Manga) { screenModelScope.launch { - var new = manga.copy( - favorite = !manga.favorite, - dateAdded = when (manga.favorite) { - true -> 0 - false -> Instant.now().toEpochMilli() - }, - ) + var new = + manga.copy( + favorite = !manga.favorite, + dateAdded = + when (manga.favorite) { + true -> 0 + false -> Instant.now().toEpochMilli() + }, + ) if (!new.favorite) { new = new.removeCovers(coverCache) @@ -282,22 +294,26 @@ class BrowseSourceScreenModel( * * @return List of categories, not including the default category */ - suspend fun getCategories(): List { - return getCategories.subscribe() + suspend fun getCategories(): List = + getCategories + .subscribe() .firstOrNull() ?.filterNot { it.isSystemCategory } .orEmpty() - } - suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { - return getDuplicateLibraryManga.await(manga).getOrNull(0) - } + suspend fun getDuplicateLibraryManga(manga: Manga): Manga? = getDuplicateLibraryManga.await(manga).getOrNull(0) - private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { + private fun moveMangaToCategories( + manga: Manga, + vararg categories: Category, + ) { moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id }) } - fun moveMangaToCategories(manga: Manga, categoryIds: List) { + fun moveMangaToCategories( + manga: Manga, + categoryIds: List, + ) { screenModelScope.launchIO { setMangaCategories.await( mangaId = manga.id, @@ -318,34 +334,50 @@ class BrowseSourceScreenModel( mutableState.update { it.copy(toolbarQuery = query) } } - sealed class Listing(open val query: String?, open val filters: FilterList) { + sealed class Listing( + open val query: String?, + open val filters: FilterList, + ) { data object Popular : Listing(query = GetRemoteManga.QUERY_POPULAR, filters = FilterList()) + data object Latest : Listing(query = GetRemoteManga.QUERY_LATEST, filters = FilterList()) + data class Search( override val query: String?, override val filters: FilterList, ) : Listing(query = query, filters = filters) companion object { - fun valueOf(query: String?): Listing { - return when (query) { + fun valueOf(query: String?): Listing = + when (query) { GetRemoteManga.QUERY_POPULAR -> Popular GetRemoteManga.QUERY_LATEST -> Latest else -> Search(query = query, filters = FilterList()) // filters are filled in later } - } } } sealed interface Dialog { data object Filter : Dialog - data class RemoveManga(val manga: Manga) : Dialog - data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog + + data class RemoveManga( + val manga: Manga, + ) : Dialog + + data class AddDuplicateManga( + val manga: Manga, + val duplicate: Manga, + ) : Dialog + data class ChangeMangaCategory( val manga: Manga, val initialSelection: ImmutableList>, ) : Dialog - data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog + + data class Migrate( + val newManga: Manga, + val oldManga: Manga, + ) : Dialog } @Immutable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt index 42355c5042..fe93875995 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt @@ -44,16 +44,18 @@ fun SourceFilterDialog( LazyColumn { stickyHeader { Row( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .padding(8.dp), + modifier = + Modifier + .background(MaterialTheme.colorScheme.background) + .padding(8.dp), ) { TextButton(onClick = onReset) { Text( text = stringResource(MR.strings.action_reset), - style = LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.primary, - ), + style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.primary, + ), ) } @@ -77,7 +79,10 @@ fun SourceFilterDialog( } @Composable -private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) { +private fun FilterItem( + filter: Filter<*>, + onUpdate: () -> Unit, +) { when (filter) { is Filter.Header -> { HeadingItem(filter.name) @@ -99,7 +104,11 @@ private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) { label = filter.name, state = filter.state.toTriStateFilter(), ) { - filter.state = filter.state.toTriStateFilter().next().toTriStateInt() + filter.state = + filter.state + .toTriStateFilter() + .next() + .toTriStateInt() onUpdate() } } @@ -130,18 +139,23 @@ private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) { filter.values.mapIndexed { index, item -> SortItem( label = item, - sortDescending = filter.state?.ascending?.not() - ?.takeIf { index == filter.state?.index }, + sortDescending = + filter.state + ?.ascending + ?.not() + ?.takeIf { index == filter.state?.index }, ) { - val ascending = if (index == filter.state?.index) { - !filter.state!!.ascending - } else { - filter.state!!.ascending - } - filter.state = Filter.Sort.Selection( - index = index, - ascending = ascending, - ) + val ascending = + if (index == filter.state?.index) { + !filter.state!!.ascending + } else { + filter.state!!.ascending + } + filter.state = + Filter.Sort.Selection( + index = index, + ascending = ascending, + ) onUpdate() } } @@ -162,19 +176,17 @@ private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) { } } -private fun Int.toTriStateFilter(): TriState { - return when (this) { +private fun Int.toTriStateFilter(): TriState = + when (this) { Filter.TriState.STATE_IGNORE -> TriState.DISABLED Filter.TriState.STATE_INCLUDE -> TriState.ENABLED_IS Filter.TriState.STATE_EXCLUDE -> TriState.ENABLED_NOT else -> throw IllegalStateException("Unknown TriState state: $this") } -} -private fun TriState.toTriStateInt(): Int { - return when (this) { +private fun TriState.toTriStateInt(): Int = + when (this) { TriState.DISABLED -> Filter.TriState.STATE_IGNORE TriState.ENABLED_IS -> Filter.TriState.STATE_INCLUDE TriState.ENABLED_NOT -> Filter.TriState.STATE_EXCLUDE } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index 44164f49b7..c800d6dc77 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -21,7 +21,6 @@ class GlobalSearchScreen( val searchQuery: String = "", private val extensionFilter: String? = null, ) : Screen() { - @Composable override fun Content() { if (!ifSourcesLoaded()) { @@ -31,12 +30,13 @@ class GlobalSearchScreen( val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { - GlobalSearchScreenModel( - initialQuery = searchQuery, - initialExtensionFilter = extensionFilter, - ) - } + val screenModel = + rememberScreenModel { + GlobalSearchScreenModel( + initialQuery = searchQuery, + initialExtensionFilter = extensionFilter, + ) + } val state by screenModel.state.collectAsState() var showSingleLoadingScreen by remember { mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt index 33c7157681..21cae2a30e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt @@ -6,7 +6,6 @@ class GlobalSearchScreenModel( initialQuery: String = "", initialExtensionFilter: String? = null, ) : SearchScreenModel(State(searchQuery = initialQuery)) { - init { extensionFilter = initialExtensionFilter if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) { @@ -18,8 +17,8 @@ class GlobalSearchScreenModel( } } - override fun getEnabledSources(): List { - return super.getEnabledSources() + override fun getEnabledSources(): List = + super + .getEnabledSources() .filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 3f7438b7cb..228c158b56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -39,7 +39,6 @@ abstract class SearchScreenModel( private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val getManga: GetManga = Injekt.get(), ) : StateScreenModel(initialState) { - private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher() private var searchJob: Job? = null @@ -61,18 +60,19 @@ abstract class SearchScreenModel( } @Composable - fun getManga(initialManga: Manga): androidx.compose.runtime.State { - return produceState(initialValue = initialManga) { - getManga.subscribe(initialManga.url, initialManga.source) + fun getManga(initialManga: Manga): androidx.compose.runtime.State = + produceState(initialValue = initialManga) { + getManga + .subscribe(initialManga.url, initialManga.source) .filterNotNull() .collectLatest { manga -> value = manga } } - } - open fun getEnabledSources(): List { - return sourceManager.getCatalogueSources() + open fun getEnabledSources(): List = + sourceManager + .getCatalogueSources() .filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources } .sortedWith( compareBy( @@ -80,7 +80,6 @@ abstract class SearchScreenModel( { "${it.name.lowercase()} (${it.lang})" }, ), ) - } private fun getSelectedSources(): List { val enabledSources = getEnabledSources() @@ -142,50 +141,58 @@ abstract class SearchScreenModel( ) } - searchJob = ioCoroutineScope.launch { - sources.map { source -> - async { - if (state.value.items[source] !is SearchItemResult.Loading) { - return@async - } - - try { - val page = withContext(coroutineDispatcher) { - source.getSearchManga(1, query, source.getFilterList()) - } - - val titles = page.mangas.map { - networkToLocalManga.await(it.toDomainManga(source.id)) - } - - if (isActive) { - updateItem(source, SearchItemResult.Success(titles)) - } - } catch (e: Exception) { - if (isActive) { - updateItem(source, SearchItemResult.Error(e)) + searchJob = + ioCoroutineScope.launch { + sources + .map { source -> + async { + if (state.value.items[source] !is SearchItemResult.Loading) { + return@async + } + + try { + val page = + withContext(coroutineDispatcher) { + source.getSearchManga(1, query, source.getFilterList()) + } + + val titles = + page.mangas.map { + networkToLocalManga.await(it.toDomainManga(source.id)) + } + + if (isActive) { + updateItem(source, SearchItemResult.Success(titles)) + } + } catch (e: Exception) { + if (isActive) { + updateItem(source, SearchItemResult.Error(e)) + } + } } - } - } + }.awaitAll() } - .awaitAll() - } } private fun updateItems(items: PersistentMap) { mutableState.update { it.copy( - items = items - .toSortedMap(sortComparator(items)) - .toPersistentMap(), + items = + items + .toSortedMap(sortComparator(items)) + .toPersistentMap(), ) } } - private fun updateItem(source: CatalogueSource, result: SearchItemResult) { - val newItems = state.value.items.mutate { - it[source] = result - } + private fun updateItem( + source: CatalogueSource, + result: SearchItemResult, + ) { + val newItems = + state.value.items.mutate { + it[source] = result + } updateItems(newItems) } @@ -222,7 +229,5 @@ sealed interface SearchItemResult { get() = result.isEmpty() } - fun isVisible(onlyShowHasResults: Boolean): Boolean { - return !onlyShowHasResults || (this is Success && !this.isEmpty) - } + fun isVisible(onlyShowHasResults: Boolean): Boolean = !onlyShowHasResults || (this is Success && !this.isEmpty) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt index 20ebdba0ff..6ce1b739ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.collectLatest import tachiyomi.presentation.core.screens.LoadingScreen class CategoryScreen : Screen() { - @Composable override fun Content() { val context = LocalContext.current diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt index 2fb78342fd..fa60cbea24 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt @@ -28,19 +28,20 @@ class CategoryScreenModel( private val reorderCategory: ReorderCategory = Injekt.get(), private val renameCategory: RenameCategory = Injekt.get(), ) : StateScreenModel(CategoryScreenState.Loading) { - private val _events: Channel = Channel() val events = _events.receiveAsFlow() init { screenModelScope.launch { - getCategories.subscribe() + getCategories + .subscribe() .collectLatest { categories -> mutableState.update { CategoryScreenState.Success( - categories = categories - .filterNot(Category::isSystemCategory) - .toImmutableList(), + categories = + categories + .filterNot(Category::isSystemCategory) + .toImmutableList(), ) } } @@ -92,7 +93,10 @@ class CategoryScreenModel( } } - fun renameCategory(category: Category, name: String) { + fun renameCategory( + category: Category, + name: String, + ) { screenModelScope.launch { when (renameCategory.await(category, name)) { is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) @@ -122,18 +126,27 @@ class CategoryScreenModel( sealed interface CategoryDialog { data object Create : CategoryDialog + data object SortAlphabetically : CategoryDialog - data class Rename(val category: Category) : CategoryDialog - data class Delete(val category: Category) : CategoryDialog + + data class Rename( + val category: Category, + ) : CategoryDialog + + data class Delete( + val category: Category, + ) : CategoryDialog } sealed interface CategoryEvent { - sealed class LocalizedMessage(val stringRes: StringResource) : CategoryEvent + sealed class LocalizedMessage( + val stringRes: StringResource, + ) : CategoryEvent + data object InternalError : LocalizedMessage(MR.strings.internal_error) } sealed interface CategoryScreenState { - @Immutable data object Loading : CategoryScreenState @@ -142,7 +155,6 @@ sealed interface CategoryScreenState { val categories: ImmutableList, val dialog: CategoryDialog? = null, ) : CategoryScreenState { - val isEmpty: Boolean get() = categories.isEmpty() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkActivity.kt index 0635b03bb9..70ad01f08a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkActivity.kt @@ -6,7 +6,6 @@ import android.os.Bundle import eu.kanade.tachiyomi.ui.main.MainActivity class DeepLinkActivity : Activity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt index bd67cae6af..6a486528f5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt @@ -22,15 +22,15 @@ import tachiyomi.presentation.core.screens.LoadingScreen class DeepLinkScreen( val query: String = "", ) : Screen() { - @Composable override fun Content() { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { - DeepLinkScreenModel(query = query) - } + val screenModel = + rememberScreenModel { + DeepLinkScreenModel(query = query) + } val state by screenModel.state.collectAsState() Scaffold( topBar = { scrollBehavior -> @@ -59,11 +59,12 @@ class DeepLinkScreen( ) } else { navigator.pop() - ReaderActivity.newIntent( - context, - resultState.manga.id, - resultState.chapterId, - ).also(context::startActivity) + ReaderActivity + .newIntent( + context, + resultState.manga.id, + resultState.chapterId, + ).also(context::startActivity) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt index 5bef14675b..3133265bab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt @@ -30,22 +30,25 @@ class DeepLinkScreenModel( private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), ) : StateScreenModel(State.Loading) { - init { screenModelScope.launchIO { - val source = sourceManager.getCatalogueSources() - .filterIsInstance() - .firstOrNull { it.getUriType(query) != UriType.Unknown } + val source = + sourceManager + .getCatalogueSources() + .filterIsInstance() + .firstOrNull { it.getUriType(query) != UriType.Unknown } - val manga = source?.getManga(query)?.let { - getMangaFromSManga(it, source.id) - } + val manga = + source?.getManga(query)?.let { + getMangaFromSManga(it, source.id) + } - val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) { - source.getChapter(query)?.let { getChapterFromSChapter(it, manga, source) } - } else { - null - } + val chapter = + if (source?.getUriType(query) == UriType.Chapter && manga != null) { + source.getChapter(query)?.let { getChapterFromSChapter(it, manga, source) } + } else { + null + } mutableState.update { if (manga == null) { @@ -61,7 +64,11 @@ class DeepLinkScreenModel( } } - private suspend fun getChapterFromSChapter(sChapter: SChapter, manga: Manga, source: Source): Chapter? { + private suspend fun getChapterFromSChapter( + sChapter: SChapter, + manga: Manga, + source: Source, + ): Chapter? { val localChapter = getChapterByUrlAndMangaId.await(sChapter.url, manga.id) return if (localChapter == null) { @@ -73,10 +80,12 @@ class DeepLinkScreenModel( } } - private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga { - return getMangaByUrlAndSourceId.await(sManga.url, sourceId) + private suspend fun getMangaFromSManga( + sManga: SManga, + sourceId: Long, + ): Manga = + getMangaByUrlAndSourceId.await(sManga.url, sourceId) ?: networkToLocalManga.await(sManga.toDomainManga(sourceId)) - } sealed interface State { @Immutable @@ -86,6 +95,9 @@ class DeepLinkScreenModel( data object NoResults : State @Immutable - data class Result(val manga: Manga, val chapterId: Long? = null) : State + data class Result( + val manga: Manga, + val chapterId: Long? = null, + ) : State } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt index 44ff8b1103..5b0804badd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt @@ -9,19 +9,27 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem * * @param downloadItemListener Listener called when an item of the list is released. */ -class DownloadAdapter(val downloadItemListener: DownloadItemListener) : FlexibleAdapter>( - null, - downloadItemListener, - true, -) { - - override fun shouldMove(fromPosition: Int, toPosition: Int): Boolean { +class DownloadAdapter( + val downloadItemListener: DownloadItemListener, +) : FlexibleAdapter>( + null, + downloadItemListener, + true, + ) { + override fun shouldMove( + fromPosition: Int, + toPosition: Int, + ): Boolean { // Don't let sub-items changing group return getHeaderOf(getItem(fromPosition)) == getHeaderOf(getItem(toPosition)) } interface DownloadItemListener { fun onItemReleased(position: Int) - fun onMenuItemClick(position: Int, menuItem: MenuItem) + + fun onMenuItemClick( + position: Int, + menuItem: MenuItem, + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt index 3362c99ed3..0566e83ac0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt @@ -7,8 +7,10 @@ import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.viewholders.ExpandableViewHolder import eu.kanade.tachiyomi.databinding.DownloadHeaderBinding -class DownloadHeaderHolder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter) { - +class DownloadHeaderHolder( + view: View, + adapter: FlexibleAdapter<*>, +) : ExpandableViewHolder(view, adapter) { private val binding = DownloadHeaderBinding.bind(view) @SuppressLint("SetTextI18n") @@ -17,7 +19,10 @@ class DownloadHeaderHolder(view: View, adapter: FlexibleAdapter<*>) : Expandable binding.title.text = "${item.name} (${item.size})" } - override fun onActionStateChanged(position: Int, actionState: Int) { + override fun onActionStateChanged( + position: Int, + actionState: Int, + ) { super.onActionStateChanged(position, actionState) if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { binding.container.isDragged = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt index e8da53cb66..ee5da62ce9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt @@ -12,17 +12,12 @@ data class DownloadHeaderItem( val name: String, val size: Int, ) : AbstractExpandableHeaderItem() { - - override fun getLayoutRes(): Int { - return R.layout.download_header - } + override fun getLayoutRes(): Int = R.layout.download_header override fun createViewHolder( view: View, adapter: FlexibleAdapter>, - ): DownloadHeaderHolder { - return DownloadHeaderHolder(view, adapter) - } + ): DownloadHeaderHolder = DownloadHeaderHolder(view, adapter) override fun bindViewHolder( adapter: FlexibleAdapter>, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt index 514357d658..27d230fe10 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt @@ -15,9 +15,10 @@ import eu.kanade.tachiyomi.util.view.popupMenu * @param view the inflated view for this holder. * @constructor creates a new download holder. */ -class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : - FlexibleViewHolder(view, adapter) { - +class DownloadHolder( + private val view: View, + val adapter: DownloadAdapter, +) : FlexibleViewHolder(view, adapter) { private val binding = DownloadItemBinding.bind(view) init { @@ -78,7 +79,10 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : binding.container.isDragged = false } - override fun onActionStateChanged(position: Int, actionState: Int) { + override fun onActionStateChanged( + position: Int, + actionState: Int, + ) { super.onActionStateChanged(position, actionState) if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { binding.container.isDragged = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt index 6dd4f9b251..972155f90c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt @@ -12,10 +12,7 @@ class DownloadItem( val download: Download, header: DownloadHeaderItem, ) : AbstractSectionableItem(header) { - - override fun getLayoutRes(): Int { - return R.layout.download_item - } + override fun getLayoutRes(): Int = R.layout.download_item /** * Returns a new view holder for this item. @@ -26,9 +23,7 @@ class DownloadItem( override fun createViewHolder( view: View, adapter: FlexibleAdapter>, - ): DownloadHolder { - return DownloadHolder(view, adapter as DownloadAdapter) - } + ): DownloadHolder = DownloadHolder(view, adapter as DownloadAdapter) /** * Binds the given view holder with this item. @@ -50,9 +45,7 @@ class DownloadItem( /** * Returns true if this item is draggable. */ - override fun isDraggable(): Boolean { - return true - } + override fun isDraggable(): Boolean = true override fun equals(other: Any?): Boolean { if (this === other) return true @@ -62,7 +55,5 @@ class DownloadItem( return false } - override fun hashCode(): Int { - return download.chapter.id.toInt() - } + override fun hashCode(): Int = download.chapter.id.toInt() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt index 8332c0c97c..d87f1a059e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt @@ -63,7 +63,6 @@ import tachiyomi.presentation.core.screens.EmptyScreen import kotlin.math.roundToInt object DownloadQueueScreen : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -76,27 +75,33 @@ object DownloadQueueScreen : Screen() { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) var fabExpanded by remember { mutableStateOf(true) } - val nestedScrollConnection = remember { - // All this lines just for fab state :/ - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - fabExpanded = available.y >= 0 - return scrollBehavior.nestedScrollConnection.onPreScroll(available, source) - } + val nestedScrollConnection = + remember { + // All this lines just for fab state :/ + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset { + fabExpanded = available.y >= 0 + return scrollBehavior.nestedScrollConnection.onPreScroll(available, source) + } - override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { - return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source) - } + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset = scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source) - override suspend fun onPreFling(available: Velocity): Velocity { - return scrollBehavior.nestedScrollConnection.onPreFling(available) - } + override suspend fun onPreFling(available: Velocity): Velocity = + scrollBehavior.nestedScrollConnection.onPreFling(available) - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available) + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity, + ): Velocity = scrollBehavior.nestedScrollConnection.onPostFling(consumed, available) } } - } Scaffold( topBar = { @@ -114,8 +119,9 @@ object DownloadQueueScreen : Screen() { Pill( text = "$downloadCount", modifier = Modifier.padding(start = 4.dp), - color = MaterialTheme.colorScheme.onBackground - .copy(alpha = pillAlpha), + color = + MaterialTheme.colorScheme.onBackground + .copy(alpha = pillAlpha), fontSize = 14.sp, ) } @@ -209,19 +215,21 @@ object DownloadQueueScreen : Screen() { val isRunning by screenModel.isDownloaderRunning.collectAsState() ExtendedFloatingActionButton( text = { - val id = if (isRunning) { - MR.strings.action_pause - } else { - MR.strings.action_resume - } + val id = + if (isRunning) { + MR.strings.action_pause + } else { + MR.strings.action_resume + } Text(text = stringResource(id)) }, icon = { - val icon = if (isRunning) { - Icons.Outlined.Pause - } else { - Icons.Filled.PlayArrow - } + val icon = + if (isRunning) { + Icons.Outlined.Pause + } else { + Icons.Filled.PlayArrow + } Icon(imageVector = icon, contentDescription = null) }, onClick = { @@ -264,11 +272,13 @@ object DownloadQueueScreen : Screen() { ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true) scope.launchUI { - screenModel.getDownloadStatusFlow() + screenModel + .getDownloadStatusFlow() .collect(screenModel::onStatusChange) } scope.launchUI { - screenModel.getDownloadProgressFlow() + screenModel + .getDownloadProgressFlow() .collect(screenModel::onUpdateDownloadedPages) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreenModel.kt index 3f88966324..b9817606c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreenModel.kt @@ -27,7 +27,6 @@ import uy.kohesive.injekt.api.get class DownloadQueueScreenModel( private val downloadManager: DownloadManager = Injekt.get(), ) : ScreenModel { - private val _state = MutableStateFlow(emptyList()) val state = _state.asStateFlow() @@ -43,77 +42,86 @@ class DownloadQueueScreenModel( */ private val progressJobs = mutableMapOf() - val listener = object : DownloadAdapter.DownloadItemListener { - /** - * Called when an item is released from a drag. - * - * @param position The position of the released item. - */ - override fun onItemReleased(position: Int) { - val adapter = adapter ?: return - val downloads = adapter.headerItems.flatMap { header -> - adapter.getSectionItems(header).map { item -> - (item as DownloadItem).download - } + val listener = + object : DownloadAdapter.DownloadItemListener { + /** + * Called when an item is released from a drag. + * + * @param position The position of the released item. + */ + override fun onItemReleased(position: Int) { + val adapter = adapter ?: return + val downloads = + adapter.headerItems.flatMap { header -> + adapter.getSectionItems(header).map { item -> + (item as DownloadItem).download + } + } + reorder(downloads) } - reorder(downloads) - } - /** - * Called when the menu item of a download is pressed - * - * @param position The position of the item - * @param menuItem The menu Item pressed - */ - override fun onMenuItemClick(position: Int, menuItem: MenuItem) { - val item = adapter?.getItem(position) ?: return - if (item is DownloadItem) { - when (menuItem.itemId) { - R.id.move_to_top, R.id.move_to_bottom -> { - val headerItems = adapter?.headerItems ?: return - val newDownloads = mutableListOf() - headerItems.forEach { headerItem -> - headerItem as DownloadHeaderItem - if (headerItem == item.header) { - headerItem.removeSubItem(item) - if (menuItem.itemId == R.id.move_to_top) { - headerItem.addSubItem(0, item) - } else { - headerItem.addSubItem(item) + /** + * Called when the menu item of a download is pressed + * + * @param position The position of the item + * @param menuItem The menu Item pressed + */ + override fun onMenuItemClick( + position: Int, + menuItem: MenuItem, + ) { + val item = adapter?.getItem(position) ?: return + if (item is DownloadItem) { + when (menuItem.itemId) { + R.id.move_to_top, R.id.move_to_bottom -> { + val headerItems = adapter?.headerItems ?: return + val newDownloads = mutableListOf() + headerItems.forEach { headerItem -> + headerItem as DownloadHeaderItem + if (headerItem == item.header) { + headerItem.removeSubItem(item) + if (menuItem.itemId == R.id.move_to_top) { + headerItem.addSubItem(0, item) + } else { + headerItem.addSubItem(item) + } } + newDownloads.addAll(headerItem.subItems.map { it.download }) } - newDownloads.addAll(headerItem.subItems.map { it.download }) + reorder(newDownloads) } - reorder(newDownloads) - } - R.id.move_to_top_series, R.id.move_to_bottom_series -> { - val (selectedSeries, otherSeries) = adapter?.currentItems - ?.filterIsInstance() - ?.map(DownloadItem::download) - ?.partition { item.download.manga.id == it.manga.id } - ?: Pair(emptyList(), emptyList()) - if (menuItem.itemId == R.id.move_to_top_series) { - reorder(selectedSeries + otherSeries) - } else { - reorder(otherSeries + selectedSeries) + R.id.move_to_top_series, R.id.move_to_bottom_series -> { + val (selectedSeries, otherSeries) = + adapter + ?.currentItems + ?.filterIsInstance() + ?.map(DownloadItem::download) + ?.partition { item.download.manga.id == it.manga.id } + ?: Pair(emptyList(), emptyList()) + if (menuItem.itemId == R.id.move_to_top_series) { + reorder(selectedSeries + otherSeries) + } else { + reorder(otherSeries + selectedSeries) + } } - } - R.id.cancel_download -> { - cancel(listOf(item.download)) - } - R.id.cancel_series -> { - val allDownloadsForSeries = adapter?.currentItems - ?.filterIsInstance() - ?.filter { item.download.manga.id == it.download.manga.id } - ?.map(DownloadItem::download) - if (!allDownloadsForSeries.isNullOrEmpty()) { - cancel(allDownloadsForSeries) + R.id.cancel_download -> { + cancel(listOf(item.download)) + } + R.id.cancel_series -> { + val allDownloadsForSeries = + adapter + ?.currentItems + ?.filterIsInstance() + ?.filter { item.download.manga.id == it.download.manga.id } + ?.map(DownloadItem::download) + if (!allDownloadsForSeries.isNullOrEmpty()) { + cancel(allDownloadsForSeries) + } } } } } } - } init { screenModelScope.launch { @@ -126,8 +134,7 @@ class DownloadQueueScreenModel( addSubItems(0, entry.value.map { DownloadItem(it, this) }) } } - } - .collect { newList -> _state.update { newList } } + }.collect { newList -> _state.update { newList } } } } @@ -139,10 +146,12 @@ class DownloadQueueScreenModel( adapter = null } - val isDownloaderRunning = downloadManager.isDownloaderRunning - .stateIn(screenModelScope, SharingStarted.WhileSubscribed(5000), false) + val isDownloaderRunning = + downloadManager.isDownloaderRunning + .stateIn(screenModelScope, SharingStarted.WhileSubscribed(5000), false) fun getDownloadStatusFlow() = downloadManager.statusFlow() + fun getDownloadProgressFlow() = downloadManager.progressFlow() fun startDownloads() { @@ -165,16 +174,20 @@ class DownloadQueueScreenModel( downloadManager.cancelQueuedDownloads(downloads) } - fun > reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) { + fun > reorderQueue( + selector: (DownloadItem) -> R, + reverse: Boolean = false, + ) { val adapter = adapter ?: return val newDownloads = mutableListOf() adapter.headerItems.forEach { headerItem -> headerItem as DownloadHeaderItem - headerItem.subItems = headerItem.subItems.sortedBy(selector).toMutableList().apply { - if (reverse) { - reverse() + headerItem.subItems = + headerItem.subItems.sortedBy(selector).toMutableList().apply { + if (reverse) { + reverse() + } } - } newDownloads.addAll(headerItem.subItems.map { it.download }) } reorder(newDownloads) @@ -199,7 +212,7 @@ class DownloadQueueScreenModel( } Download.State.ERROR -> cancelProgressJob(download) else -> { - /* unused */ + // unused } } } @@ -210,19 +223,20 @@ class DownloadQueueScreenModel( * @param download the download to observe its progress. */ private fun launchProgressJob(download: Download) { - val job = screenModelScope.launch { - while (download.pages == null) { - delay(50) - } - - val progressFlows = download.pages!!.map(Page::progressFlow) - combine(progressFlows, Array::sum) - .distinctUntilChanged() - .debounce(50) - .collectLatest { - onUpdateProgress(download) + val job = + screenModelScope.launch { + while (download.pages == null) { + delay(50) } - } + + val progressFlows = download.pages!!.map(Page::progressFlow) + combine(progressFlows, Array::sum) + .distinctUntilChanged() + .debounce(50) + .collectLatest { + onUpdateProgress(download) + } + } // Avoid leaking jobs progressJobs.remove(download)?.cancel() @@ -263,7 +277,6 @@ class DownloadQueueScreenModel( * @param download the download to find. * @return the holder of the download or null if it's not bound. */ - private fun getHolder(download: Download): DownloadHolder? { - return controllerBinding.root.findViewHolderForItemId(download.chapter.id) as? DownloadHolder - } + private fun getHolder(download: Download): DownloadHolder? = + controllerBinding.root.findViewHolderForItemId(download.chapter.id) as? DownloadHolder } 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 c0c2b555f3..89c171e859 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 @@ -34,46 +34,55 @@ class HistoryScreenModel( private val getNextChapters: GetNextChapters = Injekt.get(), private val removeHistory: RemoveHistory = Injekt.get(), ) : StateScreenModel(State()) { - private val _events: Channel = Channel(Channel.UNLIMITED) val events: Flow = _events.receiveAsFlow() init { screenModelScope.launch { - state.map { it.searchQuery } + state + .map { it.searchQuery } .distinctUntilChanged() .flatMapLatest { query -> - getHistory.subscribe(query ?: "") + getHistory + .subscribe(query ?: "") .distinctUntilChanged() .catch { error -> logcat(LogPriority.ERROR, error) _events.send(Event.InternalError) - } - .map { it.toHistoryUiModels() } + }.map { it.toHistoryUiModels() } .flowOn(Dispatchers.IO) - } - .collect { newList -> mutableState.update { it.copy(list = newList) } } + }.collect { newList -> mutableState.update { it.copy(list = newList) } } } } - private fun List.toHistoryUiModels(): List { - return map { HistoryUiModel.Item(it) } + private fun List.toHistoryUiModels(): List = + map { HistoryUiModel.Item(it) } .insertSeparators { before, after -> - val beforeDate = before?.item?.readAt?.time?.toLocalDate() - val afterDate = after?.item?.readAt?.time?.toLocalDate() + val beforeDate = + before + ?.item + ?.readAt + ?.time + ?.toLocalDate() + val afterDate = + after + ?.item + ?.readAt + ?.time + ?.toLocalDate() when { beforeDate != afterDate && afterDate != null -> HistoryUiModel.Header(afterDate) // Return null to avoid adding a separator between two items. else -> null } } - } - suspend fun getNextChapter(): Chapter? { - return withIOContext { getNextChapters.await(onlyUnread = false).firstOrNull() } - } + suspend fun getNextChapter(): Chapter? = withIOContext { getNextChapters.await(onlyUnread = false).firstOrNull() } - fun getNextChapterForManga(mangaId: Long, chapterId: Long) { + fun getNextChapterForManga( + mangaId: Long, + chapterId: Long, + ) { screenModelScope.launchIO { sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false)) } @@ -121,12 +130,19 @@ class HistoryScreenModel( sealed interface Dialog { data object DeleteAll : Dialog - data class Delete(val history: HistoryWithRelations) : Dialog + + data class Delete( + val history: HistoryWithRelations, + ) : Dialog } sealed interface Event { - data class OpenChapter(val chapter: Chapter?) : Event + data class OpenChapter( + val chapter: Chapter?, + ) : Event + data object InternalError : Event + data object HistoryCleared : Event } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt index 9f91706f16..f7f4d405c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt @@ -33,7 +33,6 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource object HistoryTab : Tab { - private val snackbarHostState = SnackbarHostState() private val resumeLastChapterReadEvent = Channel() @@ -118,7 +117,10 @@ object HistoryTab : Tab { } } - private suspend fun openChapter(context: Context, chapter: Chapter?) { + private suspend fun openChapter( + context: Context, + chapter: Chapter?, + ) { if (chapter != null) { val intent = ReaderActivity.newIntent(context, chapter.mangaId, chapter.id) context.startActivity(intent) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt index ff2cb7075e..1fe12628a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt @@ -61,7 +61,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object HomeScreen : Screen() { - private val librarySearchEvent = Channel() private val openTabEvent = Channel() private val showBottomNavEvent = Channel() @@ -69,13 +68,14 @@ object HomeScreen : Screen() { private const val TabFadeDuration = 200 private const val TabNavigatorKey = "HomeTabs" - private val tabs = listOf( - LibraryTab, - UpdatesTab, - HistoryTab, - BrowseTab(), - MoreTab, - ) + private val tabs = + listOf( + LibraryTab, + UpdatesTab, + HistoryTab, + BrowseTab(), + MoreTab, + ) @Composable override fun Content() { @@ -117,9 +117,10 @@ object HomeScreen : Screen() { contentWindowInsets = WindowInsets(0), ) { contentPadding -> Box( - modifier = Modifier - .padding(contentPadding) - .consumeWindowInsets(contentPadding), + modifier = + Modifier + .padding(contentPadding) + .consumeWindowInsets(contentPadding), ) { AnimatedContent( targetState = tabNavigator.current, @@ -152,13 +153,14 @@ object HomeScreen : Screen() { } launch { openTabEvent.receiveAsFlow().collectLatest { - tabNavigator.current = when (it) { - is Tab.Library -> LibraryTab - Tab.Updates -> UpdatesTab - Tab.History -> HistoryTab - is Tab.Browse -> BrowseTab(it.toExtensions) - is Tab.More -> MoreTab - } + tabNavigator.current = + when (it) { + is Tab.Library -> LibraryTab + Tab.Updates -> UpdatesTab + Tab.History -> HistoryTab + is Tab.Browse -> BrowseTab(it.toExtensions) + is Tab.More -> MoreTab + } if (it is Tab.Library && it.mangaIdToOpen != null) { navigator.push(MangaScreen(it.mangaIdToOpen)) @@ -244,11 +246,12 @@ object HomeScreen : Screen() { } if (count > 0) { Badge { - val desc = pluralStringResource( - MR.plurals.notification_chapters_generic, - count = count, - count, - ) + val desc = + pluralStringResource( + MR.plurals.notification_chapters_generic, + count = count, + count, + ) Text( text = count.toString(), modifier = Modifier.semantics { contentDescription = desc }, @@ -258,16 +261,20 @@ object HomeScreen : Screen() { } BrowseTab::class.isInstance(tab) -> { val count by produceState(initialValue = 0) { - Injekt.get().extensionUpdatesCount().changes() + Injekt + .get() + .extensionUpdatesCount() + .changes() .collectLatest { value = it } } if (count > 0) { Badge { - val desc = pluralStringResource( - MR.plurals.update_check_notification_ext_updates, - count = count, - count, - ) + val desc = + pluralStringResource( + MR.plurals.update_check_notification_ext_updates, + count = count, + count, + ) Text( text = count.toString(), modifier = Modifier.semantics { contentDescription = desc }, @@ -300,10 +307,20 @@ object HomeScreen : Screen() { } sealed interface Tab { - data class Library(val mangaIdToOpen: Long? = null) : Tab + data class Library( + val mangaIdToOpen: Long? = null, + ) : Tab + data object Updates : Tab + data object History : Tab - data class Browse(val toExtensions: Boolean = false) : Tab - data class More(val toDownloads: Boolean) : Tab + + data class Browse( + val toExtensions: Boolean = false, + ) : Tab + + data class More( + val toDownloads: Boolean, + ) : Tab } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index 325e054c1e..331d950a54 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -45,11 +45,10 @@ data class LibraryItem( private fun checkNegatableConstraint( constraint: String, predicate: (String) -> Boolean, - ): Boolean { - return if (constraint.startsWith("-")) { + ): Boolean = + if (constraint.startsWith("-")) { !predicate(constraint.substringAfter("-").trimStart()) } else { predicate(constraint) } - } } 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 b2771d4e20..a5e532a509 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 @@ -95,7 +95,6 @@ class LibraryScreenModel( private val downloadCache: DownloadCache = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), ) : StateScreenModel(State()) { - var activeCategoryIndex: Int by libraryPreferences.lastUsedCategory().asState(screenModelScope) init { @@ -119,15 +118,14 @@ class LibraryScreenModel( value } } - } - .collectLatest { - mutableState.update { state -> - state.copy( - isLoading = false, - library = it, - ) - } + }.collectLatest { + mutableState.update { state -> + state.copy( + isLoading = false, + library = it, + ) } + } } combine( @@ -143,8 +141,7 @@ class LibraryScreenModel( showMangaContinueButton = showMangaContinueButton, ) } - } - .launchIn(screenModelScope) + }.launchIn(screenModelScope) combine( getLibraryItemPreferencesFlow(), @@ -159,15 +156,13 @@ class LibraryScreenModel( prefs.filterCompleted, prefs.filterIntervalCustom, ) + trackFilter.values - ).any { it != TriState.DISABLED } - } - .distinctUntilChanged() + ).any { it != TriState.DISABLED } + }.distinctUntilChanged() .onEach { mutableState.update { state -> state.copy(hasActiveFilters = it) } - } - .launchIn(screenModelScope) + }.launchIn(screenModelScope) } /** @@ -214,7 +209,10 @@ class LibraryScreenModel( } val filterFnCompleted: (LibraryItem) -> Boolean = { - applyFilter(filterCompleted) { it.libraryManga.manga.status.toInt() == SManga.COMPLETED } + applyFilter(filterCompleted) { + it.libraryManga.manga.status + .toInt() == SManga.COMPLETED + } } val filterFnIntervalCustom: (LibraryItem) -> Boolean = { @@ -228,9 +226,10 @@ class LibraryScreenModel( val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item -> if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true - val mangaTracks = trackMap - .mapValues { entry -> entry.value.map { it.trackerId } }[item.libraryManga.id] - .orEmpty() + val mangaTracks = + trackMap + .mapValues { entry -> entry.value.map { it.trackerId } }[item.libraryManga.id] + .orEmpty() val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks } val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks } @@ -259,7 +258,12 @@ class LibraryScreenModel( trackMap: Map>, ): LibraryMap { val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> - i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase()) + i1.libraryManga.manga.title + .lowercase() + .compareToWithCollator( + i2.libraryManga.manga.title + .lowercase(), + ) } val defaultTrackerScoreSortValue = -1.0 @@ -286,15 +290,17 @@ class LibraryScreenModel( i1.libraryManga.lastRead.compareTo(i2.libraryManga.lastRead) } LibrarySort.Type.LastUpdate -> { - i1.libraryManga.manga.lastUpdate.compareTo(i2.libraryManga.manga.lastUpdate) - } - LibrarySort.Type.UnreadCount -> when { - // Ensure unread content comes first - i1.libraryManga.unreadCount == i2.libraryManga.unreadCount -> 0 - i1.libraryManga.unreadCount == 0L -> if (sort.isAscending) 1 else -1 - i2.libraryManga.unreadCount == 0L -> if (sort.isAscending) -1 else 1 - else -> i1.libraryManga.unreadCount.compareTo(i2.libraryManga.unreadCount) + i1.libraryManga.manga.lastUpdate + .compareTo(i2.libraryManga.manga.lastUpdate) } + LibrarySort.Type.UnreadCount -> + when { + // Ensure unread content comes first + i1.libraryManga.unreadCount == i2.libraryManga.unreadCount -> 0 + i1.libraryManga.unreadCount == 0L -> if (sort.isAscending) 1 else -1 + i2.libraryManga.unreadCount == 0L -> if (sort.isAscending) -1 else 1 + else -> i1.libraryManga.unreadCount.compareTo(i2.libraryManga.unreadCount) + } LibrarySort.Type.TotalChapters -> { i1.libraryManga.totalChapters.compareTo(i2.libraryManga.totalChapters) } @@ -305,7 +311,8 @@ class LibraryScreenModel( i1.libraryManga.chapterFetchedAt.compareTo(i2.libraryManga.chapterFetchedAt) } LibrarySort.Type.DateAdded -> { - i1.libraryManga.manga.dateAdded.compareTo(i2.libraryManga.manga.dateAdded) + i1.libraryManga.manga.dateAdded + .compareTo(i2.libraryManga.manga.dateAdded) } LibrarySort.Type.TrackerMean -> { val item1Score = trackerScores[i1.libraryManga.id] ?: defaultTrackerScoreSortValue @@ -316,23 +323,23 @@ class LibraryScreenModel( } return this.mapValues { entry -> - val comparator = if (keys.find { it.id == entry.key.id }!!.sort.isAscending) { - Comparator(sortFn) - } else { - Collections.reverseOrder(sortFn) - } + val comparator = + if (keys.find { it.id == entry.key.id }!!.sort.isAscending) { + Comparator(sortFn) + } else { + Collections.reverseOrder(sortFn) + } entry.value.sortedWith(comparator.thenComparator(sortAlphabetically)) } } - private fun getLibraryItemPreferencesFlow(): Flow { - return combine( + private fun getLibraryItemPreferencesFlow(): Flow = + combine( libraryPreferences.downloadBadge().changes(), libraryPreferences.localBadge().changes(), libraryPreferences.languageBadge().changes(), libraryPreferences.autoUpdateMangaRestrictions().changes(), - preferences.downloadedOnly().changes(), libraryPreferences.filterDownloaded().changes(), libraryPreferences.filterUnread().changes(), @@ -355,45 +362,47 @@ class LibraryScreenModel( filterIntervalCustom = it[10] as TriState, ) } - } /** * Get the categories and all its manga from the database. */ private fun getLibraryFlow(): Flow { - val libraryMangasFlow = combine( - getLibraryManga.subscribe(), - getLibraryItemPreferencesFlow(), - downloadCache.changes, - ) { libraryMangaList, prefs, _ -> - libraryMangaList - .map { libraryManga -> - // Display mode based on user preference: take it from global library setting or category - LibraryItem( - libraryManga, - downloadCount = if (prefs.downloadBadge) { - downloadManager.getDownloadCount(libraryManga.manga).toLong() - } else { - 0 - }, - unreadCount = libraryManga.unreadCount, - isLocal = if (prefs.localBadge) libraryManga.manga.isLocal() else false, - sourceLanguage = if (prefs.languageBadge) { - sourceManager.getOrStub(libraryManga.manga.source).lang - } else { - "" - }, - ) - } - .groupBy { it.libraryManga.category } - } + val libraryMangasFlow = + combine( + getLibraryManga.subscribe(), + getLibraryItemPreferencesFlow(), + downloadCache.changes, + ) { libraryMangaList, prefs, _ -> + libraryMangaList + .map { libraryManga -> + // Display mode based on user preference: take it from global library setting or category + LibraryItem( + libraryManga, + downloadCount = + if (prefs.downloadBadge) { + downloadManager.getDownloadCount(libraryManga.manga).toLong() + } else { + 0 + }, + unreadCount = libraryManga.unreadCount, + isLocal = if (prefs.localBadge) libraryManga.manga.isLocal() else false, + sourceLanguage = + if (prefs.languageBadge) { + sourceManager.getOrStub(libraryManga.manga.source).lang + } else { + "" + }, + ) + }.groupBy { it.libraryManga.category } + } return combine(getCategories.subscribe(), libraryMangasFlow) { categories, libraryManga -> - val displayCategories = if (libraryManga.isNotEmpty() && !libraryManga.containsKey(0)) { - categories.fastFilterNot { it.isSystemCategory } - } else { - categories - } + val displayCategories = + if (libraryManga.isNotEmpty() && !libraryManga.containsKey(0)) { + categories.fastFilterNot { it.isSystemCategory } + } else { + categories + } displayCategories.associateWith { libraryManga[it.id].orEmpty() } } @@ -407,9 +416,10 @@ class LibraryScreenModel( private fun getTrackingFilterFlow(): Flow> { val loggedInTrackers = trackerManager.loggedInTrackers() return if (loggedInTrackers.isNotEmpty()) { - val prefFlows = loggedInTrackers - .map { libraryPreferences.filterTracking(it.id.toInt()).changes() } - .toTypedArray() + val prefFlows = + loggedInTrackers + .map { libraryPreferences.filterTracking(it.id.toInt()).changes() } + .toTypedArray() combine(*prefFlows) { loggedInTrackers .mapIndexed { index, tracker -> tracker.id to it[index] } @@ -432,9 +442,8 @@ class LibraryScreenModel( .reduce { set1, set2 -> set1.intersect(set2) } } - suspend fun getNextUnreadChapter(manga: Manga): Chapter? { - return getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true).getNextUnread(manga, downloadManager) - } + suspend fun getNextUnreadChapter(manga: Manga): Chapter? = + getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true).getNextUnread(manga, downloadManager) /** * Returns the mix (non-common) categories for the given list of manga. @@ -467,20 +476,24 @@ class LibraryScreenModel( * @param mangas the list of manga. * @param amount the amount to queue or null to queue all */ - private fun downloadUnreadChapters(mangas: List, amount: Int?) { + private fun downloadUnreadChapters( + mangas: List, + amount: Int?, + ) { screenModelScope.launchNonCancellable { mangas.forEach { manga -> - val chapters = getNextChapters.await(manga.id) - .fastFilterNot { chapter -> - downloadManager.getQueuedDownloadOrNull(chapter.id) != null || - downloadManager.isChapterDownloaded( - chapter.name, - chapter.scanlator, - manga.title, - manga.source, - ) - } - .let { if (amount != null) it.take(amount) else it } + val chapters = + getNextChapters + .await(manga.id) + .fastFilterNot { chapter -> + downloadManager.getQueuedDownloadOrNull(chapter.id) != null || + downloadManager.isChapterDownloaded( + chapter.name, + chapter.scanlator, + manga.title, + manga.source, + ) + }.let { if (amount != null) it.take(amount) else it } downloadManager.downloadChapters(manga, chapters) } @@ -510,18 +523,23 @@ class LibraryScreenModel( * @param deleteFromLibrary whether to delete manga from library. * @param deleteChapters whether to delete downloaded chapters. */ - fun removeMangas(mangaList: List, deleteFromLibrary: Boolean, deleteChapters: Boolean) { + fun removeMangas( + mangaList: List, + deleteFromLibrary: Boolean, + deleteChapters: Boolean, + ) { screenModelScope.launchNonCancellable { val mangaToDelete = mangaList.distinctBy { it.id } if (deleteFromLibrary) { - val toDelete = mangaToDelete.map { - it.removeCovers(coverCache) - MangaUpdate( - favorite = false, - id = it.id, - ) - } + val toDelete = + mangaToDelete.map { + it.removeCovers(coverCache) + MangaUpdate( + favorite = false, + id = it.id, + ) + } updateManga.awaitAll(toDelete) } @@ -543,28 +561,32 @@ class LibraryScreenModel( * @param addCategories the categories to add for all mangas. * @param removeCategories the categories to remove in all mangas. */ - fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { + fun setMangaCategories( + mangaList: List, + addCategories: List, + removeCategories: List, + ) { screenModelScope.launchNonCancellable { mangaList.forEach { manga -> - val categoryIds = getCategories.await(manga.id) - .map { it.id } - .subtract(removeCategories.toSet()) - .plus(addCategories) - .toList() + val categoryIds = + getCategories + .await(manga.id) + .map { it.id } + .subtract(removeCategories.toSet()) + .plus(addCategories) + .toList() setMangaCategories.await(manga.id, categoryIds) } } } - fun getDisplayMode(): PreferenceMutableState { - return libraryPreferences.displayMode().asState(screenModelScope) - } + fun getDisplayMode(): PreferenceMutableState = + libraryPreferences.displayMode().asState(screenModelScope) - fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState { - return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()) + fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState = + (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()) .asState(screenModelScope) - } suspend fun getRandomLibraryItemForCurrentCategory(): LibraryItem? { if (state.value.categories.isEmpty()) return null @@ -586,13 +608,14 @@ class LibraryScreenModel( fun toggleSelection(manga: LibraryManga) { mutableState.update { state -> - val newSelection = state.selection.mutate { list -> - if (list.fastAny { it.id == manga.id }) { - list.removeAll { it.id == manga.id } - } else { - list.add(manga) + val newSelection = + state.selection.mutate { list -> + if (list.fastAny { it.id == manga.id }) { + list.removeAll { it.id == manga.id } + } else { + list.add(manga) + } } - } state.copy(selection = newSelection) } } @@ -603,60 +626,68 @@ class LibraryScreenModel( */ fun toggleRangeSelection(manga: LibraryManga) { mutableState.update { state -> - val newSelection = state.selection.mutate { list -> - val lastSelected = list.lastOrNull() - if (lastSelected?.category != manga.category) { - list.add(manga) - return@mutate - } + val newSelection = + state.selection.mutate { list -> + val lastSelected = list.lastOrNull() + if (lastSelected?.category != manga.category) { + list.add(manga) + return@mutate + } - val items = state.getLibraryItemsByCategoryId(manga.category) - ?.fastMap { it.libraryManga }.orEmpty() - val lastMangaIndex = items.indexOf(lastSelected) - val curMangaIndex = items.indexOf(manga) - - val selectedIds = list.fastMap { it.id } - val selectionRange = when { - lastMangaIndex < curMangaIndex -> IntRange(lastMangaIndex, curMangaIndex) - curMangaIndex < lastMangaIndex -> IntRange(curMangaIndex, lastMangaIndex) - // We shouldn't reach this point - else -> return@mutate - } - val newSelections = selectionRange.mapNotNull { index -> - items[index].takeUnless { it.id in selectedIds } + val items = + state + .getLibraryItemsByCategoryId(manga.category) + ?.fastMap { it.libraryManga } + .orEmpty() + val lastMangaIndex = items.indexOf(lastSelected) + val curMangaIndex = items.indexOf(manga) + + val selectedIds = list.fastMap { it.id } + val selectionRange = + when { + lastMangaIndex < curMangaIndex -> IntRange(lastMangaIndex, curMangaIndex) + curMangaIndex < lastMangaIndex -> IntRange(curMangaIndex, lastMangaIndex) + // We shouldn't reach this point + else -> return@mutate + } + val newSelections = + selectionRange.mapNotNull { index -> + items[index].takeUnless { it.id in selectedIds } + } + list.addAll(newSelections) } - list.addAll(newSelections) - } state.copy(selection = newSelection) } } fun selectAll(index: Int) { mutableState.update { state -> - val newSelection = state.selection.mutate { list -> - val categoryId = state.categories.getOrNull(index)?.id ?: -1 - val selectedIds = list.fastMap { it.id } - state.getLibraryItemsByCategoryId(categoryId) - ?.fastMapNotNull { item -> - item.libraryManga.takeUnless { it.id in selectedIds } - } - ?.let { list.addAll(it) } - } + val newSelection = + state.selection.mutate { list -> + val categoryId = state.categories.getOrNull(index)?.id ?: -1 + val selectedIds = list.fastMap { it.id } + state + .getLibraryItemsByCategoryId(categoryId) + ?.fastMapNotNull { item -> + item.libraryManga.takeUnless { it.id in selectedIds } + }?.let { list.addAll(it) } + } state.copy(selection = newSelection) } } fun invertSelection(index: Int) { mutableState.update { state -> - val newSelection = state.selection.mutate { list -> - val categoryId = state.categories[index].id - val items = state.getLibraryItemsByCategoryId(categoryId)?.fastMap { it.libraryManga }.orEmpty() - val selectedIds = list.fastMap { it.id } - val (toRemove, toAdd) = items.fastPartition { it.id in selectedIds } - val toRemoveIds = toRemove.fastMap { it.id } - list.removeAll { it.id in toRemoveIds } - list.addAll(toAdd) - } + val newSelection = + state.selection.mutate { list -> + val categoryId = state.categories[index].id + val items = state.getLibraryItemsByCategoryId(categoryId)?.fastMap { it.libraryManga }.orEmpty() + val selectedIds = list.fastMap { it.id } + val (toRemove, toAdd) = items.fastPartition { it.id in selectedIds } + val toRemoveIds = toRemove.fastMap { it.id } + list.removeAll { it.id in toRemoveIds } + list.addAll(toAdd) + } state.copy(selection = newSelection) } } @@ -677,15 +708,15 @@ class LibraryScreenModel( val common = getCommonCategories(mangaList) // Get indexes of the mix categories to preselect. val mix = getMixCategories(mangaList) - val preselected = categories - .map { - when (it) { - in common -> CheckboxState.State.Checked(it) - in mix -> CheckboxState.TriState.Exclude(it) - else -> CheckboxState.State.None(it) - } - } - .toImmutableList() + val preselected = + categories + .map { + when (it) { + in common -> CheckboxState.State.Checked(it) + in mix -> CheckboxState.TriState.Exclude(it) + else -> CheckboxState.State.None(it) + } + }.toImmutableList() mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) } } } @@ -701,11 +732,15 @@ class LibraryScreenModel( sealed interface Dialog { data object SettingsSheet : Dialog + data class ChangeCategory( val manga: List, val initialSelection: ImmutableList>, ) : Dialog - data class DeleteManga(val manga: List) : Dialog + + data class DeleteManga( + val manga: List, + ) : Dialog } @Immutable @@ -714,7 +749,6 @@ class LibraryScreenModel( val localBadge: Boolean, val languageBadge: Boolean, val skipOutsideReleasePeriod: Boolean, - val globalFilterDownloaded: Boolean, val filterDownloaded: TriState, val filterUnread: TriState, @@ -749,17 +783,21 @@ class LibraryScreenModel( val categories = library.keys.toList() - fun getLibraryItemsByCategoryId(categoryId: Long): List? { - return library.firstNotNullOfOrNull { (k, v) -> v.takeIf { k.id == categoryId } } - } + fun getLibraryItemsByCategoryId(categoryId: Long): List? = + library.firstNotNullOfOrNull { (k, v) -> + v.takeIf { + k.id == categoryId + } + } - fun getLibraryItemsByPage(page: Int): List { - return library.values.toTypedArray().getOrNull(page).orEmpty() - } + fun getLibraryItemsByPage(page: Int): List = + library.values + .toTypedArray() + .getOrNull(page) + .orEmpty() - fun getMangaCountForCategory(category: Category): Int? { - return if (showMangaCount || !searchQuery.isNullOrEmpty()) library[category]?.size else null - } + fun getMangaCountForCategory(category: Category): Int? = + if (showMangaCount || !searchQuery.isNullOrEmpty()) library[category]?.size else null fun getToolbarTitle( defaultTitle: String, @@ -767,16 +805,18 @@ class LibraryScreenModel( page: Int, ): LibraryToolbarTitle { val category = categories.getOrNull(page) ?: return LibraryToolbarTitle(defaultTitle) - val categoryName = category.let { - if (it.isSystemCategory) defaultCategoryTitle else it.name - } + val categoryName = + category.let { + if (it.isSystemCategory) defaultCategoryTitle else it.name + } val title = if (showCategoryTabs) defaultTitle else categoryName - val count = when { - !showMangaCount -> null - !showCategoryTabs -> getMangaCountForCategory(category) - // Whole library count - else -> libraryCount - } + val count = + when { + !showMangaCount -> null + !showCategoryTabs -> getMangaCountForCategory(category) + // Whole library count + else -> libraryCount + } return LibraryToolbarTitle(title, count) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt index f0856f830e..070f1f8637 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt @@ -24,7 +24,6 @@ class LibrarySettingsScreenModel( private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), ) : ScreenModel { - val trackers get() = trackerManager.trackers.filter { it.isLoggedIn } @@ -42,7 +41,11 @@ class LibrarySettingsScreenModel( setDisplayMode.await(mode) } - fun setSort(category: Category?, mode: LibrarySort.Type, direction: LibrarySort.Direction) { + fun setSort( + category: Category?, + mode: LibrarySort.Type, + direction: LibrarySort.Direction, + ) { screenModelScope.launchIO { setSortModeForCategory.await(category, mode, direction) } 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 7acdb51f75..642c523414 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 @@ -62,7 +62,6 @@ import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.source.local.isLocal object LibraryTab : Tab { - override val options: TabOptions @Composable get() { @@ -95,11 +94,12 @@ object LibraryTab : Tab { val onClickRefresh: (Category?) -> Boolean = { category -> val started = LibraryUpdateJob.startNow(context, category) scope.launch { - val msgRes = when { - !started -> MR.strings.update_already_running - category != null -> MR.strings.updating_category - else -> MR.strings.updating_library - } + val msgRes = + when { + !started -> MR.strings.update_already_running + category != null -> MR.strings.updating_category + else -> MR.strings.updating_library + } snackbarHostState.showSnackbar(context.stringResource(msgRes)) } started @@ -107,11 +107,12 @@ object LibraryTab : Tab { Scaffold( topBar = { scrollBehavior -> - val title = state.getToolbarTitle( - defaultTitle = stringResource(MR.strings.label_library), - defaultCategoryTitle = stringResource(MR.strings.label_default), - page = screenModel.activeCategoryIndex, - ) + val title = + state.getToolbarTitle( + defaultTitle = stringResource(MR.strings.label_library), + defaultCategoryTitle = stringResource(MR.strings.label_default), + page = screenModel.activeCategoryIndex, + ) val tabVisible = state.showCategoryTabs && state.categories.size > 1 LibraryToolbar( hasActiveFilters = state.hasActiveFilters, @@ -146,8 +147,9 @@ object LibraryTab : Tab { onChangeCategoryClicked = screenModel::openChangeCategoryDialog, onMarkAsReadClicked = { screenModel.markReadSelection(true) }, onMarkAsUnreadClicked = { screenModel.markReadSelection(false) }, - onDownloadClicked = screenModel::runDownloadActionSelection - .takeIf { state.selection.fastAll { !it.manga.isLocal() } }, + onDownloadClicked = + screenModel::runDownloadActionSelection + .takeIf { state.selection.fastAll { !it.manga.isLocal() } }, onDeleteClicked = screenModel::openDeleteMangaDialog, ) }, @@ -160,13 +162,14 @@ object LibraryTab : Tab { EmptyScreen( stringRes = MR.strings.information_empty_library, modifier = Modifier.padding(contentPadding), - actions = persistentListOf( - EmptyScreenAction( - stringRes = MR.strings.getting_started_guide, - icon = Icons.AutoMirrored.Outlined.HelpOutline, - onClick = { handler.openUri(GETTING_STARTED_URL) }, + actions = + persistentListOf( + EmptyScreenAction( + stringRes = MR.strings.getting_started_guide, + icon = Icons.AutoMirrored.Outlined.HelpOutline, + onClick = { handler.openUri(GETTING_STARTED_URL) }, + ), ), - ), ) } else -> { @@ -180,19 +183,22 @@ object LibraryTab : Tab { showPageTabs = state.showCategoryTabs || !state.searchQuery.isNullOrEmpty(), onChangeCurrentPage = { screenModel.activeCategoryIndex = it }, onMangaClicked = { navigator.push(MangaScreen(it)) }, - onContinueReadingClicked = { it: LibraryManga -> - scope.launchIO { - val chapter = screenModel.getNextUnreadChapter(it.manga) - if (chapter != null) { - context.startActivity( - ReaderActivity.newIntent(context, chapter.mangaId, chapter.id), - ) - } else { - snackbarHostState.showSnackbar(context.stringResource(MR.strings.no_next_chapter)) + onContinueReadingClicked = + { it: LibraryManga -> + scope.launchIO { + val chapter = screenModel.getNextUnreadChapter(it.manga) + if (chapter != null) { + context.startActivity( + ReaderActivity.newIntent(context, chapter.mangaId, chapter.id), + ) + } else { + snackbarHostState.showSnackbar( + context.stringResource(MR.strings.no_next_chapter), + ) + } } - } - Unit - }.takeIf { state.showMangaContinueButton }, + Unit + }.takeIf { state.showMangaContinueButton }, onToggleSelection = screenModel::toggleSelection, onToggleRangeSelection = { screenModel.toggleRangeSelection(it) @@ -212,18 +218,19 @@ object LibraryTab : Tab { val onDismissRequest = screenModel::closeDialog when (val dialog = state.dialog) { - is LibraryScreenModel.Dialog.SettingsSheet -> run { - val category = state.categories.getOrNull(screenModel.activeCategoryIndex) - if (category == null) { - onDismissRequest() - return@run + is LibraryScreenModel.Dialog.SettingsSheet -> + run { + val category = state.categories.getOrNull(screenModel.activeCategoryIndex) + if (category == null) { + onDismissRequest() + return@run + } + LibrarySettingsDialog( + onDismissRequest = onDismissRequest, + screenModel = settingsScreenModel, + category = category, + ) } - LibrarySettingsDialog( - onDismissRequest = onDismissRequest, - screenModel = settingsScreenModel, - category = category, - ) - } is LibraryScreenModel.Dialog.ChangeCategory -> { ChangeCategoryDialog( initialSelection = dialog.initialSelection, @@ -276,9 +283,11 @@ object LibraryTab : Tab { // For invoking search from other screen private val queryEvent = Channel() + suspend fun search(query: String) = queryEvent.send(query) // For opening settings sheet in LibraryController private val requestSettingsSheetEvent = Channel() + private suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit) } 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 39c9dd5c72..cd13a57d53 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 @@ -100,7 +100,6 @@ import uy.kohesive.injekt.injectLazy import androidx.compose.ui.graphics.Color.Companion as ComposeColor class MainActivity : BaseActivity() { - private val libraryPreferences: LibraryPreferences by injectLazy() private val preferences: BasePreferences by injectLazy() @@ -144,12 +143,13 @@ class MainActivity : BaseActivity() { // Set status bar color considering the top app state banner val systemUiController = rememberSystemUiController() val isSystemInDarkTheme = isSystemInDarkTheme() - val statusBarBackgroundColor = when { - indexing -> IndexingBannerBackgroundColor - downloadOnly -> DownloadedOnlyBannerBackgroundColor - incognito -> IncognitoModeBannerBackgroundColor - else -> MaterialTheme.colorScheme.surface - } + val statusBarBackgroundColor = + when { + indexing -> IndexingBannerBackgroundColor + downloadOnly -> DownloadedOnlyBannerBackgroundColor + incognito -> IncognitoModeBannerBackgroundColor + else -> MaterialTheme.colorScheme.surface + } LaunchedEffect(systemUiController, statusBarBackgroundColor) { systemUiController.setStatusBarColor( color = ComposeColor.Transparent, @@ -163,11 +163,12 @@ class MainActivity : BaseActivity() { val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) LaunchedEffect(systemUiController, isSystemInDarkTheme, navbarScrimColor) { systemUiController.setNavigationBarColor( - color = if (context.isNavigationBarNeedsScrim()) { - navbarScrimColor.copy(alpha = 0.7f) - } else { - ComposeColor.Transparent - }, + color = + if (context.isNavigationBarNeedsScrim()) { + navbarScrimColor.copy(alpha = 0.7f) + } else { + ComposeColor.Transparent + }, darkIcons = !isSystemInDarkTheme, navigationBarContrastEnforced = false, transformColorForLightContent = { ComposeColor.Black }, @@ -204,9 +205,10 @@ class MainActivity : BaseActivity() { ) { contentPadding -> // Consume insets already used by app state banners Box( - modifier = Modifier - .padding(contentPadding) - .consumeWindowInsets(contentPadding), + modifier = + Modifier + .padding(contentPadding) + .consumeWindowInsets(contentPadding), ) { // Shows current screen DefaultNavigatorScreenTransition(navigator = navigator) @@ -215,7 +217,9 @@ class MainActivity : BaseActivity() { // Pop source-related screens when incognito mode is turned off LaunchedEffect(Unit) { - preferences.incognitoMode().changes() + preferences + .incognitoMode() + .changes() .drop(1) .filter { !it } .onEach { @@ -225,8 +229,7 @@ class MainActivity : BaseActivity() { ) { navigator.popUntilRoot() } - } - .launchIn(this) + }.launchIn(this) } HandleOnNewIntent(context = context, navigator = navigator) @@ -278,7 +281,10 @@ class MainActivity : BaseActivity() { } @Composable - private fun HandleOnNewIntent(context: Context, navigator: Navigator) { + private fun HandleOnNewIntent( + context: Context, + navigator: Navigator, + ) { LaunchedEffect(Unit) { callbackFlow { val componentActivity = context as ComponentActivity @@ -300,12 +306,13 @@ class MainActivity : BaseActivity() { try { val result = AppUpdateChecker().checkForUpdate(context) if (result is GetApplicationRelease.Result.NewUpdate) { - val updateScreen = NewUpdateScreen( - versionName = result.release.version, - changelogInfo = result.release.info, - releaseLink = result.release.releaseLink, - downloadLink = result.release.getDownloadLink(), - ) + val updateScreen = + NewUpdateScreen( + versionName = result.release.version, + changelogInfo = result.release.info, + releaseLink = result.release.releaseLink, + downloadLink = result.release.getDownloadLink(), + ) navigator.push(updateScreen) } } catch (e: Exception) { @@ -351,26 +358,28 @@ class MainActivity : BaseActivity() { // For some reason the SplashScreen applies (incorrect) Y translation to the iconView splashProvider.iconView.translationY = 0F - val activityAnim = ValueAnimator.ofFloat(1F, 0F).apply { - interpolator = LinearOutSlowInInterpolator() - duration = SPLASH_EXIT_ANIM_DURATION - addUpdateListener { va -> - val value = va.animatedValue as Float - root.translationY = value * 16.dpToPx + val activityAnim = + ValueAnimator.ofFloat(1F, 0F).apply { + interpolator = LinearOutSlowInInterpolator() + duration = SPLASH_EXIT_ANIM_DURATION + addUpdateListener { va -> + val value = va.animatedValue as Float + root.translationY = value * 16.dpToPx + } } - } - val splashAnim = ValueAnimator.ofFloat(1F, 0F).apply { - interpolator = FastOutSlowInInterpolator() - duration = SPLASH_EXIT_ANIM_DURATION - addUpdateListener { va -> - val value = va.animatedValue as Float - splashProvider.view.alpha = value - } - doOnEnd { - splashProvider.remove() + val splashAnim = + ValueAnimator.ofFloat(1F, 0F).apply { + interpolator = FastOutSlowInInterpolator() + duration = SPLASH_EXIT_ANIM_DURATION + addUpdateListener { va -> + val value = va.animatedValue as Float + splashProvider.view.alpha = value + } + doOnEnd { + splashProvider.remove() + } } - } activityAnim.start() splashAnim.start() @@ -378,7 +387,10 @@ class MainActivity : BaseActivity() { } } - private fun handleIntentAction(intent: Intent, navigator: Navigator): Boolean { + private fun handleIntentAction( + intent: Intent, + navigator: Navigator, + ): Boolean { val notificationId = intent.getIntExtra("notificationId", -1) if (notificationId > -1) { NotificationReceiver.dismissNotification( @@ -388,59 +400,60 @@ class MainActivity : BaseActivity() { ) } - val tabToOpen = when (intent.action) { - Constants.SHORTCUT_LIBRARY -> HomeScreen.Tab.Library() - Constants.SHORTCUT_MANGA -> { - val idToOpen = intent.extras?.getLong(Constants.MANGA_EXTRA) ?: return false - navigator.popUntilRoot() - HomeScreen.Tab.Library(idToOpen) - } - Constants.SHORTCUT_UPDATES -> HomeScreen.Tab.Updates - Constants.SHORTCUT_HISTORY -> HomeScreen.Tab.History - Constants.SHORTCUT_SOURCES -> HomeScreen.Tab.Browse(false) - Constants.SHORTCUT_EXTENSIONS -> HomeScreen.Tab.Browse(true) - Constants.SHORTCUT_DOWNLOADS -> { - navigator.popUntilRoot() - HomeScreen.Tab.More(toDownloads = true) - } - Intent.ACTION_SEARCH, Intent.ACTION_SEND, "com.google.android.gms.actions.SEARCH_ACTION" -> { - // If the intent match the "standard" Android search intent - // or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant) - - // Get the search query provided in extras, and if not null, perform a global search with it. - val query = intent.getStringExtra(SearchManager.QUERY) ?: intent.getStringExtra(Intent.EXTRA_TEXT) - if (!query.isNullOrEmpty()) { + val tabToOpen = + when (intent.action) { + Constants.SHORTCUT_LIBRARY -> HomeScreen.Tab.Library() + Constants.SHORTCUT_MANGA -> { + val idToOpen = intent.extras?.getLong(Constants.MANGA_EXTRA) ?: return false navigator.popUntilRoot() - navigator.push(DeepLinkScreen(query)) + HomeScreen.Tab.Library(idToOpen) } - null - } - INTENT_SEARCH -> { - val query = intent.getStringExtra(INTENT_SEARCH_QUERY) - if (!query.isNullOrEmpty()) { - val filter = intent.getStringExtra(INTENT_SEARCH_FILTER) + Constants.SHORTCUT_UPDATES -> HomeScreen.Tab.Updates + Constants.SHORTCUT_HISTORY -> HomeScreen.Tab.History + Constants.SHORTCUT_SOURCES -> HomeScreen.Tab.Browse(false) + Constants.SHORTCUT_EXTENSIONS -> HomeScreen.Tab.Browse(true) + Constants.SHORTCUT_DOWNLOADS -> { navigator.popUntilRoot() - navigator.push(GlobalSearchScreen(query, filter)) + HomeScreen.Tab.More(toDownloads = true) } - null - } - Intent.ACTION_VIEW -> { - // Handling opening of backup files - if (intent.data.toString().endsWith(".tachibk")) { - navigator.popUntilRoot() - navigator.push(RestoreBackupScreen(intent.data.toString())) + Intent.ACTION_SEARCH, Intent.ACTION_SEND, "com.google.android.gms.actions.SEARCH_ACTION" -> { + // If the intent match the "standard" Android search intent + // or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant) + + // Get the search query provided in extras, and if not null, perform a global search with it. + val query = intent.getStringExtra(SearchManager.QUERY) ?: intent.getStringExtra(Intent.EXTRA_TEXT) + if (!query.isNullOrEmpty()) { + navigator.popUntilRoot() + navigator.push(DeepLinkScreen(query)) + } + null } - // Deep link to add extension repo - else if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") { - intent.data?.getQueryParameter("url")?.let { repoUrl -> + INTENT_SEARCH -> { + val query = intent.getStringExtra(INTENT_SEARCH_QUERY) + if (!query.isNullOrEmpty()) { + val filter = intent.getStringExtra(INTENT_SEARCH_FILTER) navigator.popUntilRoot() - navigator.push(ExtensionReposScreen(repoUrl)) + navigator.push(GlobalSearchScreen(query, filter)) } + null } - null + Intent.ACTION_VIEW -> { + // Handling opening of backup files + if (intent.data.toString().endsWith(".tachibk")) { + navigator.popUntilRoot() + navigator.push(RestoreBackupScreen(intent.data.toString())) + } + // Deep link to add extension repo + else if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") { + intent.data?.getQueryParameter("url")?.let { repoUrl -> + navigator.popUntilRoot() + navigator.push(ExtensionReposScreen(repoUrl)) + } + } + null + } + else -> return false } - else -> return false - } if (tabToOpen != null) { lifecycleScope.launch { HomeScreen.openTab(tabToOpen) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt index de79d4d0dd..2defee115a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt @@ -37,13 +37,12 @@ class MangaCoverScreenModel( private val imageSaver: ImageSaver = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), - val snackbarHostState: SnackbarHostState = SnackbarHostState(), ) : StateScreenModel(null) { - init { screenModelScope.launchIO { - getManga.subscribe(mangaId) + getManga + .subscribe(mangaId) .collect { newManga -> mutableState.update { newManga } } } } @@ -89,15 +88,24 @@ class MangaCoverScreenModel( * @param context The context for building and executing the ImageRequest * @return the uri to saved file */ - private suspend fun saveCoverInternal(context: Context, temp: Boolean): Uri? { + private suspend fun saveCoverInternal( + context: Context, + temp: Boolean, + ): Uri? { val manga = state.value ?: return null - val req = ImageRequest.Builder(context) - .data(manga) - .size(Size.ORIGINAL) - .build() + val req = + ImageRequest + .Builder(context) + .data(manga) + .size(Size.ORIGINAL) + .build() return withIOContext { - val result = context.imageLoader.execute(req).image?.asDrawable(context.resources) + val result = + context.imageLoader + .execute(req) + .image + ?.asDrawable(context.resources) // TODO: Handle animated cover val bitmap = result?.getBitmapOrNull() ?: return@withIOContext null @@ -117,7 +125,10 @@ class MangaCoverScreenModel( * @param context Context. * @param data uri of the cover resource. */ - fun editCover(context: Context, data: Uri) { + fun editCover( + context: Context, + data: Uri, + ) { val manga = state.value ?: return screenModelScope.launchIO { context.contentResolver.openInputStream(data)?.use { @@ -153,7 +164,10 @@ class MangaCoverScreenModel( } } - private fun notifyFailedCoverUpdate(context: Context, e: Throwable) { + private fun notifyFailedCoverUpdate( + context: Context, + e: Throwable, + ) { screenModelScope.launch { snackbarHostState.showSnackbar( context.stringResource(MR.strings.notification_cover_update_failed), 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 c2466ffd4f..270bda569c 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 @@ -68,8 +68,8 @@ import tachiyomi.presentation.core.screens.LoadingScreen class MangaScreen( private val mangaId: Long, val fromSource: Boolean = false, -) : Screen(), AssistContentScreen { - +) : Screen(), + AssistContentScreen { private var assistUrl: String? = null override fun onProvideAssistUrl() = assistUrl @@ -123,20 +123,22 @@ class MangaScreen( screenModel.toggleFavorite() haptic.performHapticFeedback(HapticFeedbackType.LongPress) }, - onWebViewClicked = { - openMangaInWebView( - navigator, - screenModel.manga, - screenModel.source, - ) - }.takeIf { isHttpSource }, - onWebViewLongClicked = { - copyMangaUrl( - context, - screenModel.manga, - screenModel.source, - ) - }.takeIf { isHttpSource }, + onWebViewClicked = + { + openMangaInWebView( + navigator, + screenModel.manga, + screenModel.source, + ) + }.takeIf { isHttpSource }, + onWebViewLongClicked = + { + copyMangaUrl( + context, + screenModel.manga, + screenModel.source, + ) + }.takeIf { isHttpSource }, onTrackingClicked = { if (screenModel.loggedInTrackers.isEmpty()) { navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking)) @@ -153,12 +155,14 @@ class MangaScreen( onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource }, onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() }, onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite }, - onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf { - successState.manga.favorite - }, - onMigrateClicked = { - navigator.push(MigrateSearchScreen(successState.manga.id)) - }.takeIf { successState.manga.favorite }, + onEditFetchIntervalClicked = + screenModel::showSetFetchIntervalDialog.takeIf { + successState.manga.favorite + }, + onMigrateClicked = + { + navigator.push(MigrateSearchScreen(successState.manga.id)) + }.takeIf { successState.manga.favorite }, onMultiBookmarkClicked = screenModel::bookmarkChapters, onMultiMarkAsReadClicked = screenModel::markChaptersRead, onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead, @@ -215,26 +219,28 @@ class MangaScreen( onPopScreen = { navigator.replace(MangaScreen(dialog.newManga.id)) }, ) } - MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog( - onDismissRequest = onDismissRequest, - manga = successState.manga, - onDownloadFilterChanged = screenModel::setDownloadedFilter, - onUnreadFilterChanged = screenModel::setUnreadFilter, - onBookmarkedFilterChanged = screenModel::setBookmarkedFilter, - onSortModeChanged = screenModel::setSorting, - onDisplayModeChanged = screenModel::setDisplayMode, - onSetAsDefault = screenModel::setCurrentSettingsAsDefault, - onResetToDefault = screenModel::resetToDefaultSettings, - scanlatorFilterActive = successState.scanlatorFilterActive, - onScanlatorFilterClicked = { showScanlatorsDialog = true }, - ) + MangaScreenModel.Dialog.SettingsSheet -> + ChapterSettingsDialog( + onDismissRequest = onDismissRequest, + manga = successState.manga, + onDownloadFilterChanged = screenModel::setDownloadedFilter, + onUnreadFilterChanged = screenModel::setUnreadFilter, + onBookmarkedFilterChanged = screenModel::setBookmarkedFilter, + onSortModeChanged = screenModel::setSorting, + onDisplayModeChanged = screenModel::setDisplayMode, + onSetAsDefault = screenModel::setCurrentSettingsAsDefault, + onResetToDefault = screenModel::resetToDefaultSettings, + scanlatorFilterActive = successState.scanlatorFilterActive, + onScanlatorFilterClicked = { showScanlatorsDialog = true }, + ) MangaScreenModel.Dialog.TrackSheet -> { NavigatorAdaptiveSheet( - screen = TrackInfoDialogHomeScreen( - mangaId = successState.manga.id, - mangaTitle = successState.manga.title, - sourceId = successState.source.id, - ), + screen = + TrackInfoDialogHomeScreen( + mangaId = successState.manga.id, + mangaTitle = successState.manga.title, + sourceId = successState.source.id, + ), enableSwipeDismiss = { it.lastItem is TrackInfoDialogHomeScreen }, onDismissRequest = onDismissRequest, ) @@ -243,10 +249,11 @@ class MangaScreen( val sm = rememberScreenModel { MangaCoverScreenModel(successState.manga.id) } val manga by sm.state.collectAsState() if (manga != null) { - val getContent = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { - if (it == null) return@rememberLauncherForActivityResult - sm.editCover(context, it) - } + val getContent = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { + if (it == null) return@rememberLauncherForActivityResult + sm.editCover(context, it) + } MangaCoverDialog( coverDataProvider = { manga!! }, snackbarHostState = sm.snackbarHostState, @@ -270,8 +277,9 @@ class MangaScreen( interval = dialog.manga.fetchInterval, nextUpdate = dialog.manga.expectedNextUpdate, onDismissRequest = onDismissRequest, - onValueChanged = { interval: Int -> screenModel.setFetchInterval(dialog.manga, interval) } - .takeIf { screenModel.isUpdateIntervalEnabled }, + onValueChanged = + { interval: Int -> screenModel.setFetchInterval(dialog.manga, interval) } + .takeIf { screenModel.isUpdateIntervalEnabled }, ) } } @@ -286,15 +294,24 @@ class MangaScreen( } } - private fun continueReading(context: Context, unreadChapter: Chapter?) { + private fun continueReading( + context: Context, + unreadChapter: Chapter?, + ) { if (unreadChapter != null) openChapter(context, unreadChapter) } - private fun openChapter(context: Context, chapter: Chapter) { + private fun openChapter( + context: Context, + chapter: Chapter, + ) { context.startActivity(ReaderActivity.newIntent(context, chapter.mangaId, chapter.id)) } - private fun getMangaUrl(manga_: Manga?, source_: Source?): String? { + private fun getMangaUrl( + manga_: Manga?, + source_: Source?, + ): String? { val manga = manga_ ?: return null val source = source_ as? HttpSource ?: return null @@ -305,7 +322,11 @@ class MangaScreen( } } - private fun openMangaInWebView(navigator: Navigator, manga_: Manga?, source_: Source?) { + private fun openMangaInWebView( + navigator: Navigator, + manga_: Manga?, + source_: Source?, + ) { getMangaUrl(manga_, source_)?.let { url -> navigator.push( WebViewScreen( @@ -317,7 +338,11 @@ class MangaScreen( } } - private fun shareManga(context: Context, manga_: Manga?, source_: Source?) { + private fun shareManga( + context: Context, + manga_: Manga?, + source_: Source?, + ) { try { getMangaUrl(manga_, source_)?.let { url -> val intent = url.toUri().toShareIntent(context, type = "text/plain") @@ -338,7 +363,11 @@ class MangaScreen( * * @param query the search query to the parent controller */ - private suspend fun performSearch(navigator: Navigator, query: String, global: Boolean) { + private suspend fun performSearch( + navigator: Navigator, + query: String, + global: Boolean, + ) { if (global) { navigator.push(GlobalSearchScreen(query)) return @@ -365,7 +394,11 @@ class MangaScreen( * * @param genreName the search genre to the parent controller */ - private suspend fun performGenreSearch(navigator: Navigator, genreName: String, source: Source) { + private suspend fun performGenreSearch( + navigator: Navigator, + genreName: String, + source: Source, + ) { if (navigator.size < 2) { return } @@ -382,7 +415,11 @@ class MangaScreen( /** * Copy Manga URL to Clipboard */ - private fun copyMangaUrl(context: Context, manga_: Manga?, source_: Source?) { + private fun copyMangaUrl( + context: Context, + manga_: Manga?, + source_: Source?, + ) { val manga = manga_ ?: return val source = source_ as? HttpSource ?: return val url = source.getMangaUrl(manga.toSManga()) 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 14cb037df2..421c76b23e 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 @@ -113,7 +113,6 @@ class MangaScreenModel( private val mangaRepository: MangaRepository = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), ) : StateScreenModel(State.Loading) { - private val successState: State.Success? get() = state.value as? State.Success @@ -175,7 +174,8 @@ class MangaScreenModel( } screenModelScope.launchIO { - getExcludedScanlators.subscribe(mangaId) + getExcludedScanlators + .subscribe(mangaId) .distinctUntilChanged() .collectLatest { excludedScanlators -> updateSuccessState { @@ -185,7 +185,8 @@ class MangaScreenModel( } screenModelScope.launchIO { - getAvailableScanlators.subscribe(mangaId) + getAvailableScanlators + .subscribe(mangaId) .distinctUntilChanged() .collectLatest { availableScanlators -> updateSuccessState { @@ -198,8 +199,10 @@ class MangaScreenModel( screenModelScope.launchIO { val manga = getMangaAndChapters.awaitManga(mangaId) - val chapters = getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true) - .toChapterListItems(manga) + val chapters = + getMangaAndChapters + .awaitChapters(mangaId, applyScanlatorFilter = true) + .toChapterListItems(manga) if (!manga.favorite) { setMangaDefaultChapterFlags.await(manga) @@ -227,10 +230,11 @@ class MangaScreenModel( // Fetch info-chapters when needed if (screenModelScope.isActive) { - val fetchFromSourceTasks = listOf( - async { if (needRefreshInfo) fetchMangaFromSource() }, - async { if (needRefreshChapter) fetchChaptersFromSource() }, - ) + val fetchFromSourceTasks = + listOf( + async { if (needRefreshInfo) fetchMangaFromSource() }, + async { if (needRefreshChapter) fetchChaptersFromSource() }, + ) fetchFromSourceTasks.awaitAll() } @@ -242,10 +246,11 @@ class MangaScreenModel( fun fetchAllFromSource(manualFetch: Boolean = true) { screenModelScope.launch { updateSuccessState { it.copy(isRefreshingData = true) } - val fetchFromSourceTasks = listOf( - async { fetchMangaFromSource(manualFetch) }, - async { fetchChaptersFromSource(manualFetch) }, - ) + val fetchFromSourceTasks = + listOf( + async { fetchMangaFromSource(manualFetch) }, + async { fetchChaptersFromSource(manualFetch) }, + ) fetchFromSourceTasks.awaitAll() updateSuccessState { it.copy(isRefreshingData = false) } } @@ -279,11 +284,12 @@ class MangaScreenModel( onRemoved = { screenModelScope.launch { if (!hasDownloads()) return@launch - val result = snackbarHostState.showSnackbar( - message = context.stringResource(MR.strings.delete_downloads_for_manga), - actionLabel = context.stringResource(MR.strings.action_delete), - withDismissAction = true, - ) + val result = + snackbarHostState.showSnackbar( + message = context.stringResource(MR.strings.delete_downloads_for_manga), + actionLabel = context.stringResource(MR.strings.action_delete), + withDismissAction = true, + ) if (result == SnackbarResult.ActionPerformed) { deleteDownloads() } @@ -360,10 +366,11 @@ class MangaScreenModel( val selection = getMangaCategoryIds(manga) updateSuccessState { successState -> successState.copy( - dialog = Dialog.ChangeCategory( - manga = manga, - initialSelection = categories.mapAsCheckboxState { it.id in selection }.toImmutableList(), - ), + dialog = + Dialog.ChangeCategory( + manga = manga, + initialSelection = categories.mapAsCheckboxState { it.id in selection }.toImmutableList(), + ), ) } } @@ -376,7 +383,10 @@ class MangaScreenModel( } } - fun setFetchInterval(manga: Manga, interval: Int) { + fun setFetchInterval( + manga: Manga, + interval: Int, + ) { screenModelScope.launchIO { if ( updateManga.awaitUpdateFetchInterval( @@ -411,9 +421,7 @@ class MangaScreenModel( * * @return List of categories, not including the default category */ - suspend fun getCategories(): List { - return getCategories.await().filterNot { it.isSystemCategory } - } + suspend fun getCategories(): List = getCategories.await().filterNot { it.isSystemCategory } /** * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. @@ -421,12 +429,15 @@ class MangaScreenModel( * @param manga the manga to get categories from. * @return Array of category ids the manga is in, if none returns default id */ - private suspend fun getMangaCategoryIds(manga: Manga): List { - return getCategories.await(manga.id) + private suspend fun getMangaCategoryIds(manga: Manga): List = + getCategories + .await(manga.id) .map { it.id } - } - fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List) { + fun moveMangaToCategoriesAndAddToLibrary( + manga: Manga, + categories: List, + ) { moveMangaToCategory(categories) if (manga.favorite) return @@ -466,7 +477,8 @@ class MangaScreenModel( private fun observeDownloads() { screenModelScope.launchIO { - downloadManager.statusFlow() + downloadManager + .statusFlow() .filter { it.manga.id == successState?.manga?.id } .catch { error -> logcat(LogPriority.ERROR, error) } .collect { @@ -477,7 +489,8 @@ class MangaScreenModel( } screenModelScope.launchIO { - downloadManager.progressFlow() + downloadManager + .progressFlow() .filter { it.manga.id == successState?.manga?.id } .catch { error -> logcat(LogPriority.ERROR, error) } .collect { @@ -493,11 +506,13 @@ class MangaScreenModel( val modifiedIndex = successState.chapters.indexOfFirst { it.id == download.chapter.id } if (modifiedIndex < 0) return@updateSuccessState successState - val newChapters = successState.chapters.toMutableList().apply { - val item = removeAt(modifiedIndex) - .copy(downloadState = download.status, downloadProgress = download.progress) - add(modifiedIndex, item) - } + val newChapters = + successState.chapters.toMutableList().apply { + val item = + removeAt(modifiedIndex) + .copy(downloadState = download.status, downloadProgress = download.progress) + add(modifiedIndex, item) + } successState.copy(chapters = newChapters) } } @@ -505,21 +520,24 @@ class MangaScreenModel( private fun List.toChapterListItems(manga: Manga): List { val isLocal = manga.isLocal() return map { chapter -> - val activeDownload = if (isLocal) { - null - } else { - downloadManager.getQueuedDownloadOrNull(chapter.id) - } - val downloaded = if (isLocal) { - true - } else { - downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source) - } - val downloadState = when { - activeDownload != null -> activeDownload.status - downloaded -> Download.State.DOWNLOADED - else -> Download.State.NOT_DOWNLOADED - } + val activeDownload = + if (isLocal) { + null + } else { + downloadManager.getQueuedDownloadOrNull(chapter.id) + } + val downloaded = + if (isLocal) { + true + } else { + downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source) + } + val downloadState = + when { + activeDownload != null -> activeDownload.status + downloaded -> Download.State.DOWNLOADED + else -> Download.State.NOT_DOWNLOADED + } ChapterList.Item( chapter = chapter, @@ -539,24 +557,26 @@ class MangaScreenModel( withIOContext { val chapters = state.source.getChapterList(state.manga.toSManga()) - val newChapters = syncChaptersWithSource.await( - chapters, - state.manga, - state.source, - manualFetch, - ) + val newChapters = + syncChaptersWithSource.await( + chapters, + state.manga, + state.source, + manualFetch, + ) if (manualFetch) { downloadNewChapters(newChapters) } } } catch (e: Throwable) { - val message = if (e is NoChaptersException) { - context.stringResource(MR.strings.no_chapters_error) - } else { - logcat(LogPriority.ERROR, e) - with(context) { e.formattedMessage } - } + val message = + if (e is NoChaptersException) { + context.stringResource(MR.strings.no_chapters_error) + } else { + logcat(LogPriority.ERROR, e) + with(context) { e.formattedMessage } + } screenModelScope.launch { snackbarHostState.showSnackbar(message = message) @@ -569,7 +589,10 @@ class MangaScreenModel( /** * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] */ - fun chapterSwipe(chapterItem: ChapterList.Item, swipeAction: LibraryPreferences.ChapterSwipeAction) { + fun chapterSwipe( + chapterItem: ChapterList.Item, + swipeAction: LibraryPreferences.ChapterSwipeAction, + ) { screenModelScope.launch { executeChapterSwipeAction(chapterItem, swipeAction) } @@ -591,15 +614,16 @@ class MangaScreenModel( bookmarkChapters(listOf(chapter), !chapter.bookmark) } LibraryPreferences.ChapterSwipeAction.Download -> { - val downloadAction: ChapterDownloadAction = when (chapterItem.downloadState) { - Download.State.ERROR, - Download.State.NOT_DOWNLOADED, - -> ChapterDownloadAction.START_NOW - Download.State.QUEUE, - Download.State.DOWNLOADING, - -> ChapterDownloadAction.CANCEL - Download.State.DOWNLOADED -> ChapterDownloadAction.DELETE - } + val downloadAction: ChapterDownloadAction = + when (chapterItem.downloadState) { + Download.State.ERROR, + Download.State.NOT_DOWNLOADED, + -> ChapterDownloadAction.START_NOW + Download.State.QUEUE, + Download.State.DOWNLOADING, + -> ChapterDownloadAction.CANCEL + Download.State.DOWNLOADED -> ChapterDownloadAction.DELETE + } runChapterDownloadActions( items = listOf(chapterItem), action = downloadAction, @@ -648,11 +672,12 @@ class MangaScreenModel( updateSuccessState { state -> state.copy(hasPromptedToAddBefore = true) } - val result = snackbarHostState.showSnackbar( - message = context.stringResource(MR.strings.snack_add_to_library), - actionLabel = context.stringResource(MR.strings.action_add), - withDismissAction = true, - ) + val result = + snackbarHostState.showSnackbar( + message = context.stringResource(MR.strings.snack_add_to_library), + actionLabel = context.stringResource(MR.strings.action_add), + withDismissAction = true, + ) if (result == SnackbarResult.ActionPerformed && !isFavorited) { toggleFavorite() } @@ -686,13 +711,14 @@ class MangaScreenModel( } fun runDownloadAction(action: DownloadAction) { - val chaptersToDownload = when (action) { - DownloadAction.NEXT_1_CHAPTER -> getUnreadChaptersSorted().take(1) - DownloadAction.NEXT_5_CHAPTERS -> getUnreadChaptersSorted().take(5) - DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10) - DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25) - DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters() - } + val chaptersToDownload = + when (action) { + DownloadAction.NEXT_1_CHAPTER -> getUnreadChaptersSorted().take(1) + DownloadAction.NEXT_5_CHAPTERS -> getUnreadChaptersSorted().take(5) + DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10) + DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25) + DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters() + } if (chaptersToDownload.isNotEmpty()) { startDownload(chaptersToDownload, false) } @@ -717,7 +743,10 @@ class MangaScreenModel( * @param chapters the list of selected chapters. * @param read whether to mark chapters as read or unread. */ - fun markChaptersRead(chapters: List, read: Boolean) { + fun markChaptersRead( + chapters: List, + read: Boolean, + ) { screenModelScope.launchIO { setReadStatus.await( read = read, @@ -741,7 +770,10 @@ class MangaScreenModel( * Bookmarks the given list of chapters. * @param chapters the list of chapters to bookmark. */ - fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + fun bookmarkChapters( + chapters: List, + bookmarked: Boolean, + ) { screenModelScope.launchIO { chapters .filterNot { it.bookmark == bookmarked } @@ -794,11 +826,12 @@ class MangaScreenModel( fun setUnreadFilter(state: TriState) { val manga = successState?.manga ?: return - val flag = when (state) { - TriState.DISABLED -> Manga.SHOW_ALL - TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD - TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ - } + val flag = + when (state) { + TriState.DISABLED -> Manga.SHOW_ALL + TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD + TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ + } screenModelScope.launchNonCancellable { setMangaChapterFlags.awaitSetUnreadFilter(manga, flag) } @@ -811,11 +844,12 @@ class MangaScreenModel( fun setDownloadedFilter(state: TriState) { val manga = successState?.manga ?: return - val flag = when (state) { - TriState.DISABLED -> Manga.SHOW_ALL - TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED - TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED - } + val flag = + when (state) { + TriState.DISABLED -> Manga.SHOW_ALL + TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED + TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED + } screenModelScope.launchNonCancellable { setMangaChapterFlags.awaitSetDownloadedFilter(manga, flag) @@ -829,11 +863,12 @@ class MangaScreenModel( fun setBookmarkedFilter(state: TriState) { val manga = successState?.manga ?: return - val flag = when (state) { - TriState.DISABLED -> Manga.SHOW_ALL - TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED - TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED - } + val flag = + when (state) { + TriState.DISABLED -> Manga.SHOW_ALL + TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED + TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED + } screenModelScope.launchNonCancellable { setMangaChapterFlags.awaitSetBookmarkFilter(manga, flag) @@ -889,69 +924,71 @@ class MangaScreenModel( fromLongPress: Boolean = false, ) { updateSuccessState { successState -> - val newChapters = successState.processedChapters.toMutableList().apply { - val selectedIndex = successState.processedChapters.indexOfFirst { it.id == item.chapter.id } - if (selectedIndex < 0) return@apply + 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 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) + val firstSelection = none { it.selected } + set(selectedIndex, selectedItem.copy(selected = selected)) + selectedChapterIds.addOrRemove(item.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.. selectedPositions[1]) { - range = (selectedPositions[1] + 1).. selectedPositions[1]) { + range = (selectedPositions[1] + 1).. selectedPositions[1]) { - selectedPositions[1] = selectedIndex + } 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 + } } } } - } successState.copy(chapters = newChapters) } } fun toggleAllSelection(selected: Boolean) { updateSuccessState { successState -> - val newChapters = successState.chapters.map { - selectedChapterIds.addOrRemove(it.id, selected) - it.copy(selected = selected) - } + val newChapters = + successState.chapters.map { + selectedChapterIds.addOrRemove(it.id, selected) + it.copy(selected = selected) + } selectedPositions[0] = -1 selectedPositions[1] = -1 successState.copy(chapters = newChapters) @@ -960,10 +997,11 @@ class MangaScreenModel( fun invertSelection() { updateSuccessState { successState -> - val newChapters = successState.chapters.map { - selectedChapterIds.addOrRemove(it.id, !it.selected) - it.copy(selected = !it.selected) - } + val newChapters = + successState.chapters.map { + selectedChapterIds.addOrRemove(it.id, !it.selected) + it.copy(selected = !it.selected) + } selectedPositions[0] = -1 selectedPositions[1] = -1 successState.copy(chapters = newChapters) @@ -978,7 +1016,8 @@ class MangaScreenModel( val manga = successState?.manga ?: return screenModelScope.launchIO { - getTracks.subscribe(manga.id) + getTracks + .subscribe(manga.id) .catch { logcat(LogPriority.ERROR, it) } .map { tracks -> loggedInTrackers @@ -986,8 +1025,7 @@ class MangaScreenModel( .map { service -> TrackItem(tracks.find { it.trackerId == service.id }, service) } // Show only if the service supports this manga's source .filter { (it.tracker as? EnhancedTracker)?.accept(source!!) ?: true } - } - .distinctUntilChanged() + }.distinctUntilChanged() .collectLatest { trackItems -> updateSuccessState { it.copy(trackItems = trackItems) } } @@ -1001,12 +1039,29 @@ class MangaScreenModel( val manga: Manga, val initialSelection: ImmutableList>, ) : Dialog - data class DeleteChapters(val chapters: List) : Dialog - data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog - data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog - data class SetFetchInterval(val manga: Manga) : Dialog + + data class DeleteChapters( + val chapters: List, + ) : Dialog + + data class DuplicateManga( + val manga: Manga, + val duplicate: Manga, + ) : Dialog + + data class Migrate( + val newManga: Manga, + val oldManga: Manga, + ) : Dialog + + data class SetFetchInterval( + val manga: Manga, + ) : Dialog + data object SettingsSheet : Dialog + data object TrackSheet : Dialog + data object FullCover : Dialog } @@ -1068,11 +1123,12 @@ class MangaScreenModel( val chapterListItems by lazy { processedChapters.insertSeparators { before, after -> - val (lowerChapter, higherChapter) = if (manga.sortDescending()) { - after to before - } else { - before to after - } + val (lowerChapter, higherChapter) = + if (manga.sortDescending()) { + after to before + } else { + before to after + } if (higherChapter == null) return@insertSeparators null if (lowerChapter == null) { @@ -1082,8 +1138,7 @@ class MangaScreenModel( .coerceAtLeast(0) } else { calculateChapterGap(higherChapter.chapter, lowerChapter.chapter) - } - .takeIf { it > 0 } + }.takeIf { it > 0 } ?.let { missingCount -> ChapterList.MissingCount( id = "${lowerChapter?.id}-${higherChapter.id}", diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt index 1ba697f245..dcc86867e8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt @@ -91,7 +91,6 @@ data class TrackInfoDialogHomeScreen( private val mangaTitle: String, private val sourceId: Long, ) : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -176,7 +175,10 @@ data class TrackInfoDialogHomeScreen( /** * Opens registered tracker url in browser */ - private fun openTrackerInBrowser(context: Context, trackItem: TrackItem) { + private fun openTrackerInBrowser( + context: Context, + trackItem: TrackItem, + ) { val url = trackItem.track?.remoteUrl ?: return if (url.isNotBlank()) { context.openInBrowser(url) @@ -188,14 +190,14 @@ data class TrackInfoDialogHomeScreen( private val sourceId: Long, private val getTracks: GetTracks = Injekt.get(), ) : StateScreenModel(State()) { - init { screenModelScope.launch { refreshTrackers() } screenModelScope.launch { - getTracks.subscribe(mangaId) + getTracks + .subscribe(mangaId) .catch { logcat(LogPriority.ERROR, it) } .distinctUntilChanged() .map { it.mapToTrackItem() } @@ -220,7 +222,8 @@ data class TrackInfoDialogHomeScreen( val refreshTracks = Injekt.get() val context = Injekt.get() - refreshTracks.await(mangaId) + refreshTracks + .await(mangaId) .filter { it.first != null } .forEach { (track, e) -> logcat(LogPriority.ERROR, e) { @@ -259,16 +262,16 @@ private data class TrackStatusSelectorScreen( private val track: Track, private val serviceId: Long, ) : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { - Model( - track = track, - tracker = Injekt.get().get(serviceId)!!, - ) - } + val screenModel = + rememberScreenModel { + Model( + track = track, + tracker = Injekt.get().get(serviceId)!!, + ) + } val state by screenModel.state.collectAsState() TrackStatusSelector( selection = state.selection, @@ -286,10 +289,8 @@ private data class TrackStatusSelectorScreen( private val track: Track, private val tracker: Tracker, ) : StateScreenModel(State(track.status)) { - - fun getSelections(): Map { - return tracker.getStatusList().associateWith { tracker.getStatus(it) } - } + fun getSelections(): Map = + tracker.getStatusList().associateWith { tracker.getStatus(it) } fun setSelection(selection: Long) { mutableState.update { it.copy(selection = selection) } @@ -312,16 +313,16 @@ private data class TrackChapterSelectorScreen( private val track: Track, private val serviceId: Long, ) : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { - Model( - track = track, - tracker = Injekt.get().get(serviceId)!!, - ) - } + val screenModel = + rememberScreenModel { + Model( + track = track, + tracker = Injekt.get().get(serviceId)!!, + ) + } val state by screenModel.state.collectAsState() TrackChapterSelector( @@ -340,13 +341,13 @@ private data class TrackChapterSelectorScreen( private val track: Track, private val tracker: Tracker, ) : StateScreenModel(State(track.lastChapterRead.toInt())) { - fun getRange(): Iterable { - val endRange = if (track.totalChapters > 0) { - track.totalChapters - } else { - 10000 - } + val endRange = + if (track.totalChapters > 0) { + track.totalChapters + } else { + 10000 + } return 0..endRange.toInt() } @@ -371,16 +372,16 @@ private data class TrackScoreSelectorScreen( private val track: Track, private val serviceId: Long, ) : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { - Model( - track = track, - tracker = Injekt.get().get(serviceId)!!, - ) - } + val screenModel = + rememberScreenModel { + Model( + track = track, + tracker = Injekt.get().get(serviceId)!!, + ) + } val state by screenModel.state.collectAsState() TrackScoreSelector( @@ -399,10 +400,7 @@ private data class TrackScoreSelectorScreen( private val track: Track, private val tracker: Tracker, ) : StateScreenModel(State(tracker.displayScore(track))) { - - fun getSelections(): ImmutableList { - return tracker.getScoreList() - } + fun getSelections(): ImmutableList = tracker.getScoreList() fun setSelection(selection: String) { mutableState.update { it.copy(selection = selection) } @@ -426,86 +424,99 @@ private data class TrackDateSelectorScreen( private val serviceId: Long, private val start: Boolean, ) : Screen() { - @Transient - private val selectableDates = object : SelectableDates { - override fun isSelectableDate(utcTimeMillis: Long): Boolean { - val dateToCheck = Instant.ofEpochMilli(utcTimeMillis) - .atZone(ZoneOffset.systemDefault()) - .toLocalDate() - - if (dateToCheck > LocalDate.now()) { - // Disallow future dates - return false - } + private val selectableDates = + object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + val dateToCheck = + Instant + .ofEpochMilli(utcTimeMillis) + .atZone(ZoneOffset.systemDefault()) + .toLocalDate() + + if (dateToCheck > LocalDate.now()) { + // Disallow future dates + return false + } - return if (start && track.finishDate > 0) { - // Disallow start date to be set later than finish date - val dateFinished = Instant.ofEpochMilli(track.finishDate) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - dateToCheck <= dateFinished - } else if (!start && track.startDate > 0) { - // Disallow end date to be set earlier than start date - val dateStarted = Instant.ofEpochMilli(track.startDate) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - dateToCheck >= dateStarted - } else { - // Nothing set before - true + return if (start && track.finishDate > 0) { + // Disallow start date to be set later than finish date + val dateFinished = + Instant + .ofEpochMilli(track.finishDate) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + dateToCheck <= dateFinished + } else if (!start && track.startDate > 0) { + // Disallow end date to be set earlier than start date + val dateStarted = + Instant + .ofEpochMilli(track.startDate) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + dateToCheck >= dateStarted + } else { + // Nothing set before + true + } } - } - override fun isSelectableYear(year: Int): Boolean { - if (year > LocalDate.now().year) { - // Disallow future dates - return false - } + override fun isSelectableYear(year: Int): Boolean { + if (year > LocalDate.now().year) { + // Disallow future dates + return false + } - return if (start && track.finishDate > 0) { - // Disallow start date to be set later than finish date - val dateFinished = Instant.ofEpochMilli(track.finishDate) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - .year - year <= dateFinished - } else if (!start && track.startDate > 0) { - // Disallow end date to be set earlier than start date - val dateStarted = Instant.ofEpochMilli(track.startDate) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - .year - year >= dateStarted - } else { - // Nothing set before - true + return if (start && track.finishDate > 0) { + // Disallow start date to be set later than finish date + val dateFinished = + Instant + .ofEpochMilli(track.finishDate) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .year + year <= dateFinished + } else if (!start && track.startDate > 0) { + // Disallow end date to be set earlier than start date + val dateStarted = + Instant + .ofEpochMilli(track.startDate) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .year + year >= dateStarted + } else { + // Nothing set before + true + } } } - } @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { - Model( - track = track, - tracker = Injekt.get().get(serviceId)!!, - start = start, - ) - } + val screenModel = + rememberScreenModel { + Model( + track = track, + tracker = Injekt.get().get(serviceId)!!, + start = start, + ) + } - val canRemove = if (start) { - track.startDate > 0 - } else { - track.finishDate > 0 - } - TrackDateSelector( - title = if (start) { - stringResource(MR.strings.track_started_reading_date) + val canRemove = + if (start) { + track.startDate > 0 } else { - stringResource(MR.strings.track_finished_reading_date) - }, + track.finishDate > 0 + } + TrackDateSelector( + title = + if (start) { + stringResource(MR.strings.track_started_reading_date) + } else { + stringResource(MR.strings.track_finished_reading_date) + }, initialSelectedDateMillis = screenModel.initialSelection, selectableDates = selectableDates, onConfirm = { @@ -522,13 +533,13 @@ private data class TrackDateSelectorScreen( private val tracker: Tracker, private val start: Boolean, ) : ScreenModel { - // In UTC val initialSelection: Long get() { - val millis = (if (start) track.startDate else track.finishDate) - .takeIf { it != 0L } - ?: Instant.now().toEpochMilli() + val millis = + (if (start) track.startDate else track.finishDate) + .takeIf { it != 0L } + ?: Instant.now().toEpochMilli() return millis.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC) } @@ -556,17 +567,17 @@ private data class TrackDateRemoverScreen( private val serviceId: Long, private val start: Boolean, ) : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { - Model( - track = track, - tracker = Injekt.get().get(serviceId)!!, - start = start, - ) - } + val screenModel = + rememberScreenModel { + Model( + track = track, + tracker = Injekt.get().get(serviceId)!!, + start = start, + ) + } AlertDialogContent( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars), icon = { @@ -584,11 +595,12 @@ private data class TrackDateRemoverScreen( text = { val serviceName = screenModel.getServiceName() Text( - text = if (start) { - stringResource(MR.strings.track_remove_start_date_conf_text, serviceName) - } else { - stringResource(MR.strings.track_remove_finish_date_conf_text, serviceName) - }, + text = + if (start) { + stringResource(MR.strings.track_remove_start_date_conf_text, serviceName) + } else { + stringResource(MR.strings.track_remove_finish_date_conf_text, serviceName) + }, ) }, buttons = { @@ -604,10 +616,11 @@ private data class TrackDateRemoverScreen( screenModel.removeDate() navigator.popUntil { it is TrackInfoDialogHomeScreen } }, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), + colors = + ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ), ) { Text(text = stringResource(MR.strings.action_remove)) } @@ -621,7 +634,6 @@ private data class TrackDateRemoverScreen( private val tracker: Tracker, private val start: Boolean, ) : ScreenModel { - fun getServiceName() = tracker.name fun removeDate() { @@ -642,18 +654,18 @@ data class TrackerSearchScreen( private val currentUrl: String?, private val serviceId: Long, ) : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { - Model( - mangaId = mangaId, - currentUrl = currentUrl, - initialQuery = initialQuery, - tracker = Injekt.get().get(serviceId)!!, - ) - } + val screenModel = + rememberScreenModel { + Model( + mangaId = mangaId, + currentUrl = currentUrl, + initialQuery = initialQuery, + tracker = Injekt.get().get(serviceId)!!, + ) + } val state by screenModel.state.collectAsState() @@ -679,7 +691,6 @@ data class TrackerSearchScreen( initialQuery: String, private val tracker: Tracker, ) : StateScreenModel(State()) { - init { // Run search on first launch if (initialQuery.isNotBlank()) { @@ -692,14 +703,15 @@ data class TrackerSearchScreen( // To show loading state mutableState.update { it.copy(queryResult = null, selected = null) } - val result = withIOContext { - try { - val results = tracker.search(query) - Result.success(results) - } catch (e: Throwable) { - Result.failure(e) + val result = + withIOContext { + try { + val results = tracker.search(query) + Result.success(results) + } catch (e: Throwable) { + Result.failure(e) + } } - } mutableState.update { oldState -> oldState.copy( queryResult = result, @@ -730,17 +742,17 @@ private data class TrackerRemoveScreen( private val track: Track, private val serviceId: Long, ) : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { - Model( - mangaId = mangaId, - track = track, - tracker = Injekt.get().get(serviceId)!!, - ) - } + val screenModel = + rememberScreenModel { + Model( + mangaId = mangaId, + track = track, + tracker = Injekt.get().get(serviceId)!!, + ) + } val serviceName = screenModel.getName() var removeRemoteTrack by remember { mutableStateOf(false) } AlertDialogContent( @@ -777,10 +789,11 @@ private data class TrackerRemoveScreen( buttons = { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.small, - Alignment.End, - ), + horizontalArrangement = + Arrangement.spacedBy( + MaterialTheme.padding.small, + Alignment.End, + ), ) { TextButton(onClick = navigator::pop) { Text(text = stringResource(MR.strings.action_cancel)) @@ -791,10 +804,11 @@ private data class TrackerRemoveScreen( if (removeRemoteTrack) screenModel.deleteMangaFromService() navigator.pop() }, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), + colors = + ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ), ) { Text(text = stringResource(MR.strings.action_ok)) } @@ -809,7 +823,6 @@ private data class TrackerRemoveScreen( private val tracker: Tracker, private val deleteTrack: DeleteTrack = Injekt.get(), ) : ScreenModel { - fun getName() = tracker.name fun isDeletable() = tracker is DeletableTracker diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt index fd46e0eec4..14fee066d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt @@ -3,4 +3,7 @@ package eu.kanade.tachiyomi.ui.manga.track import eu.kanade.tachiyomi.data.track.Tracker import tachiyomi.domain.track.model.Track -data class TrackItem(val track: Track?, val tracker: Tracker) +data class TrackItem( + val track: Track?, + val tracker: Tracker, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt index 7e4ac0932c..bd37d67ce7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt @@ -39,7 +39,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object MoreTab : Tab { - override val options: TabOptions @Composable get() { @@ -83,7 +82,6 @@ private class MoreScreenModel( private val downloadManager: DownloadManager = Injekt.get(), preferences: BasePreferences = Injekt.get(), ) : ScreenModel { - var downloadedOnly by preferences.downloadedOnly().asState(screenModelScope) var incognitoMode by preferences.incognitoMode().asState(screenModelScope) @@ -99,11 +97,12 @@ private class MoreScreenModel( ) { isRunning, downloadQueue -> Pair(isRunning, downloadQueue.size) } .collectLatest { (isDownloading, downloadQueueSize) -> val pendingDownloadExists = downloadQueueSize != 0 - _state.value = when { - !pendingDownloadExists -> DownloadQueueState.Stopped - !isDownloading -> DownloadQueueState.Paused(downloadQueueSize) - else -> DownloadQueueState.Downloading(downloadQueueSize) - } + _state.value = + when { + !pendingDownloadExists -> DownloadQueueState.Stopped + !isDownloading -> DownloadQueueState.Paused(downloadQueueSize) + else -> DownloadQueueState.Downloading(downloadQueueSize) + } } } } @@ -111,6 +110,12 @@ private class MoreScreenModel( sealed interface DownloadQueueState { data object Stopped : DownloadQueueState - data class Paused(val pending: Int) : DownloadQueueState - data class Downloading(val pending: Int) : DownloadQueueState + + data class Paused( + val pending: Int, + ) : DownloadQueueState + + data class Downloading( + val pending: Int, + ) : DownloadQueueState } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt index eae88d838e..1f61700dcb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt @@ -16,14 +16,14 @@ class NewUpdateScreen( private val releaseLink: String, private val downloadLink: String, ) : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current - val changelogInfoNoChecksum = remember { - changelogInfo.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "") - } + val changelogInfoNoChecksum = + remember { + changelogInfo.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "") + } NewUpdateScreen( versionName = versionName, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt index a624c47309..69ca1ae16d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt @@ -18,7 +18,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class OnboardingScreen : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index ae471a025c..8fe5cb4cad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -98,15 +98,17 @@ import uy.kohesive.injekt.api.get import java.io.ByteArrayOutputStream class ReaderActivity : BaseActivity() { - companion object { - fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent { - return Intent(context, ReaderActivity::class.java).apply { + fun newIntent( + context: Context, + mangaId: Long?, + chapterId: Long?, + ): Intent = + Intent(context, ReaderActivity::class.java).apply { putExtra("manga", mangaId) putExtra("chapter", chapterId) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } - } } private val readerPreferences = Injekt.get() @@ -180,7 +182,9 @@ class ReaderActivity : BaseActivity() { initializeMenu() // Finish when incognito mode is disabled - preferences.incognitoMode().changes() + preferences + .incognitoMode() + .changes() .drop(1) .onEach { if (!it) finish() } .launchIn(lifecycleScope) @@ -209,7 +213,8 @@ class ReaderActivity : BaseActivity() { .onEach { event -> when (event) { ReaderViewModel.Event.ReloadViewerChapters -> { - viewModel.state.value.viewerChapters?.let(::setChapters) + viewModel.state.value.viewerChapters + ?.let(::setChapters) } ReaderViewModel.Event.PageChanged -> { displayRefreshHost.flash() @@ -227,8 +232,7 @@ class ReaderActivity : BaseActivity() { onSetAsCoverResult(event.result) } } - } - .launchIn(lifecycleScope) + }.launchIn(lifecycleScope) } /** @@ -236,7 +240,8 @@ class ReaderActivity : BaseActivity() { */ override fun onDestroy() { super.onDestroy() - viewModel.state.value.viewer?.destroy() + viewModel.state.value.viewer + ?.destroy() config = null menuToggleToast?.cancel() readingModeToast?.cancel() @@ -292,7 +297,10 @@ class ReaderActivity : BaseActivity() { } } - override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + override fun onKeyUp( + keyCode: Int, + event: KeyEvent?, + ): Boolean { if (keyCode == KeyEvent.KEYCODE_N) { loadNextChapter() return true @@ -307,7 +315,9 @@ class ReaderActivity : BaseActivity() { * Dispatches a key event. If the viewer doesn't handle it, call the default implementation. */ override fun dispatchKeyEvent(event: KeyEvent): Boolean { - val handled = viewModel.state.value.viewer?.handleKeyEvent(event) ?: false + val handled = + viewModel.state.value.viewer + ?.handleKeyEvent(event) ?: false return handled || super.dispatchKeyEvent(event) } @@ -316,7 +326,9 @@ class ReaderActivity : BaseActivity() { * implementation. */ override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { - val handled = viewModel.state.value.viewer?.handleGenericMotionEvent(event) ?: false + val handled = + viewModel.state.value.viewer + ?.handleGenericMotionEvent(event) ?: false return handled || super.dispatchGenericMotionEvent(event) } @@ -338,14 +350,15 @@ class ReaderActivity : BaseActivity() { binding.dialogRoot.setComposeContent { val state by viewModel.state.collectAsState() - val settingsScreenModel = remember { - ReaderSettingsScreenModel( - readerState = viewModel.state, - hasDisplayCutout = hasCutout, - onChangeReadingMode = viewModel::setMangaReadingMode, - onChangeOrientation = viewModel::setMangaOrientationType, - ) - } + val settingsScreenModel = + remember { + ReaderSettingsScreenModel( + readerState = viewModel.state, + hasDisplayCutout = hasCutout, + onChangeReadingMode = viewModel::setMangaReadingMode, + onChangeOrientation = viewModel::setMangaOrientationType, + ) + } if (!ifSourcesLoaded()) { return@setComposeContent @@ -358,9 +371,10 @@ class ReaderActivity : BaseActivity() { val colorOverlayEnabled by readerPreferences.colorFilter().collectAsState() val colorOverlay by readerPreferences.colorFilterValue().collectAsState() val colorOverlayMode by readerPreferences.colorFilterMode().collectAsState() - val colorOverlayBlendMode = remember(colorOverlayMode) { - ReaderPreferences.ColorFilterMode.getOrNull(colorOverlayMode)?.second - } + val colorOverlayBlendMode = + remember(colorOverlayMode) { + ReaderPreferences.ColorFilterMode.getOrNull(colorOverlayMode)?.second + } val cropBorderPaged by readerPreferences.cropBorders().collectAsState() val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState() @@ -376,7 +390,6 @@ class ReaderActivity : BaseActivity() { ReaderAppBars( visible = state.menuVisible, fullscreen = isFullscreen, - mangaTitle = state.manga?.title, chapterTitle = state.currentChapter?.chapter?.name, navigateUp = onBackPressedDispatcher::onBackPressed, @@ -385,7 +398,6 @@ class ReaderActivity : BaseActivity() { onToggleBookmarked = viewModel::toggleChapterBookmark, onOpenInWebView = ::openChapterInWebView.takeIf { isHttpSource }, onShare = ::shareChapter.takeIf { isHttpSource }, - viewer = state.viewer, onNextChapter = ::loadNextChapter, enabledNext = state.viewerChapters?.nextChapter != null, @@ -397,14 +409,15 @@ class ReaderActivity : BaseActivity() { isScrollingThroughPages = true moveToPageIndex(it) }, - - readingMode = ReadingMode.fromPreference( - viewModel.getMangaReadingMode(resolveDefault = false), - ), + readingMode = + ReadingMode.fromPreference( + viewModel.getMangaReadingMode(resolveDefault = false), + ), onClickReadingMode = viewModel::openReadingModeSelectDialog, - orientation = ReaderOrientation.fromPreference( - viewModel.getMangaOrientation(resolveDefault = false), - ), + orientation = + ReaderOrientation.fromPreference( + viewModel.getMangaOrientation(resolveDefault = false), + ), onClickOrientation = viewModel::openOrientationModeSelectDialog, cropEnabled = cropEnabled, onClickCropBorder = { @@ -480,10 +493,11 @@ class ReaderActivity : BaseActivity() { } } - val toolbarColor = ColorUtils.setAlphaComponent( - SurfaceColors.SURFACE_2.getColor(this), - if (isNightMode()) 230 else 242, // 90% dark 95% light - ) + val toolbarColor = + ColorUtils.setAlphaComponent( + SurfaceColors.SURFACE_2.getColor(this), + if (isNightMode()) 230 else 242, // 90% dark 95% light + ) window.statusBarColor = toolbarColor if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { window.navigationBarColor = toolbarColor @@ -590,7 +604,8 @@ class ReaderActivity : BaseActivity() { @SuppressLint("RestrictedApi") private fun setChapters(viewerChapters: ViewerChapters) { binding.readerContainer.removeView(loadingIndicator) - viewModel.state.value.viewer?.setChapters(viewerChapters) + viewModel.state.value.viewer + ?.setChapters(viewerChapters) lifecycleScope.launchIO { viewModel.getChapterUrl()?.let { url -> @@ -710,14 +725,18 @@ class ReaderActivity : BaseActivity() { * Called from the presenter when a page is ready to be shared. It shows Android's default * sharing tool. */ - private fun onShareImageResult(uri: Uri, page: ReaderPage) { + private fun onShareImageResult( + uri: Uri, + page: ReaderPage, + ) { val manga = viewModel.manga ?: return val chapter = page.chapter.chapter - val intent = uri.toShareIntent( - context = applicationContext, - message = stringResource(MR.strings.share_page_info, manga.title, chapter.name, page.number), - ) + val intent = + uri.toShareIntent( + context = applicationContext, + message = stringResource(MR.strings.share_page_info, manga.title, chapter.name, page.number), + ) startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share))) } @@ -777,30 +796,48 @@ class ReaderActivity : BaseActivity() { * Class that handles the user preferences of the reader. */ private inner class ReaderConfig { - - private fun getCombinedPaint(grayscale: Boolean, invertedColors: Boolean): Paint { - return Paint().apply { - colorFilter = ColorMatrixColorFilter( - ColorMatrix().apply { - if (grayscale) { - setSaturation(0f) - } - if (invertedColors) { - postConcat( - ColorMatrix( - floatArrayOf( - -1f, 0f, 0f, 0f, 255f, - 0f, -1f, 0f, 0f, 255f, - 0f, 0f, -1f, 0f, 255f, - 0f, 0f, 0f, 1f, 0f, + private fun getCombinedPaint( + grayscale: Boolean, + invertedColors: Boolean, + ): Paint = + Paint().apply { + colorFilter = + ColorMatrixColorFilter( + ColorMatrix().apply { + if (grayscale) { + setSaturation(0f) + } + if (invertedColors) { + postConcat( + ColorMatrix( + floatArrayOf( + -1f, + 0f, + 0f, + 0f, + 255f, + 0f, + -1f, + 0f, + 0f, + 255f, + 0f, + 0f, + -1f, + 0f, + 255f, + 0f, + 0f, + 0f, + 1f, + 0f, + ), ), - ), - ) - } - }, - ) + ) + } + }, + ) } - } private val grayBackgroundColor = Color.rgb(0x20, 0x21, 0x25) @@ -808,7 +845,9 @@ class ReaderActivity : BaseActivity() { * Initializes the reader subscriptions. */ init { - readerPreferences.readerTheme().changes() + readerPreferences + .readerTheme() + .changes() .onEach { theme -> binding.readerContainer.setBackgroundColor( when (theme) { @@ -818,22 +857,29 @@ class ReaderActivity : BaseActivity() { else -> Color.BLACK }, ) - } - .launchIn(lifecycleScope) + }.launchIn(lifecycleScope) - preferences.displayProfile().changes() + preferences + .displayProfile() + .changes() .onEach { setDisplayProfile(it) } .launchIn(lifecycleScope) - readerPreferences.cutoutShort().changes() + readerPreferences + .cutoutShort() + .changes() .onEach(::setCutoutShort) .launchIn(lifecycleScope) - readerPreferences.keepScreenOn().changes() + readerPreferences + .keepScreenOn() + .changes() .onEach(::setKeepScreenOn) .launchIn(lifecycleScope) - readerPreferences.customBrightness().changes() + readerPreferences + .customBrightness() + .changes() .onEach(::setCustomBrightness) .launchIn(lifecycleScope) @@ -841,24 +887,24 @@ class ReaderActivity : BaseActivity() { .onEach { setLayerPaint(readerPreferences.grayscale().get(), readerPreferences.invertedColors().get()) } .launchIn(lifecycleScope) - readerPreferences.fullscreen().changes() + readerPreferences + .fullscreen() + .changes() .onEach { WindowCompat.setDecorFitsSystemWindows(window, !it) updateViewerInset(it) - } - .launchIn(lifecycleScope) + }.launchIn(lifecycleScope) } /** * Picks background color for [ReaderActivity] based on light/dark theme preference */ - private fun automaticBackgroundColor(): Int { - return if (baseContext.isNightMode()) { + private fun automaticBackgroundColor(): Int = + if (baseContext.isNightMode()) { grayBackgroundColor } else { Color.WHITE } - } /** * Sets the display profile to [path]. @@ -882,10 +928,11 @@ class ReaderActivity : BaseActivity() { private fun setCutoutShort(enabled: Boolean) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return - window.attributes.layoutInDisplayCutoutMode = when (enabled) { - true -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - false -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - } + window.attributes.layoutInDisplayCutoutMode = + when (enabled) { + true -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + false -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + } // Trigger relayout setMenuVisibility(viewModel.state.value.menuVisible) @@ -907,7 +954,9 @@ class ReaderActivity : BaseActivity() { */ private fun setCustomBrightness(enabled: Boolean) { if (enabled) { - readerPreferences.customBrightnessValue().changes() + readerPreferences + .customBrightnessValue() + .changes() .sample(100) .onEach(::setCustomBrightnessValue) .launchIn(lifecycleScope) @@ -924,20 +973,25 @@ class ReaderActivity : BaseActivity() { */ private fun setCustomBrightnessValue(value: Int) { // Calculate and set reader brightness. - val readerBrightness = when { - value > 0 -> { - value / 100f - } - value < 0 -> { - 0.01f + val readerBrightness = + when { + value > 0 -> { + value / 100f + } + value < 0 -> { + 0.01f + } + else -> WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE } - else -> WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE - } window.attributes = window.attributes.apply { screenBrightness = readerBrightness } viewModel.setBrightnessOverlayValue(value) } - private fun setLayerPaint(grayscale: Boolean, invertedColors: Boolean) { + + private fun setLayerPaint( + grayscale: Boolean, + invertedColors: Boolean, + ) { val paint = if (grayscale || invertedColors) getCombinedPaint(grayscale, invertedColors) else null binding.viewerContainer.setLayerType(LAYER_TYPE_HARDWARE, paint) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt index 08f5f83499..ce5ee55a69 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt @@ -16,13 +16,18 @@ import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation import tachiyomi.core.common.i18n.stringResource import kotlin.math.abs -class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) { - +class ReaderNavigationOverlayView( + context: Context, + attributeSet: AttributeSet, +) : View(context, attributeSet) { private var viewPropertyAnimator: ViewPropertyAnimator? = null private var navigation: ViewerNavigation? = null - fun setNavigation(navigation: ViewerNavigation, showOnStart: Boolean) { + fun setNavigation( + navigation: ViewerNavigation, + showOnStart: Boolean, + ) { val firstLaunch = this.navigation == null this.navigation = navigation invalidate() @@ -31,33 +36,35 @@ class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet) return } - viewPropertyAnimator = animate() - .alpha(1f) - .setDuration(FADE_DURATION) - .withStartAction { - isVisible = true - } - .withEndAction { - viewPropertyAnimator = null - } + viewPropertyAnimator = + animate() + .alpha(1f) + .setDuration(FADE_DURATION) + .withStartAction { + isVisible = true + }.withEndAction { + viewPropertyAnimator = null + } viewPropertyAnimator?.start() } private val regionPaint = Paint() - private val textPaint = Paint().apply { - textAlign = Paint.Align.CENTER - color = Color.WHITE - textSize = 64f - } + private val textPaint = + Paint().apply { + textAlign = Paint.Align.CENTER + color = Color.WHITE + textSize = 64f + } - private val textBorderPaint = Paint().apply { - textAlign = Paint.Align.CENTER - color = Color.BLACK - textSize = 64f - style = Paint.Style.STROKE - strokeWidth = 8f - } + private val textBorderPaint = + Paint().apply { + textAlign = Paint.Align.CENTER + color = Color.BLACK + textSize = 64f + style = Paint.Style.STROKE + strokeWidth = 8f + } override fun onDraw(canvas: Canvas) { if (navigation == null) return @@ -90,13 +97,14 @@ class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet) super.performClick() if (viewPropertyAnimator == null && isVisible) { - viewPropertyAnimator = animate() - .alpha(0f) - .setDuration(FADE_DURATION) - .withEndAction { - isVisible = false - viewPropertyAnimator = null - } + viewPropertyAnimator = + animate() + .alpha(0f) + .setDuration(FADE_DURATION) + .withEndAction { + isVisible = false + viewPropertyAnimator = null + } viewPropertyAnimator?.start() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 432eff749e..cf2bfa6aab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -81,873 +81,923 @@ import java.util.Date /** * Presenter used by the activity to perform background operations. */ -class ReaderViewModel @JvmOverloads constructor( - private val savedState: SavedStateHandle, - private val sourceManager: SourceManager = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get(), - private val downloadProvider: DownloadProvider = Injekt.get(), - private val imageSaver: ImageSaver = Injekt.get(), - preferences: BasePreferences = Injekt.get(), - val readerPreferences: ReaderPreferences = Injekt.get(), - private val basePreferences: BasePreferences = Injekt.get(), - private val downloadPreferences: DownloadPreferences = Injekt.get(), - private val trackPreferences: TrackPreferences = Injekt.get(), - private val trackChapter: TrackChapter = Injekt.get(), - private val getManga: GetManga = Injekt.get(), - private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), - private val getNextChapters: GetNextChapters = Injekt.get(), - private val upsertHistory: UpsertHistory = Injekt.get(), - private val updateChapter: UpdateChapter = Injekt.get(), - private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(), -) : ViewModel() { - - private val mutableState = MutableStateFlow(State()) - val state = mutableState.asStateFlow() - - private val eventChannel = Channel() - val eventFlow = eventChannel.receiveAsFlow() - - /** - * The manga loaded in the reader. It can be null when instantiated for a short time. - */ - val manga: Manga? - get() = state.value.manga - - /** - * The chapter id of the currently loaded chapter. Used to restore from process kill. - */ - private var chapterId = savedState.get("chapter_id") ?: -1L - set(value) { - savedState["chapter_id"] = value - field = value - } - - /** - * The visible page index of the currently loaded chapter. Used to restore from process kill. - */ - private var chapterPageIndex = savedState.get("page_index") ?: -1 - set(value) { - savedState["page_index"] = value - field = value - } - - /** - * The chapter loader for the loaded manga. It'll be null until [manga] is set. - */ - private var loader: ChapterLoader? = null - - /** - * The time the chapter was started reading - */ - private var chapterReadStartTime: Long? = null - - private var chapterToDownload: Download? = null - - /** - * Chapter list for the active manga. It's retrieved lazily and should be accessed for the first - * time in a background thread to avoid blocking the UI. - */ - private val chapterList by lazy { - val manga = manga!! - val chapters = runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) } - - val selectedChapter = chapters.find { it.id == chapterId } - ?: error("Requested chapter of id $chapterId not found in chapter list") - - val chaptersForReader = when { - (readerPreferences.skipRead().get() || readerPreferences.skipFiltered().get()) -> { - val filteredChapters = chapters.filterNot { - when { - readerPreferences.skipRead().get() && it.read -> true - readerPreferences.skipFiltered().get() -> { - (manga.unreadFilterRaw == Manga.CHAPTER_SHOW_READ && !it.read) || - (manga.unreadFilterRaw == Manga.CHAPTER_SHOW_UNREAD && it.read) || - ( - manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_DOWNLOADED && - !downloadManager.isChapterDownloaded( - it.name, it.scanlator, manga.title, manga.source, - ) - ) || - ( - manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_NOT_DOWNLOADED && - downloadManager.isChapterDownloaded( - it.name, it.scanlator, manga.title, manga.source, - ) - ) || - (manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) || - (manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark) +class ReaderViewModel + @JvmOverloads + constructor( + private val savedState: SavedStateHandle, + private val sourceManager: SourceManager = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get(), + private val downloadProvider: DownloadProvider = Injekt.get(), + private val imageSaver: ImageSaver = Injekt.get(), + preferences: BasePreferences = Injekt.get(), + val readerPreferences: ReaderPreferences = Injekt.get(), + private val basePreferences: BasePreferences = Injekt.get(), + private val downloadPreferences: DownloadPreferences = Injekt.get(), + private val trackPreferences: TrackPreferences = Injekt.get(), + private val trackChapter: TrackChapter = Injekt.get(), + private val getManga: GetManga = Injekt.get(), + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), + private val getNextChapters: GetNextChapters = Injekt.get(), + private val upsertHistory: UpsertHistory = Injekt.get(), + private val updateChapter: UpdateChapter = Injekt.get(), + private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(), + ) : ViewModel() { + private val mutableState = MutableStateFlow(State()) + val state = mutableState.asStateFlow() + + private val eventChannel = Channel() + val eventFlow = eventChannel.receiveAsFlow() + + /** + * The manga loaded in the reader. It can be null when instantiated for a short time. + */ + val manga: Manga? + get() = state.value.manga + + /** + * The chapter id of the currently loaded chapter. Used to restore from process kill. + */ + private var chapterId = savedState.get("chapter_id") ?: -1L + set(value) { + savedState["chapter_id"] = value + field = value + } + + /** + * The visible page index of the currently loaded chapter. Used to restore from process kill. + */ + private var chapterPageIndex = savedState.get("page_index") ?: -1 + set(value) { + savedState["page_index"] = value + field = value + } + + /** + * The chapter loader for the loaded manga. It'll be null until [manga] is set. + */ + private var loader: ChapterLoader? = null + + /** + * The time the chapter was started reading + */ + private var chapterReadStartTime: Long? = null + + private var chapterToDownload: Download? = null + + /** + * Chapter list for the active manga. It's retrieved lazily and should be accessed for the first + * time in a background thread to avoid blocking the UI. + */ + private val chapterList by lazy { + val manga = manga!! + val chapters = runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) } + + val selectedChapter = + chapters.find { it.id == chapterId } + ?: error("Requested chapter of id $chapterId not found in chapter list") + + val chaptersForReader = + when { + (readerPreferences.skipRead().get() || readerPreferences.skipFiltered().get()) -> { + val filteredChapters = + chapters.filterNot { + when { + readerPreferences.skipRead().get() && it.read -> true + readerPreferences.skipFiltered().get() -> { + (manga.unreadFilterRaw == Manga.CHAPTER_SHOW_READ && !it.read) || + (manga.unreadFilterRaw == Manga.CHAPTER_SHOW_UNREAD && it.read) || + ( + manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_DOWNLOADED && + !downloadManager.isChapterDownloaded( + it.name, + it.scanlator, + manga.title, + manga.source, + ) + ) || + ( + manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_NOT_DOWNLOADED && + downloadManager.isChapterDownloaded( + it.name, + it.scanlator, + manga.title, + manga.source, + ) + ) || + (manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) || + ( + manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_NOT_BOOKMARKED && + it.bookmark + ) + } + else -> false + } + } + + if (filteredChapters.any { it.id == chapterId }) { + filteredChapters + } else { + filteredChapters + listOf(selectedChapter) } - else -> false } + else -> chapters } - if (filteredChapters.any { it.id == chapterId }) { - filteredChapters - } else { - filteredChapters + listOf(selectedChapter) - } - } - else -> chapters + chaptersForReader + .sortedWith(getChapterSort(manga, sortDescending = false)) + .run { + if (readerPreferences.skipDupe().get()) { + removeDuplicates(selectedChapter) + } else { + this + } + }.run { + if (basePreferences.downloadedOnly().get()) { + filterDownloaded(manga) + } else { + this + } + }.map { it.toDbChapter() } + .map(::ReaderChapter) + } + + private val incognitoMode = preferences.incognitoMode().get() + private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading().get() + + init { + // To save state + state + .map { it.viewerChapters?.currChapter } + .distinctUntilChanged() + .filterNotNull() + .onEach { currentChapter -> + if (chapterPageIndex >= 0) { + // Restore from SavedState + currentChapter.requestedPage = chapterPageIndex + } else if (!currentChapter.chapter.read) { + currentChapter.requestedPage = currentChapter.chapter.last_page_read + } + chapterId = currentChapter.chapter.id!! + }.launchIn(viewModelScope) } - chaptersForReader - .sortedWith(getChapterSort(manga, sortDescending = false)) - .run { - if (readerPreferences.skipDupe().get()) { - removeDuplicates(selectedChapter) - } else { - this + override fun onCleared() { + val currentChapters = state.value.viewerChapters + if (currentChapters != null) { + currentChapters.unref() + chapterToDownload?.let { + downloadManager.addDownloadsToStartOfQueue(listOf(it)) } } - .run { - if (basePreferences.downloadedOnly().get()) { - filterDownloaded(manga) - } else { - this + } + + /** + * Called when the user pressed the back button and is going to leave the reader. Used to + * trigger deletion of the downloaded chapters. + */ + fun onActivityFinish() { + deletePendingChapters() + } + + /** + * Whether this presenter is initialized yet. + */ + fun needsInit(): Boolean = manga == null + + /** + * Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will + * fetch the manga from the database and initialize the initial chapter. + */ + suspend fun init( + mangaId: Long, + initialChapterId: Long, + ): Result { + if (!needsInit()) return Result.success(true) + return withIOContext { + try { + val manga = getManga.await(mangaId) + if (manga != null) { + sourceManager.isInitialized.first { it } + mutableState.update { it.copy(manga = manga) } + if (chapterId == -1L) chapterId = initialChapterId + + val context = Injekt.get() + val source = sourceManager.getOrStub(manga.source) + loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) + + loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id }) + Result.success(true) + } else { + // Unlikely but okay + Result.success(false) + } + } catch (e: Throwable) { + if (e is CancellationException) { + throw e + } + Result.failure(e) } } - .map { it.toDbChapter() } - .map(::ReaderChapter) - } + } + + /** + * Loads the given [chapter] with this [loader] and updates the currently active chapters. + * Callers must handle errors. + */ + private suspend fun loadChapter( + loader: ChapterLoader, + chapter: ReaderChapter, + ): ViewerChapters { + loader.loadChapter(chapter) - private val incognitoMode = preferences.incognitoMode().get() - private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading().get() - - init { - // To save state - state.map { it.viewerChapters?.currChapter } - .distinctUntilChanged() - .filterNotNull() - .onEach { currentChapter -> - if (chapterPageIndex >= 0) { - // Restore from SavedState - currentChapter.requestedPage = chapterPageIndex - } else if (!currentChapter.chapter.read) { - currentChapter.requestedPage = currentChapter.chapter.last_page_read + val chapterPos = chapterList.indexOf(chapter) + val newChapters = + ViewerChapters( + chapter, + chapterList.getOrNull(chapterPos - 1), + chapterList.getOrNull(chapterPos + 1), + ) + + withUIContext { + mutableState.update { + // Add new references first to avoid unnecessary recycling + newChapters.ref() + it.viewerChapters?.unref() + + chapterToDownload = cancelQueuedDownloads(newChapters.currChapter) + it.copy( + viewerChapters = newChapters, + bookmarked = newChapters.currChapter.chapter.bookmark, + ) } - chapterId = currentChapter.chapter.id!! } - .launchIn(viewModelScope) - } + return newChapters + } + + /** + * Called when the user changed to the given [chapter] when changing pages from the viewer. + * It's used only to set this chapter as active. + */ + private fun loadNewChapter(chapter: ReaderChapter) { + val loader = loader ?: return + + viewModelScope.launchIO { + logcat { "Loading ${chapter.chapter.url}" } - override fun onCleared() { - val currentChapters = state.value.viewerChapters - if (currentChapters != null) { - currentChapters.unref() - chapterToDownload?.let { - downloadManager.addDownloadsToStartOfQueue(listOf(it)) + flushReadTimer() + restartReadTimer() + + try { + loadChapter(loader, chapter) + } catch (e: Throwable) { + if (e is CancellationException) { + throw e + } + logcat(LogPriority.ERROR, e) + } } } - } - /** - * Called when the user pressed the back button and is going to leave the reader. Used to - * trigger deletion of the downloaded chapters. - */ - fun onActivityFinish() { - deletePendingChapters() - } + /** + * Called when the user is going to load the prev/next chapter through the toolbar buttons. + */ + private suspend fun loadAdjacent(chapter: ReaderChapter) { + val loader = loader ?: return - /** - * Whether this presenter is initialized yet. - */ - fun needsInit(): Boolean { - return manga == null - } + logcat { "Loading adjacent ${chapter.chapter.url}" } - /** - * Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will - * fetch the manga from the database and initialize the initial chapter. - */ - suspend fun init(mangaId: Long, initialChapterId: Long): Result { - if (!needsInit()) return Result.success(true) - return withIOContext { + mutableState.update { it.copy(isLoadingAdjacentChapter = true) } try { - val manga = getManga.await(mangaId) - if (manga != null) { - sourceManager.isInitialized.first { it } - mutableState.update { it.copy(manga = manga) } - if (chapterId == -1L) chapterId = initialChapterId - - val context = Injekt.get() - val source = sourceManager.getOrStub(manga.source) - loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) - - loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id }) - Result.success(true) - } else { - // Unlikely but okay - Result.success(false) + withIOContext { + loadChapter(loader, chapter) } } catch (e: Throwable) { if (e is CancellationException) { throw e } - Result.failure(e) + logcat(LogPriority.ERROR, e) + } finally { + mutableState.update { it.copy(isLoadingAdjacentChapter = false) } } } - } - - /** - * Loads the given [chapter] with this [loader] and updates the currently active chapters. - * Callers must handle errors. - */ - private suspend fun loadChapter( - loader: ChapterLoader, - chapter: ReaderChapter, - ): ViewerChapters { - loader.loadChapter(chapter) - - val chapterPos = chapterList.indexOf(chapter) - val newChapters = ViewerChapters( - chapter, - chapterList.getOrNull(chapterPos - 1), - chapterList.getOrNull(chapterPos + 1), - ) - - withUIContext { - mutableState.update { - // Add new references first to avoid unnecessary recycling - newChapters.ref() - it.viewerChapters?.unref() - chapterToDownload = cancelQueuedDownloads(newChapters.currChapter) - it.copy( - viewerChapters = newChapters, - bookmarked = newChapters.currChapter.chapter.bookmark, - ) + /** + * Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so + * that the user doesn't have to wait too long to continue reading. + */ + suspend fun preload(chapter: ReaderChapter) { + if (chapter.state is ReaderChapter.State.Loaded || chapter.state == ReaderChapter.State.Loading) { + return } - } - return newChapters - } - - /** - * Called when the user changed to the given [chapter] when changing pages from the viewer. - * It's used only to set this chapter as active. - */ - private fun loadNewChapter(chapter: ReaderChapter) { - val loader = loader ?: return - viewModelScope.launchIO { - logcat { "Loading ${chapter.chapter.url}" } + if (chapter.pageLoader?.isLocal == false) { + val manga = manga ?: return + val dbChapter = chapter.chapter + val isDownloaded = + downloadManager.isChapterDownloaded( + dbChapter.name, + dbChapter.scanlator, + manga.title, + manga.source, + skipCache = true, + ) + if (isDownloaded) { + chapter.state = ReaderChapter.State.Wait + } + } - flushReadTimer() - restartReadTimer() + if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) { + return + } + val loader = loader ?: return try { - loadChapter(loader, chapter) + logcat { "Preloading ${chapter.chapter.url}" } + loader.loadChapter(chapter) } catch (e: Throwable) { if (e is CancellationException) { throw e } - logcat(LogPriority.ERROR, e) + return } + eventChannel.trySend(Event.ReloadViewerChapters) } - } - /** - * Called when the user is going to load the prev/next chapter through the toolbar buttons. - */ - private suspend fun loadAdjacent(chapter: ReaderChapter) { - val loader = loader ?: return + fun onViewerLoaded(viewer: Viewer?) { + mutableState.update { + it.copy(viewer = viewer) + } + } + + /** + * Called every time a page changes on the reader. Used to mark the flag of chapters being + * read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this + * [page]'s chapter is different from the currently active. + */ + fun onPageSelected(page: ReaderPage) { + // InsertPage doesn't change page progress + if (page is InsertPage) { + return + } + + val selectedChapter = page.chapter + val pages = selectedChapter.pages ?: return - logcat { "Loading adjacent ${chapter.chapter.url}" } + // Save last page read and mark as read if needed + viewModelScope.launchNonCancellable { + updateChapterProgress(selectedChapter, page) + } - mutableState.update { it.copy(isLoadingAdjacentChapter = true) } - try { - withIOContext { - loadChapter(loader, chapter) + if (selectedChapter != getCurrentChapter()) { + logcat { "Setting ${selectedChapter.chapter.url} as active" } + loadNewChapter(selectedChapter) } - } catch (e: Throwable) { - if (e is CancellationException) { - throw e + + val inDownloadRange = page.number.toDouble() / pages.size > 0.25 + if (inDownloadRange) { + downloadNextChapters() } - logcat(LogPriority.ERROR, e) - } finally { - mutableState.update { it.copy(isLoadingAdjacentChapter = false) } - } - } - /** - * Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so - * that the user doesn't have to wait too long to continue reading. - */ - suspend fun preload(chapter: ReaderChapter) { - if (chapter.state is ReaderChapter.State.Loaded || chapter.state == ReaderChapter.State.Loading) { - return + eventChannel.trySend(Event.PageChanged) } - if (chapter.pageLoader?.isLocal == false) { + private fun downloadNextChapters() { + if (downloadAheadAmount == 0) return val manga = manga ?: return - val dbChapter = chapter.chapter - val isDownloaded = downloadManager.isChapterDownloaded( - dbChapter.name, - dbChapter.scanlator, - manga.title, - manga.source, - skipCache = true, - ) - if (isDownloaded) { - chapter.state = ReaderChapter.State.Wait - } - } - if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) { - return + // Only download ahead if current + next chapter is already downloaded too to avoid jank + if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return + val nextChapter = + state.value.viewerChapters + ?.nextChapter + ?.chapter ?: return + + viewModelScope.launchIO { + val isNextChapterDownloaded = + downloadManager.isChapterDownloaded( + nextChapter.name, + nextChapter.scanlator, + manga.title, + manga.source, + ) + if (!isNextChapterDownloaded) return@launchIO + + val chaptersToDownload = + getNextChapters + .await(manga.id, nextChapter.id!!) + .run { + if (readerPreferences.skipDupe().get()) { + removeDuplicates(nextChapter.toDomainChapter()!!) + } else { + this + } + }.take(downloadAheadAmount) + + downloadManager.downloadChapters( + manga, + chaptersToDownload, + ) + } } - val loader = loader ?: return - try { - logcat { "Preloading ${chapter.chapter.url}" } - loader.loadChapter(chapter) - } catch (e: Throwable) { - if (e is CancellationException) { - throw e + /** + * Removes [currentChapter] from download queue + * if setting is enabled and [currentChapter] is queued for download + */ + private fun cancelQueuedDownloads(currentChapter: ReaderChapter): Download? = + downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!.toLong())?.also { + downloadManager.cancelQueuedDownloads(listOf(it)) } - return - } - eventChannel.trySend(Event.ReloadViewerChapters) - } - fun onViewerLoaded(viewer: Viewer?) { - mutableState.update { - it.copy(viewer = viewer) - } - } + /** + * Determines if deleting option is enabled and nth to last chapter actually exists. + * If both conditions are satisfied enqueues chapter for delete + * @param currentChapter current chapter, which is going to be marked as read. + */ + private fun deleteChapterIfNeeded(currentChapter: ReaderChapter) { + val removeAfterReadSlots = downloadPreferences.removeAfterReadSlots().get() + if (removeAfterReadSlots == -1) return - /** - * Called every time a page changes on the reader. Used to mark the flag of chapters being - * read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this - * [page]'s chapter is different from the currently active. - */ - fun onPageSelected(page: ReaderPage) { - // InsertPage doesn't change page progress - if (page is InsertPage) { - return - } + // Determine which chapter should be deleted and enqueue + val currentChapterPosition = chapterList.indexOf(currentChapter) + val chapterToDelete = chapterList.getOrNull(currentChapterPosition - removeAfterReadSlots) - val selectedChapter = page.chapter - val pages = selectedChapter.pages ?: return + // If chapter is completely read, no need to download it + chapterToDownload = null - // Save last page read and mark as read if needed - viewModelScope.launchNonCancellable { - updateChapterProgress(selectedChapter, page) + if (chapterToDelete != null) { + enqueueDeleteReadChapters(chapterToDelete) + } } - if (selectedChapter != getCurrentChapter()) { - logcat { "Setting ${selectedChapter.chapter.url} as active" } - loadNewChapter(selectedChapter) - } + /** + * Saves the chapter progress (last read page and whether it's read) + * if incognito mode isn't on. + */ + private suspend fun updateChapterProgress( + readerChapter: ReaderChapter, + page: Page, + ) { + val pageIndex = page.index - val inDownloadRange = page.number.toDouble() / pages.size > 0.25 - if (inDownloadRange) { - downloadNextChapters() - } + mutableState.update { + it.copy(currentPage = pageIndex + 1) + } + readerChapter.requestedPage = pageIndex + chapterPageIndex = pageIndex - eventChannel.trySend(Event.PageChanged) - } + if (!incognitoMode && page.status != Page.State.ERROR) { + readerChapter.chapter.last_page_read = pageIndex - private fun downloadNextChapters() { - if (downloadAheadAmount == 0) return - val manga = manga ?: return - - // Only download ahead if current + next chapter is already downloaded too to avoid jank - if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return - val nextChapter = state.value.viewerChapters?.nextChapter?.chapter ?: return - - viewModelScope.launchIO { - val isNextChapterDownloaded = downloadManager.isChapterDownloaded( - nextChapter.name, - nextChapter.scanlator, - manga.title, - manga.source, - ) - if (!isNextChapterDownloaded) return@launchIO - - val chaptersToDownload = getNextChapters.await(manga.id, nextChapter.id!!).run { - if (readerPreferences.skipDupe().get()) { - removeDuplicates(nextChapter.toDomainChapter()!!) - } else { - this + if (readerChapter.pages?.lastIndex == pageIndex) { + readerChapter.chapter.read = true + updateTrackChapterRead(readerChapter) + deleteChapterIfNeeded(readerChapter) } - }.take(downloadAheadAmount) - downloadManager.downloadChapters( - manga, - chaptersToDownload, - ) + updateChapter.await( + ChapterUpdate( + id = readerChapter.chapter.id!!, + read = readerChapter.chapter.read, + lastPageRead = readerChapter.chapter.last_page_read.toLong(), + ), + ) + } } - } - /** - * Removes [currentChapter] from download queue - * if setting is enabled and [currentChapter] is queued for download - */ - private fun cancelQueuedDownloads(currentChapter: ReaderChapter): Download? { - return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!.toLong())?.also { - downloadManager.cancelQueuedDownloads(listOf(it)) + fun restartReadTimer() { + chapterReadStartTime = Instant.now().toEpochMilli() } - } - /** - * Determines if deleting option is enabled and nth to last chapter actually exists. - * If both conditions are satisfied enqueues chapter for delete - * @param currentChapter current chapter, which is going to be marked as read. - */ - private fun deleteChapterIfNeeded(currentChapter: ReaderChapter) { - val removeAfterReadSlots = downloadPreferences.removeAfterReadSlots().get() - if (removeAfterReadSlots == -1) return + fun flushReadTimer() { + getCurrentChapter()?.let { + viewModelScope.launchNonCancellable { + updateHistory(it) + } + } + } - // Determine which chapter should be deleted and enqueue - val currentChapterPosition = chapterList.indexOf(currentChapter) - val chapterToDelete = chapterList.getOrNull(currentChapterPosition - removeAfterReadSlots) + /** + * Saves the chapter last read history if incognito mode isn't on. + */ + private suspend fun updateHistory(readerChapter: ReaderChapter) { + if (incognitoMode) return - // If chapter is completely read, no need to download it - chapterToDownload = null + val chapterId = readerChapter.chapter.id!! + val endTime = Date() + val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0 - if (chapterToDelete != null) { - enqueueDeleteReadChapters(chapterToDelete) + upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration)) + chapterReadStartTime = null } - } - /** - * Saves the chapter progress (last read page and whether it's read) - * if incognito mode isn't on. - */ - private suspend fun updateChapterProgress(readerChapter: ReaderChapter, page: Page) { - val pageIndex = page.index + /** + * Called from the activity to load and set the next chapter as active. + */ + suspend fun loadNextChapter() { + val nextChapter = state.value.viewerChapters?.nextChapter ?: return + loadAdjacent(nextChapter) + } - mutableState.update { - it.copy(currentPage = pageIndex + 1) + /** + * Called from the activity to load and set the previous chapter as active. + */ + suspend fun loadPreviousChapter() { + val prevChapter = state.value.viewerChapters?.prevChapter ?: return + loadAdjacent(prevChapter) } - readerChapter.requestedPage = pageIndex - chapterPageIndex = pageIndex - if (!incognitoMode && page.status != Page.State.ERROR) { - readerChapter.chapter.last_page_read = pageIndex + /** + * Returns the currently active chapter. + */ + private fun getCurrentChapter(): ReaderChapter? = state.value.currentChapter - if (readerChapter.pages?.lastIndex == pageIndex) { - readerChapter.chapter.read = true - updateTrackChapterRead(readerChapter) - deleteChapterIfNeeded(readerChapter) - } + fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource - updateChapter.await( - ChapterUpdate( - id = readerChapter.chapter.id!!, - read = readerChapter.chapter.read, - lastPageRead = readerChapter.chapter.last_page_read.toLong(), - ), - ) - } - } + fun getChapterUrl(): String? { + val sChapter = getCurrentChapter()?.chapter ?: return null + val source = getSource() ?: return null - fun restartReadTimer() { - chapterReadStartTime = Instant.now().toEpochMilli() - } - - fun flushReadTimer() { - getCurrentChapter()?.let { - viewModelScope.launchNonCancellable { - updateHistory(it) + return try { + source.getChapterUrl(sChapter) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + null } } - } - - /** - * Saves the chapter last read history if incognito mode isn't on. - */ - private suspend fun updateHistory(readerChapter: ReaderChapter) { - if (incognitoMode) return - - val chapterId = readerChapter.chapter.id!! - val endTime = Date() - val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0 - - upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration)) - chapterReadStartTime = null - } - /** - * Called from the activity to load and set the next chapter as active. - */ - suspend fun loadNextChapter() { - val nextChapter = state.value.viewerChapters?.nextChapter ?: return - loadAdjacent(nextChapter) - } - - /** - * Called from the activity to load and set the previous chapter as active. - */ - suspend fun loadPreviousChapter() { - val prevChapter = state.value.viewerChapters?.prevChapter ?: return - loadAdjacent(prevChapter) - } - - /** - * Returns the currently active chapter. - */ - private fun getCurrentChapter(): ReaderChapter? { - return state.value.currentChapter - } - - fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource + /** + * Bookmarks the currently active chapter. + */ + fun toggleChapterBookmark() { + val chapter = getCurrentChapter()?.chapter ?: return + val bookmarked = !chapter.bookmark + chapter.bookmark = bookmarked - fun getChapterUrl(): String? { - val sChapter = getCurrentChapter()?.chapter ?: return null - val source = getSource() ?: return null + viewModelScope.launchNonCancellable { + updateChapter.await( + ChapterUpdate( + id = chapter.id!!.toLong(), + bookmark = bookmarked, + ), + ) + } - return try { - source.getChapterUrl(sChapter) - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - null + mutableState.update { + it.copy( + bookmarked = bookmarked, + ) + } } - } - - /** - * Bookmarks the currently active chapter. - */ - fun toggleChapterBookmark() { - val chapter = getCurrentChapter()?.chapter ?: return - val bookmarked = !chapter.bookmark - chapter.bookmark = bookmarked - viewModelScope.launchNonCancellable { - updateChapter.await( - ChapterUpdate( - id = chapter.id!!.toLong(), - bookmark = bookmarked, - ), - ) + /** + * Returns the viewer position used by this manga or the default one. + */ + fun getMangaReadingMode(resolveDefault: Boolean = true): Int { + val default = readerPreferences.defaultReadingMode().get() + val readingMode = ReadingMode.fromPreference(manga?.readingMode?.toInt()) + return when { + resolveDefault && readingMode == ReadingMode.DEFAULT -> default + else -> manga?.readingMode?.toInt() ?: default + } } - mutableState.update { - it.copy( - bookmarked = bookmarked, - ) + /** + * Updates the viewer position for the open manga. + */ + fun setMangaReadingMode(readingMode: ReadingMode) { + val manga = manga ?: return + runBlocking(Dispatchers.IO) { + setMangaViewerFlags.awaitSetReadingMode(manga.id, readingMode.flagValue.toLong()) + val currChapters = state.value.viewerChapters + if (currChapters != null) { + // Save current page + val currChapter = currChapters.currChapter + currChapter.requestedPage = currChapter.chapter.last_page_read + + mutableState.update { + it.copy( + manga = getManga.await(manga.id), + viewerChapters = currChapters, + ) + } + eventChannel.send(Event.ReloadViewerChapters) + } + } } - } - /** - * Returns the viewer position used by this manga or the default one. - */ - fun getMangaReadingMode(resolveDefault: Boolean = true): Int { - val default = readerPreferences.defaultReadingMode().get() - val readingMode = ReadingMode.fromPreference(manga?.readingMode?.toInt()) - return when { - resolveDefault && readingMode == ReadingMode.DEFAULT -> default - else -> manga?.readingMode?.toInt() ?: default + /** + * Returns the orientation type used by this manga or the default one. + */ + fun getMangaOrientation(resolveDefault: Boolean = true): Int { + val default = readerPreferences.defaultOrientationType().get() + val orientation = ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) + return when { + resolveDefault && orientation == ReaderOrientation.DEFAULT -> default + else -> manga?.readerOrientation?.toInt() ?: default + } } - } - /** - * Updates the viewer position for the open manga. - */ - fun setMangaReadingMode(readingMode: ReadingMode) { - val manga = manga ?: return - runBlocking(Dispatchers.IO) { - setMangaViewerFlags.awaitSetReadingMode(manga.id, readingMode.flagValue.toLong()) - val currChapters = state.value.viewerChapters - if (currChapters != null) { - // Save current page - val currChapter = currChapters.currChapter - currChapter.requestedPage = currChapter.chapter.last_page_read - - mutableState.update { - it.copy( - manga = getManga.await(manga.id), - viewerChapters = currChapters, - ) + /** + * Updates the orientation type for the open manga. + */ + fun setMangaOrientationType(orientation: ReaderOrientation) { + val manga = manga ?: return + viewModelScope.launchIO { + setMangaViewerFlags.awaitSetOrientation(manga.id, orientation.flagValue.toLong()) + val currChapters = state.value.viewerChapters + if (currChapters != null) { + // Save current page + val currChapter = currChapters.currChapter + currChapter.requestedPage = currChapter.chapter.last_page_read + + mutableState.update { + it.copy( + manga = getManga.await(manga.id), + viewerChapters = currChapters, + ) + } + eventChannel.send(Event.SetOrientation(getMangaOrientation())) + eventChannel.send(Event.ReloadViewerChapters) } - eventChannel.send(Event.ReloadViewerChapters) } } - } - /** - * Returns the orientation type used by this manga or the default one. - */ - fun getMangaOrientation(resolveDefault: Boolean = true): Int { - val default = readerPreferences.defaultOrientationType().get() - val orientation = ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) - return when { - resolveDefault && orientation == ReaderOrientation.DEFAULT -> default - else -> manga?.readerOrientation?.toInt() ?: default + fun toggleCropBorders(): Boolean { + val isPagerType = ReadingMode.isPagerType(getMangaReadingMode()) + return if (isPagerType) { + readerPreferences.cropBorders().toggle() + } else { + readerPreferences.cropBordersWebtoon().toggle() + } } - } - /** - * Updates the orientation type for the open manga. - */ - fun setMangaOrientationType(orientation: ReaderOrientation) { - val manga = manga ?: return - viewModelScope.launchIO { - setMangaViewerFlags.awaitSetOrientation(manga.id, orientation.flagValue.toLong()) - val currChapters = state.value.viewerChapters - if (currChapters != null) { - // Save current page - val currChapter = currChapters.currChapter - currChapter.requestedPage = currChapter.chapter.last_page_read + /** + * Generate a filename for the given [manga] and [page] + */ + private fun generateFilename( + manga: Manga, + page: ReaderPage, + ): String { + val chapter = page.chapter.chapter + val filenameSuffix = " - ${page.number}" + return DiskUtil.buildValidFilename( + "${manga.title} - ${chapter.name}".takeBytes(DiskUtil.MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()), + ) + filenameSuffix + } - mutableState.update { - it.copy( - manga = getManga.await(manga.id), - viewerChapters = currChapters, - ) - } - eventChannel.send(Event.SetOrientation(getMangaOrientation())) - eventChannel.send(Event.ReloadViewerChapters) - } + fun showMenus(visible: Boolean) { + mutableState.update { it.copy(menuVisible = visible) } } - } - fun toggleCropBorders(): Boolean { - val isPagerType = ReadingMode.isPagerType(getMangaReadingMode()) - return if (isPagerType) { - readerPreferences.cropBorders().toggle() - } else { - readerPreferences.cropBordersWebtoon().toggle() + fun showLoadingDialog() { + mutableState.update { it.copy(dialog = Dialog.Loading) } } - } - /** - * Generate a filename for the given [manga] and [page] - */ - private fun generateFilename( - manga: Manga, - page: ReaderPage, - ): String { - val chapter = page.chapter.chapter - val filenameSuffix = " - ${page.number}" - return DiskUtil.buildValidFilename( - "${manga.title} - ${chapter.name}".takeBytes(DiskUtil.MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()), - ) + filenameSuffix - } + fun openReadingModeSelectDialog() { + mutableState.update { it.copy(dialog = Dialog.ReadingModeSelect) } + } - fun showMenus(visible: Boolean) { - mutableState.update { it.copy(menuVisible = visible) } - } + fun openOrientationModeSelectDialog() { + mutableState.update { it.copy(dialog = Dialog.OrientationModeSelect) } + } - fun showLoadingDialog() { - mutableState.update { it.copy(dialog = Dialog.Loading) } - } + fun openPageDialog(page: ReaderPage) { + mutableState.update { it.copy(dialog = Dialog.PageActions(page)) } + } - fun openReadingModeSelectDialog() { - mutableState.update { it.copy(dialog = Dialog.ReadingModeSelect) } - } + fun openSettingsDialog() { + mutableState.update { it.copy(dialog = Dialog.Settings) } + } - fun openOrientationModeSelectDialog() { - mutableState.update { it.copy(dialog = Dialog.OrientationModeSelect) } - } + fun closeDialog() { + mutableState.update { it.copy(dialog = null) } + } - fun openPageDialog(page: ReaderPage) { - mutableState.update { it.copy(dialog = Dialog.PageActions(page)) } - } + fun setBrightnessOverlayValue(value: Int) { + mutableState.update { it.copy(brightnessOverlayValue = value) } + } - fun openSettingsDialog() { - mutableState.update { it.copy(dialog = Dialog.Settings) } - } + /** + * Saves the image of the selected page on the pictures directory and notifies the UI of the result. + * There's also a notification to allow sharing the image somewhere else or deleting it. + */ + fun saveImage() { + val page = (state.value.dialog as? Dialog.PageActions)?.page + if (page?.status != Page.State.READY) return + val manga = manga ?: return - fun closeDialog() { - mutableState.update { it.copy(dialog = null) } - } + val context = Injekt.get() + val notifier = SaveImageNotifier(context) + notifier.onClear() - fun setBrightnessOverlayValue(value: Int) { - mutableState.update { it.copy(brightnessOverlayValue = value) } - } + val filename = generateFilename(manga, page) + + // Pictures directory. + val relativePath = + if (readerPreferences.folderPerManga().get()) { + DiskUtil.buildValidFilename( + manga.title, + ) + } else { + "" + } - /** - * Saves the image of the selected page on the pictures directory and notifies the UI of the result. - * There's also a notification to allow sharing the image somewhere else or deleting it. - */ - fun saveImage() { - val page = (state.value.dialog as? Dialog.PageActions)?.page - if (page?.status != Page.State.READY) return - val manga = manga ?: return + // Copy file in background. + viewModelScope.launchNonCancellable { + try { + val uri = + imageSaver.save( + image = + Image.Page( + inputStream = page.stream!!, + name = filename, + location = Location.Pictures.create(relativePath), + ), + ) + withUIContext { + notifier.onComplete(uri) + eventChannel.send(Event.SavedImage(SaveImageResult.Success(uri))) + } + } catch (e: Throwable) { + notifier.onError(e.message) + eventChannel.send(Event.SavedImage(SaveImageResult.Error(e))) + } + } + } - val context = Injekt.get() - val notifier = SaveImageNotifier(context) - notifier.onClear() + /** + * Shares the image of the selected page and notifies the UI with the path of the file to share. + * The image must be first copied to the internal partition because there are many possible + * formats it can come from, like a zipped chapter, in which case it's not possible to directly + * get a path to the file and it has to be decompressed somewhere first. Only the last shared + * image will be kept so it won't be taking lots of internal disk space. + */ + fun shareImage() { + val page = (state.value.dialog as? Dialog.PageActions)?.page + if (page?.status != Page.State.READY) return + val manga = manga ?: return - val filename = generateFilename(manga, page) + val context = Injekt.get() + val destDir = context.cacheImageDir - // Pictures directory. - val relativePath = if (readerPreferences.folderPerManga().get()) { - DiskUtil.buildValidFilename( - manga.title, - ) - } else { - "" - } + val filename = generateFilename(manga, page) - // Copy file in background. - viewModelScope.launchNonCancellable { try { - val uri = imageSaver.save( - image = Image.Page( - inputStream = page.stream!!, - name = filename, - location = Location.Pictures.create(relativePath), - ), - ) - withUIContext { - notifier.onComplete(uri) - eventChannel.send(Event.SavedImage(SaveImageResult.Success(uri))) + viewModelScope.launchNonCancellable { + destDir.deleteRecursively() + val uri = + imageSaver.save( + image = + Image.Page( + inputStream = page.stream!!, + name = filename, + location = Location.Cache, + ), + ) + eventChannel.send(Event.ShareImage(uri, page)) } } catch (e: Throwable) { - notifier.onError(e.message) - eventChannel.send(Event.SavedImage(SaveImageResult.Error(e))) + logcat(LogPriority.ERROR, e) } } - } - /** - * Shares the image of the selected page and notifies the UI with the path of the file to share. - * The image must be first copied to the internal partition because there are many possible - * formats it can come from, like a zipped chapter, in which case it's not possible to directly - * get a path to the file and it has to be decompressed somewhere first. Only the last shared - * image will be kept so it won't be taking lots of internal disk space. - */ - fun shareImage() { - val page = (state.value.dialog as? Dialog.PageActions)?.page - if (page?.status != Page.State.READY) return - val manga = manga ?: return + /** + * Sets the image of the selected page as cover and notifies the UI of the result. + */ + fun setAsCover() { + val page = (state.value.dialog as? Dialog.PageActions)?.page + if (page?.status != Page.State.READY) return + val manga = manga ?: return + val stream = page.stream ?: return + + viewModelScope.launchNonCancellable { + val result = + try { + manga.editCover(Injekt.get(), stream()) + if (manga.isLocal() || manga.favorite) { + SetAsCoverResult.Success + } else { + SetAsCoverResult.AddToLibraryFirst + } + } catch (e: Exception) { + SetAsCoverResult.Error + } + eventChannel.send(Event.SetCoverResult(result)) + } + } + + enum class SetAsCoverResult { + Success, + AddToLibraryFirst, + Error, + } + + sealed interface SaveImageResult { + class Success( + val uri: Uri, + ) : SaveImageResult - val context = Injekt.get() - val destDir = context.cacheImageDir + class Error( + val error: Throwable, + ) : SaveImageResult + } - val filename = generateFilename(manga, page) + /** + * Starts the service that updates the last chapter read in sync services. This operation + * will run in a background thread and errors are ignored. + */ + private fun updateTrackChapterRead(readerChapter: ReaderChapter) { + if (incognitoMode) return + if (!trackPreferences.autoUpdateTrack().get()) return + + val manga = manga ?: return + val context = Injekt.get() - try { viewModelScope.launchNonCancellable { - destDir.deleteRecursively() - val uri = imageSaver.save( - image = Image.Page( - inputStream = page.stream!!, - name = filename, - location = Location.Cache, - ), - ) - eventChannel.send(Event.ShareImage(uri, page)) + trackChapter.await(context, manga.id, readerChapter.chapter.chapter_number.toDouble()) } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) } - } - /** - * Sets the image of the selected page as cover and notifies the UI of the result. - */ - fun setAsCover() { - val page = (state.value.dialog as? Dialog.PageActions)?.page - if (page?.status != Page.State.READY) return - val manga = manga ?: return - val stream = page.stream ?: return - - viewModelScope.launchNonCancellable { - val result = try { - manga.editCover(Injekt.get(), stream()) - if (manga.isLocal() || manga.favorite) { - SetAsCoverResult.Success - } else { - SetAsCoverResult.AddToLibraryFirst - } - } catch (e: Exception) { - SetAsCoverResult.Error + /** + * Enqueues this [chapter] to be deleted when [deletePendingChapters] is called. The download + * manager handles persisting it across process deaths. + */ + private fun enqueueDeleteReadChapters(chapter: ReaderChapter) { + if (!chapter.chapter.read) return + val manga = manga ?: return + + viewModelScope.launchNonCancellable { + downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga) } - eventChannel.send(Event.SetCoverResult(result)) } - } - enum class SetAsCoverResult { - Success, - AddToLibraryFirst, - Error, - } + /** + * Deletes all the pending chapters. This operation will run in a background thread and errors + * are ignored. + */ + private fun deletePendingChapters() { + viewModelScope.launchNonCancellable { + downloadManager.deletePendingChapters() + } + } - sealed interface SaveImageResult { - class Success(val uri: Uri) : SaveImageResult - class Error(val error: Throwable) : SaveImageResult - } + @Immutable + data class State( + val manga: Manga? = null, + val viewerChapters: ViewerChapters? = null, + val bookmarked: Boolean = false, + val isLoadingAdjacentChapter: Boolean = false, + val currentPage: Int = -1, + /** + * Viewer used to display the pages (pager, webtoon, ...). + */ + val viewer: Viewer? = null, + val dialog: Dialog? = null, + val menuVisible: Boolean = false, + @IntRange(from = -100, to = 100) val brightnessOverlayValue: Int = 0, + ) { + val currentChapter: ReaderChapter? + get() = viewerChapters?.currChapter - /** - * Starts the service that updates the last chapter read in sync services. This operation - * will run in a background thread and errors are ignored. - */ - private fun updateTrackChapterRead(readerChapter: ReaderChapter) { - if (incognitoMode) return - if (!trackPreferences.autoUpdateTrack().get()) return + val totalPages: Int + get() = currentChapter?.pages?.size ?: -1 + } - val manga = manga ?: return - val context = Injekt.get() + sealed interface Dialog { + data object Loading : Dialog - viewModelScope.launchNonCancellable { - trackChapter.await(context, manga.id, readerChapter.chapter.chapter_number.toDouble()) - } - } + data object Settings : Dialog - /** - * Enqueues this [chapter] to be deleted when [deletePendingChapters] is called. The download - * manager handles persisting it across process deaths. - */ - private fun enqueueDeleteReadChapters(chapter: ReaderChapter) { - if (!chapter.chapter.read) return - val manga = manga ?: return + data object ReadingModeSelect : Dialog - viewModelScope.launchNonCancellable { - downloadManager.enqueueChaptersToDelete(listOf(chapter.chapter.toDomainChapter()!!), manga) - } - } + data object OrientationModeSelect : Dialog - /** - * Deletes all the pending chapters. This operation will run in a background thread and errors - * are ignored. - */ - private fun deletePendingChapters() { - viewModelScope.launchNonCancellable { - downloadManager.deletePendingChapters() + data class PageActions( + val page: ReaderPage, + ) : Dialog } - } - @Immutable - data class State( - val manga: Manga? = null, - val viewerChapters: ViewerChapters? = null, - val bookmarked: Boolean = false, - val isLoadingAdjacentChapter: Boolean = false, - val currentPage: Int = -1, + sealed interface Event { + data object ReloadViewerChapters : Event - /** - * Viewer used to display the pages (pager, webtoon, ...). - */ - val viewer: Viewer? = null, - val dialog: Dialog? = null, - val menuVisible: Boolean = false, - @IntRange(from = -100, to = 100) val brightnessOverlayValue: Int = 0, - ) { - val currentChapter: ReaderChapter? - get() = viewerChapters?.currChapter + data object PageChanged : Event - val totalPages: Int - get() = currentChapter?.pages?.size ?: -1 - } + data class SetOrientation( + val orientation: Int, + ) : Event - sealed interface Dialog { - data object Loading : Dialog - data object Settings : Dialog - data object ReadingModeSelect : Dialog - data object OrientationModeSelect : Dialog - data class PageActions(val page: ReaderPage) : Dialog - } + data class SetCoverResult( + val result: SetAsCoverResult, + ) : Event - sealed interface Event { - data object ReloadViewerChapters : Event - data object PageChanged : Event - data class SetOrientation(val orientation: Int) : Event - data class SetCoverResult(val result: SetAsCoverResult) : Event + data class SavedImage( + val result: SaveImageResult, + ) : Event - data class SavedImage(val result: SaveImageResult) : Event - data class ShareImage(val uri: Uri, val page: ReaderPage) : Event + data class ShareImage( + val uri: Uri, + val page: ReaderPage, + ) : Event + } } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt index 6b08dfeb30..9f4244854f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt @@ -22,8 +22,9 @@ import tachiyomi.i18n.MR /** * Class used to show BigPictureStyle notifications */ -class SaveImageNotifier(private val context: Context) { - +class SaveImageNotifier( + private val context: Context, +) { private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_COMMON) private val notificationId: Int = Notifications.ID_DOWNLOAD_IMAGE @@ -33,15 +34,16 @@ class SaveImageNotifier(private val context: Context) { * @param uri image file containing downloaded page image. */ fun onComplete(uri: Uri) { - val request = ImageRequest.Builder(context) - .data(uri) - .memoryCachePolicy(CachePolicy.DISABLED) - .size(720, 1280) - .target( - onSuccess = { showCompleteNotification(uri, it.asDrawable(context.resources).getBitmapOrNull()) }, - onError = { onError(null) }, - ) - .build() + val request = + ImageRequest + .Builder(context) + .data(uri) + .memoryCachePolicy(CachePolicy.DISABLED) + .size(720, 1280) + .target( + onSuccess = { showCompleteNotification(uri, it.asDrawable(context.resources).getBitmapOrNull()) }, + onError = { onError(null) }, + ).build() context.imageLoader.enqueue(request) } @@ -66,7 +68,10 @@ class SaveImageNotifier(private val context: Context) { updateNotification() } - private fun showCompleteNotification(uri: Uri, image: Bitmap?) { + private fun showCompleteNotification( + uri: Uri, + image: Bitmap?, + ) { with(notificationBuilder) { setContentTitle(context.stringResource(MR.strings.picture_saved)) setSmallIcon(R.drawable.ic_photo_24dp) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt index 397ac51bc1..6c2c391da7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt @@ -9,21 +9,23 @@ import tachiyomi.core.common.util.system.ImageUtil /** * Loader used to load a chapter from an archive file. */ -internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() { +internal class ArchivePageLoader( + private val reader: ArchiveReader, +) : PageLoader() { override var isLocal: Boolean = true - override suspend fun getPages(): List = reader.useEntries { entries -> - entries - .filter { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } } - .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - .mapIndexed { i, entry -> - ReaderPage(i).apply { - stream = { reader.getInputStream(entry.name)!! } - status = Page.State.READY - } - } - .toList() - } + override suspend fun getPages(): List = + reader.useEntries { entries -> + entries + .filter { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } } + .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + .mapIndexed { i, entry -> + ReaderPage(i).apply { + stream = { reader.getInputStream(entry.name)!! } + status = Page.State.READY + } + }.toList() + } override suspend fun loadPage(page: ReaderPage) { check(!isRecycled) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 3c1b34d6c7..f4abd34573 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -26,7 +26,6 @@ class ChapterLoader( private val manga: Manga, private val source: Source, ) { - /** * Assigns the chapter's page loader and loads the its pages. Returns immediately if the chapter * is already loaded. @@ -43,8 +42,10 @@ class ChapterLoader( val loader = getPageLoader(chapter) chapter.pageLoader = loader - val pages = loader.getPages() - .onEach { it.chapter = chapter } + val pages = + loader + .getPages() + .onEach { it.chapter = chapter } if (pages.isEmpty()) { throw Exception(context.stringResource(MR.strings.page_list_empty_error)) @@ -67,37 +68,39 @@ class ChapterLoader( /** * Checks [chapter] to be loaded based on present pages and loader in addition to state. */ - private fun chapterIsReady(chapter: ReaderChapter): Boolean { - return chapter.state is ReaderChapter.State.Loaded && chapter.pageLoader != null - } + private fun chapterIsReady(chapter: ReaderChapter): Boolean = + chapter.state is ReaderChapter.State.Loaded && chapter.pageLoader != null /** * Returns the page loader to use for this [chapter]. */ private fun getPageLoader(chapter: ReaderChapter): PageLoader { val dbChapter = chapter.chapter - val isDownloaded = downloadManager.isChapterDownloaded( - dbChapter.name, - dbChapter.scanlator, - manga.title, - manga.source, - skipCache = true, - ) - return when { - isDownloaded -> DownloadPageLoader( - chapter, - manga, - source, - downloadManager, - downloadProvider, + val isDownloaded = + downloadManager.isChapterDownloaded( + dbChapter.name, + dbChapter.scanlator, + manga.title, + manga.source, + skipCache = true, ) - source is LocalSource -> source.getFormat(chapter.chapter).let { format -> - when (format) { - is Format.Directory -> DirectoryPageLoader(format.file) - is Format.Archive -> ArchivePageLoader(format.file.archiveReader(context)) - is Format.Epub -> EpubPageLoader(format.file.archiveReader(context)) + return when { + isDownloaded -> + DownloadPageLoader( + chapter, + manga, + source, + downloadManager, + downloadProvider, + ) + source is LocalSource -> + source.getFormat(chapter.chapter).let { format -> + when (format) { + is Format.Directory -> DirectoryPageLoader(format.file) + is Format.Archive -> ArchivePageLoader(format.file.archiveReader(context)) + is Format.Epub -> EpubPageLoader(format.file.archiveReader(context)) + } } - } source is HttpSource -> HttpPageLoader(chapter, source) source is StubSource -> error(context.stringResource(MR.strings.source_not_installed, source.toString())) else -> error(context.stringResource(MR.strings.loader_not_implemented_error)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt index 8817b0682c..1a3a78e08c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt @@ -9,12 +9,14 @@ import tachiyomi.core.common.util.system.ImageUtil /** * Loader used to load a chapter from a directory given on [file]. */ -internal class DirectoryPageLoader(val file: UniFile) : PageLoader() { - +internal class DirectoryPageLoader( + val file: UniFile, +) : PageLoader() { override var isLocal: Boolean = true - override suspend fun getPages(): List { - return file.listFiles() + override suspend fun getPages(): List = + file + .listFiles() ?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } } ?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) } ?.mapIndexed { i, file -> @@ -23,7 +25,5 @@ internal class DirectoryPageLoader(val file: UniFile) : PageLoader() { stream = streamFn status = Page.State.READY } - } - .orEmpty() - } + }.orEmpty() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index 7b0f5c36c7..6e0878f650 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -24,7 +24,6 @@ internal class DownloadPageLoader( private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, ) : PageLoader() { - private val context: Application by injectLazy() private var archivePageLoader: ArchivePageLoader? = null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index 8ace2fdeee..3adac38b00 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -8,14 +8,16 @@ import mihon.core.common.archive.ArchiveReader /** * Loader used to load a chapter from a .epub file. */ -internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() { - +internal class EpubPageLoader( + reader: ArchiveReader, +) : PageLoader() { private val epub = EpubFile(reader) override var isLocal: Boolean = true - override suspend fun getPages(): List { - return epub.getImagesFromPages() + override suspend fun getPages(): List = + epub + .getImagesFromPages() .mapIndexed { i, path -> val streamFn = { epub.getInputStream(path)!! } ReaderPage(i).apply { @@ -23,7 +25,6 @@ internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() { status = Page.State.READY } } - } override suspend fun loadPage(page: ReaderPage) { check(!isRecycled) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index 14c27bf957..21745d9848 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -31,7 +31,6 @@ internal class HttpPageLoader( private val source: HttpSource, private val chapterCache: ChapterCache = Injekt.get(), ) : PageLoader() { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) /** @@ -47,8 +46,7 @@ internal class HttpPageLoader( while (true) { emit(runInterruptible { queue.take() }.page) } - } - .filter { it.status == Page.State.QUEUE } + }.filter { it.status == Page.State.QUEUE } .collect(::internalLoadPage) } } @@ -60,14 +58,15 @@ internal class HttpPageLoader( * otherwise fallbacks to network. */ override suspend fun getPages(): List { - val pages = try { - chapterCache.getPageListFromCache(chapter.chapter.toDomainChapter()!!) - } catch (e: Throwable) { - if (e is CancellationException) { - throw e + val pages = + try { + chapterCache.getPageListFromCache(chapter.chapter.toDomainChapter()!!) + } catch (e: Throwable) { + if (e is CancellationException) { + throw e + } + source.getPageList(chapter.chapter) } - source.getPageList(chapter.chapter) - } return pages.mapIndexed { index, page -> // Don't trust sources and use our own indexing ReaderPage(index, page.url, page.imageUrl) @@ -77,35 +76,36 @@ internal class HttpPageLoader( /** * Loads a page through the queue. Handles re-enqueueing pages if they were evicted from the cache. */ - override suspend fun loadPage(page: ReaderPage) = withIOContext { - val imageUrl = page.imageUrl - - // Check if the image has been deleted - if (page.status == Page.State.READY && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) { - page.status = Page.State.QUEUE - } + override suspend fun loadPage(page: ReaderPage) = + withIOContext { + val imageUrl = page.imageUrl - // Automatically retry failed pages when subscribed to this page - if (page.status == Page.State.ERROR) { - page.status = Page.State.QUEUE - } + // Check if the image has been deleted + if (page.status == Page.State.READY && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) { + page.status = Page.State.QUEUE + } - val queuedPages = mutableListOf() - if (page.status == Page.State.QUEUE) { - queuedPages += PriorityPage(page, 1).also { queue.offer(it) } - } - queuedPages += preloadNextPages(page, preloadSize) + // Automatically retry failed pages when subscribed to this page + if (page.status == Page.State.ERROR) { + page.status = Page.State.QUEUE + } - suspendCancellableCoroutine { continuation -> - continuation.invokeOnCancellation { - queuedPages.forEach { - if (it.page.status == Page.State.QUEUE) { - queue.remove(it) + val queuedPages = mutableListOf() + if (page.status == Page.State.QUEUE) { + queuedPages += PriorityPage(page, 1).also { queue.offer(it) } + } + queuedPages += preloadNextPages(page, preloadSize) + + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { + queuedPages.forEach { + if (it.page.status == Page.State.QUEUE) { + queue.remove(it) + } } } } } - } /** * Retries a page. This method is only called from user interaction on the viewer. @@ -143,7 +143,10 @@ internal class HttpPageLoader( * * @return a list of [PriorityPage] that were added to the [queue] */ - private fun preloadNextPages(currentPage: ReaderPage, amount: Int): List { + private fun preloadNextPages( + currentPage: ReaderPage, + amount: Int, + ): List { val pageIndex = currentPage.index val pages = currentPage.chapter.pages ?: return emptyList() if (pageIndex == pages.lastIndex) return emptyList() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt index 164de6bda4..40aec8005a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt @@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage * method [recycle] is called. */ abstract class PageLoader { - /** * Whether this loader has been already recycled. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt index 2da46e5b0a..4de9943fea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.model sealed class ChapterTransition { - abstract val from: ReaderChapter abstract val to: ReaderChapter? @@ -29,7 +28,5 @@ sealed class ChapterTransition { return result } - override fun toString(): String { - return "${javaClass.simpleName}(from=${from.chapter.url}, to=${to?.chapter?.url})" - } + override fun toString(): String = "${javaClass.simpleName}(from=${from.chapter.url}, to=${to?.chapter?.url})" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/InsertPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/InsertPage.kt index c605d6e65d..74070f778e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/InsertPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/InsertPage.kt @@ -1,7 +1,8 @@ package eu.kanade.tachiyomi.ui.reader.model -class InsertPage(val parent: ReaderPage) : ReaderPage(parent.index, parent.url, parent.imageUrl) { - +class InsertPage( + val parent: ReaderPage, +) : ReaderPage(parent.index, parent.url, parent.imageUrl) { override var chapter: ReaderChapter = parent.chapter init { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt index cb3bca2568..9423bfb9d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt @@ -6,8 +6,9 @@ import eu.kanade.tachiyomi.ui.reader.loader.PageLoader import kotlinx.coroutines.flow.MutableStateFlow import tachiyomi.core.common.util.system.logcat -data class ReaderChapter(val chapter: Chapter) { - +data class ReaderChapter( + val chapter: Chapter, +) { val stateFlow = MutableStateFlow(State.Wait) var state: State get() = stateFlow.value @@ -44,8 +45,15 @@ data class ReaderChapter(val chapter: Chapter) { sealed interface State { data object Wait : State + data object Loading : State - data class Error(val error: Throwable) : State - data class Loaded(val pages: List) : State + + data class Error( + val error: Throwable, + ) : State + + data class Loaded( + val pages: List, + ) : State } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt index 6602b96185..108a02fc6b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt @@ -9,6 +9,5 @@ open class ReaderPage( imageUrl: String? = null, var stream: (() -> InputStream)? = null, ) : Page(index, url, imageUrl, null) { - open lateinit var chapter: ReaderChapter } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ViewerChapters.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ViewerChapters.kt index 6fb5905c33..9b08614282 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ViewerChapters.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ViewerChapters.kt @@ -5,7 +5,6 @@ data class ViewerChapters( val prevChapter: ReaderChapter?, val nextChapter: ReaderChapter?, ) { - fun ref() { currChapter.ref() prevChapter?.ref() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt index 57def3c12a..a7b0d18e8e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt @@ -10,7 +10,6 @@ import tachiyomi.i18n.MR class ReaderPreferences( private val preferenceStore: PreferenceStore, ) { - // region General fun pageTransitions() = preferenceStore.getBoolean("pref_enable_transitions_key", true) @@ -35,15 +34,17 @@ class ReaderPreferences( fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", true) - fun defaultReadingMode() = preferenceStore.getInt( - "pref_default_reading_mode_key", - ReadingMode.RIGHT_TO_LEFT.flagValue, - ) + fun defaultReadingMode() = + preferenceStore.getInt( + "pref_default_reading_mode_key", + ReadingMode.RIGHT_TO_LEFT.flagValue, + ) - fun defaultOrientationType() = preferenceStore.getInt( - "pref_default_orientation_type_key", - ReaderOrientation.FREE.flagValue, - ) + fun defaultOrientationType() = + preferenceStore.getInt( + "pref_default_orientation_type_key", + ReaderOrientation.FREE.flagValue, + ) fun webtoonDoubleTapZoomEnabled() = preferenceStore.getBoolean("pref_enable_double_tap_zoom_webtoon", true) @@ -142,7 +143,7 @@ class ReaderPreferences( enum class FlashColor { BLACK, WHITE, - WHITE_BLACK + WHITE_BLACK, } enum class TappingInvertMode( @@ -156,7 +157,9 @@ class ReaderPreferences( BOTH(MR.strings.tapping_inverted_both, shouldInvertHorizontal = true, shouldInvertVertical = true), } - enum class ReaderHideThreshold(val threshold: Int) { + enum class ReaderHideThreshold( + val threshold: Int, + ) { HIGHEST(5), HIGH(13), LOW(31), @@ -169,48 +172,52 @@ class ReaderPreferences( const val MILLI_CONVERSION = 100 - val TapZones = listOf( - MR.strings.label_default, - MR.strings.l_nav, - MR.strings.kindlish_nav, - MR.strings.edge_nav, - MR.strings.right_and_left_nav, - MR.strings.disabled_nav, - ) - - val ImageScaleType = listOf( - MR.strings.scale_type_fit_screen, - MR.strings.scale_type_stretch, - MR.strings.scale_type_fit_width, - MR.strings.scale_type_fit_height, - MR.strings.scale_type_original_size, - MR.strings.scale_type_smart_fit, - ) + val TapZones = + listOf( + MR.strings.label_default, + MR.strings.l_nav, + MR.strings.kindlish_nav, + MR.strings.edge_nav, + MR.strings.right_and_left_nav, + MR.strings.disabled_nav, + ) - val ZoomStart = listOf( - MR.strings.zoom_start_automatic, - MR.strings.zoom_start_left, - MR.strings.zoom_start_right, - MR.strings.zoom_start_center, - ) + val ImageScaleType = + listOf( + MR.strings.scale_type_fit_screen, + MR.strings.scale_type_stretch, + MR.strings.scale_type_fit_width, + MR.strings.scale_type_fit_height, + MR.strings.scale_type_original_size, + MR.strings.scale_type_smart_fit, + ) - val ColorFilterMode = buildList { - addAll( - listOf( - MR.strings.label_default to BlendMode.SrcOver, - MR.strings.filter_mode_multiply to BlendMode.Modulate, - MR.strings.filter_mode_screen to BlendMode.Screen, - ), + val ZoomStart = + listOf( + MR.strings.zoom_start_automatic, + MR.strings.zoom_start_left, + MR.strings.zoom_start_right, + MR.strings.zoom_start_center, ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + + val ColorFilterMode = + buildList { addAll( listOf( - MR.strings.filter_mode_overlay to BlendMode.Overlay, - MR.strings.filter_mode_lighten to BlendMode.Lighten, - MR.strings.filter_mode_darken to BlendMode.Darken, + MR.strings.label_default to BlendMode.SrcOver, + MR.strings.filter_mode_multiply to BlendMode.Modulate, + MR.strings.filter_mode_screen to BlendMode.Screen, ), ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAll( + listOf( + MR.strings.filter_mode_overlay to BlendMode.Overlay, + MR.strings.filter_mode_lighten to BlendMode.Lighten, + MR.strings.filter_mode_darken to BlendMode.Darken, + ), + ) + } } - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt index 5f107c3889..705dab91c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt @@ -18,14 +18,15 @@ class ReaderSettingsScreenModel( val onChangeOrientation: (ReaderOrientation) -> Unit, val preferences: ReaderPreferences = Injekt.get(), ) : ScreenModel { + val viewerFlow = + readerState + .map { it.viewer } + .distinctUntilChanged() + .stateIn(ioCoroutineScope, SharingStarted.Lazily, null) - val viewerFlow = readerState - .map { it.viewer } - .distinctUntilChanged() - .stateIn(ioCoroutineScope, SharingStarted.Lazily, null) - - val mangaFlow = readerState - .map { it.manga } - .distinctUntilChanged() - .stateIn(ioCoroutineScope, SharingStarted.Lazily, null) + val mangaFlow = + readerState + .map { it.manga } + .distinctUntilChanged() + .stateIn(ioCoroutineScope, SharingStarted.Lazily, null) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingMode.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingMode.kt index 8c97ed818b..b06a854a13 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingMode.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingMode.kt @@ -66,8 +66,11 @@ enum class ReadingMode( return mode.type is ViewerType.Pager } - fun toViewer(preference: Int?, activity: ReaderActivity): Viewer { - return when (fromPreference(preference)) { + fun toViewer( + preference: Int?, + activity: ReaderActivity, + ): Viewer = + when (fromPreference(preference)) { LEFT_TO_RIGHT -> L2RPagerViewer(activity) RIGHT_TO_LEFT -> R2LPagerViewer(activity) VERTICAL -> VerticalPagerViewer(activity) @@ -75,16 +78,17 @@ enum class ReadingMode( CONTINUOUS_VERTICAL -> WebtoonViewer(activity, isContinuous = false) DEFAULT -> throw IllegalStateException("Preference value must be resolved: $preference") } - } } sealed interface Direction { data object Horizontal : Direction + data object Vertical : Direction } sealed interface ViewerType { data object Pager : ViewerType + data object Webtoon : ViewerType } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt index 50c687a333..08220c07d3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt @@ -16,7 +16,6 @@ open class GestureDetectorWithLongTap( context: Context, listener: Listener, ) : GestureDetector(context, listener) { - private val handler = Handler(Looper.getMainLooper()) private val slop = ViewConfiguration.get(context).scaledTouchSlop private val longTapTime = ViewConfiguration.getLongPressTimeout().toLong() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/MissingChapters.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/MissingChapters.kt index 3c7c03aea1..ec60d27555 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/MissingChapters.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/MissingChapters.kt @@ -4,9 +4,11 @@ import eu.kanade.tachiyomi.data.database.models.toDomainChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import tachiyomi.domain.chapter.service.calculateChapterGap as domainCalculateChapterGap -fun calculateChapterGap(higherReaderChapter: ReaderChapter?, lowerReaderChapter: ReaderChapter?): Int { - return domainCalculateChapterGap( +fun calculateChapterGap( + higherReaderChapter: ReaderChapter?, + lowerReaderChapter: ReaderChapter?, +): Int = + domainCalculateChapterGap( higherReaderChapter?.chapter?.toDomainChapter(), lowerReaderChapter?.chapter?.toDomainChapter(), ) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt index 270d5549bb..7ac385f289 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt @@ -11,19 +11,20 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer * A button class to be used by child views of the pager viewer. All tap gestures are handled by * the pager, but this class disables that behavior to allow clickable buttons. */ -class ReaderButton @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.materialButtonStyle, -) : MaterialButton(context, attrs, defStyleAttr) { +class ReaderButton + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.materialButtonStyle, + ) : MaterialButton(context, attrs, defStyleAttr) { + var viewer: PagerViewer? = null - var viewer: PagerViewer? = null - - override fun onTouchEvent(event: MotionEvent): Boolean { - viewer?.pager?.setGestureDetectorEnabled(false) - if (event.actionMasked == MotionEvent.ACTION_UP) { - viewer?.pager?.setGestureDetectorEnabled(true) + override fun onTouchEvent(event: MotionEvent): Boolean { + viewer?.pager?.setGestureDetectorEnabled(false) + if (event.actionMasked == MotionEvent.ACTION_UP) { + viewer?.pager?.setGestureDetectorEnabled(true) + } + return super.onTouchEvent(event) } - return super.onTouchEvent(event) } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 61e78c161d..4bb3c8770a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -49,363 +49,384 @@ import okio.BufferedSource * @param isWebtoon if true, [WebtoonSubsamplingImageView] will be used instead of [SubsamplingScaleImageView] * and [AppCompatImageView] will be used instead of [PhotoView] */ -open class ReaderPageImageView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttrs: Int = 0, - @StyleRes defStyleRes: Int = 0, - private val isWebtoon: Boolean = false, -) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) { - - private var pageView: View? = null - - private var config: Config? = null - - var onImageLoaded: (() -> Unit)? = null - var onImageLoadError: (() -> Unit)? = null - var onScaleChanged: ((newScale: Float) -> Unit)? = null - var onViewClicked: (() -> Unit)? = null - - /** - * For automatic background. Will be set as background color when [onImageLoaded] is called. - */ - var pageBackground: Drawable? = null - - @CallSuper - open fun onImageLoaded() { - onImageLoaded?.invoke() - background = pageBackground - } +open class ReaderPageImageView + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttrs: Int = 0, + @StyleRes defStyleRes: Int = 0, + private val isWebtoon: Boolean = false, + ) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) { + private var pageView: View? = null + + private var config: Config? = null + + var onImageLoaded: (() -> Unit)? = null + var onImageLoadError: (() -> Unit)? = null + var onScaleChanged: ((newScale: Float) -> Unit)? = null + var onViewClicked: (() -> Unit)? = null + + /** + * For automatic background. Will be set as background color when [onImageLoaded] is called. + */ + var pageBackground: Drawable? = null + + @CallSuper + open fun onImageLoaded() { + onImageLoaded?.invoke() + background = pageBackground + } - @CallSuper - open fun onImageLoadError() { - onImageLoadError?.invoke() - } + @CallSuper + open fun onImageLoadError() { + onImageLoadError?.invoke() + } - @CallSuper - open fun onScaleChanged(newScale: Float) { - onScaleChanged?.invoke(newScale) - } + @CallSuper + open fun onScaleChanged(newScale: Float) { + onScaleChanged?.invoke(newScale) + } - @CallSuper - open fun onViewClicked() { - onViewClicked?.invoke() - } + @CallSuper + open fun onViewClicked() { + onViewClicked?.invoke() + } - open fun onPageSelected(forward: Boolean) { - with(pageView as? SubsamplingScaleImageView) { - if (this == null) return - if (isReady) { - landscapeZoom(forward) - } else { - setOnImageEventListener( - object : SubsamplingScaleImageView.DefaultOnImageEventListener() { - override fun onReady() { - setupZoom(config) - landscapeZoom(forward) - this@ReaderPageImageView.onImageLoaded() - } + open fun onPageSelected(forward: Boolean) { + with(pageView as? SubsamplingScaleImageView) { + if (this == null) return + if (isReady) { + landscapeZoom(forward) + } else { + setOnImageEventListener( + object : SubsamplingScaleImageView.DefaultOnImageEventListener() { + override fun onReady() { + setupZoom(config) + landscapeZoom(forward) + this@ReaderPageImageView.onImageLoaded() + } + + override fun onImageLoadError(e: Exception) { + onImageLoadError() + } + }, + ) + } + } + } - override fun onImageLoadError(e: Exception) { - onImageLoadError() + private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean) { + if ( + config != null && + config!!.landscapeZoom && + config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && + sWidth > sHeight && + scale == minScale + ) { + handler?.postDelayed(500) { + val point = + when (config!!.zoomStartPosition) { + ZoomStartPosition.LEFT -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F) + ZoomStartPosition.RIGHT -> if (forward) PointF(sWidth.toFloat(), 0F) else PointF(0F, 0F) + ZoomStartPosition.CENTER -> center } - }, - ) + + val targetScale = height.toFloat() / sHeight.toFloat() + animateScaleAndCenter(targetScale, point)!! + .withDuration(500) + .withEasing(EASE_IN_OUT_QUAD) + .withInterruptible(true) + .start() + } } } - } - private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean) { - if ( - config != null && - config!!.landscapeZoom && - config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && - sWidth > sHeight && - scale == minScale + fun setImage( + drawable: Drawable, + config: Config, ) { - handler?.postDelayed(500) { - val point = when (config!!.zoomStartPosition) { - ZoomStartPosition.LEFT -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F) - ZoomStartPosition.RIGHT -> if (forward) PointF(sWidth.toFloat(), 0F) else PointF(0F, 0F) - ZoomStartPosition.CENTER -> center - } + this.config = config + if (drawable is Animatable) { + prepareAnimatedImageView() + setAnimatedImage(drawable, config) + } else { + prepareNonAnimatedImageView() + setNonAnimatedImage(drawable, config) + } + } - val targetScale = height.toFloat() / sHeight.toFloat() - animateScaleAndCenter(targetScale, point)!! - .withDuration(500) - .withEasing(EASE_IN_OUT_QUAD) - .withInterruptible(true) - .start() + fun setImage( + source: BufferedSource, + isAnimated: Boolean, + config: Config, + ) { + this.config = config + if (isAnimated) { + prepareAnimatedImageView() + setAnimatedImage(source, config) + } else { + prepareNonAnimatedImageView() + setNonAnimatedImage(source, config) } } - } - fun setImage(drawable: Drawable, config: Config) { - this.config = config - if (drawable is Animatable) { - prepareAnimatedImageView() - setAnimatedImage(drawable, config) - } else { - prepareNonAnimatedImageView() - setNonAnimatedImage(drawable, config) + fun recycle() = + pageView?.let { + when (it) { + is SubsamplingScaleImageView -> it.recycle() + is AppCompatImageView -> it.dispose() + } + it.isVisible = false + } + + /** + * Check if the image can be panned to the left + */ + fun canPanLeft(): Boolean = canPan { it.left } + + /** + * Check if the image can be panned to the right + */ + fun canPanRight(): Boolean = canPan { it.right } + + /** + * Check whether the image can be panned. + * @param fn a function that returns the direction to check for + */ + private fun canPan(fn: (RectF) -> Float): Boolean { + (pageView as? SubsamplingScaleImageView)?.let { view -> + RectF().let { + view.getPanRemaining(it) + return fn(it) > 1 + } + } + return false } - } - fun setImage(source: BufferedSource, isAnimated: Boolean, config: Config) { - this.config = config - if (isAnimated) { - prepareAnimatedImageView() - setAnimatedImage(source, config) - } else { - prepareNonAnimatedImageView() - setNonAnimatedImage(source, config) + /** + * Pans the image to the left by a screen's width worth. + */ + fun panLeft() { + pan { center, view -> center.also { it.x -= view.width / view.scale } } } - } - fun recycle() = pageView?.let { - when (it) { - is SubsamplingScaleImageView -> it.recycle() - is AppCompatImageView -> it.dispose() + /** + * Pans the image to the right by a screen's width worth. + */ + fun panRight() { + pan { center, view -> center.also { it.x += view.width / view.scale } } } - it.isVisible = false - } - /** - * Check if the image can be panned to the left - */ - fun canPanLeft(): Boolean = canPan { it.left } - - /** - * Check if the image can be panned to the right - */ - fun canPanRight(): Boolean = canPan { it.right } - - /** - * Check whether the image can be panned. - * @param fn a function that returns the direction to check for - */ - private fun canPan(fn: (RectF) -> Float): Boolean { - (pageView as? SubsamplingScaleImageView)?.let { view -> - RectF().let { - view.getPanRemaining(it) - return fn(it) > 1 + /** + * Pans the image. + * @param fn a function that computes the new center of the image + */ + private fun pan(fn: (PointF, SubsamplingScaleImageView) -> PointF) { + (pageView as? SubsamplingScaleImageView)?.let { view -> + + val target = fn(view.center ?: return, view) + view + .animateCenter(target)!! + .withEasing(EASE_OUT_QUAD) + .withDuration(250) + .withInterruptible(true) + .start() } } - return false - } - /** - * Pans the image to the left by a screen's width worth. - */ - fun panLeft() { - pan { center, view -> center.also { it.x -= view.width / view.scale } } - } + private fun prepareNonAnimatedImageView() { + if (pageView is SubsamplingScaleImageView) return + removeView(pageView) + + pageView = + if (isWebtoon) { + WebtoonSubsamplingImageView(context) + } else { + SubsamplingScaleImageView(context) + }.apply { + setMaxTileSize(GLUtil.maxTextureSize) + setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) + setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) + setMinimumTileDpi(180) + setOnStateChangedListener( + object : SubsamplingScaleImageView.OnStateChangedListener { + override fun onScaleChanged( + newScale: Float, + origin: Int, + ) { + this@ReaderPageImageView.onScaleChanged(newScale) + } - /** - * Pans the image to the right by a screen's width worth. - */ - fun panRight() { - pan { center, view -> center.also { it.x += view.width / view.scale } } - } + override fun onCenterChanged( + newCenter: PointF?, + origin: Int, + ) { + // Not used + } + }, + ) + setOnClickListener { this@ReaderPageImageView.onViewClicked() } + } + addView(pageView, MATCH_PARENT, MATCH_PARENT) + } - /** - * Pans the image. - * @param fn a function that computes the new center of the image - */ - private fun pan(fn: (PointF, SubsamplingScaleImageView) -> PointF) { - (pageView as? SubsamplingScaleImageView)?.let { view -> - - val target = fn(view.center ?: return, view) - view.animateCenter(target)!! - .withEasing(EASE_OUT_QUAD) - .withDuration(250) - .withInterruptible(true) - .start() + private fun SubsamplingScaleImageView.setupZoom(config: Config?) { + // 5x zoom + maxScale = scale * MAX_ZOOM_SCALE + setDoubleTapZoomScale(scale * 2) + + when (config?.zoomStartPosition) { + ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F)) + ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F)) + ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center) + null -> {} + } } - } - private fun prepareNonAnimatedImageView() { - if (pageView is SubsamplingScaleImageView) return - removeView(pageView) - - pageView = if (isWebtoon) { - WebtoonSubsamplingImageView(context) - } else { - SubsamplingScaleImageView(context) - }.apply { - setMaxTileSize(GLUtil.maxTextureSize) - setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) - setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) - setMinimumTileDpi(180) - setOnStateChangedListener( - object : SubsamplingScaleImageView.OnStateChangedListener { - override fun onScaleChanged(newScale: Float, origin: Int) { - this@ReaderPageImageView.onScaleChanged(newScale) + private fun setNonAnimatedImage( + data: Any, + config: Config, + ) = (pageView as? SubsamplingScaleImageView)?.apply { + setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration()) + setMinimumScaleType(config.minimumScaleType) + setMinimumDpi(1) // Just so that very small image will be fit for initial load + setCropBorders(config.cropBorders) + setOnImageEventListener( + object : SubsamplingScaleImageView.DefaultOnImageEventListener() { + override fun onReady() { + setupZoom(config) + if (isVisibleOnScreen()) landscapeZoom(true) + this@ReaderPageImageView.onImageLoaded() } - override fun onCenterChanged(newCenter: PointF?, origin: Int) { - // Not used + override fun onImageLoadError(e: Exception) { + this@ReaderPageImageView.onImageLoadError() } }, ) - setOnClickListener { this@ReaderPageImageView.onViewClicked() } - } - addView(pageView, MATCH_PARENT, MATCH_PARENT) - } - - private fun SubsamplingScaleImageView.setupZoom(config: Config?) { - // 5x zoom - maxScale = scale * MAX_ZOOM_SCALE - setDoubleTapZoomScale(scale * 2) - - when (config?.zoomStartPosition) { - ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F)) - ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F)) - ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center) - null -> {} - } - } - - private fun setNonAnimatedImage( - data: Any, - config: Config, - ) = (pageView as? SubsamplingScaleImageView)?.apply { - setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration()) - setMinimumScaleType(config.minimumScaleType) - setMinimumDpi(1) // Just so that very small image will be fit for initial load - setCropBorders(config.cropBorders) - setOnImageEventListener( - object : SubsamplingScaleImageView.DefaultOnImageEventListener() { - override fun onReady() { - setupZoom(config) - if (isVisibleOnScreen()) landscapeZoom(true) - this@ReaderPageImageView.onImageLoaded() - } - override fun onImageLoadError(e: Exception) { - this@ReaderPageImageView.onImageLoadError() + if (isWebtoon) { + val request = + ImageRequest + .Builder(context) + .data(data) + .memoryCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.DISABLED) + .target( + onSuccess = { result -> + val image = result as BitmapImage + setImage(ImageSource.bitmap(image.bitmap)) + isVisible = true + }, + onError = { + this@ReaderPageImageView.onImageLoadError() + }, + ).size(ViewSizeResolver(this@ReaderPageImageView)) + .precision(Precision.INEXACT) + .cropBorders(config.cropBorders) + .customDecoder(true) + .crossfade(false) + .build() + context.imageLoader.enqueue(request) + } else { + when (data) { + is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap)) + is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream())) + else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}") } - }, - ) - - if (isWebtoon) { - val request = ImageRequest.Builder(context) - .data(data) - .memoryCachePolicy(CachePolicy.DISABLED) - .diskCachePolicy(CachePolicy.DISABLED) - .target( - onSuccess = { result -> - val image = result as BitmapImage - setImage(ImageSource.bitmap(image.bitmap)) - isVisible = true - }, - onError = { - this@ReaderPageImageView.onImageLoadError() - }, - ) - .size(ViewSizeResolver(this@ReaderPageImageView)) - .precision(Precision.INEXACT) - .cropBorders(config.cropBorders) - .customDecoder(true) - .crossfade(false) - .build() - context.imageLoader.enqueue(request) - } else { - when (data) { - is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap)) - is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream())) - else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}") + isVisible = true } - isVisible = true } - } - - private fun prepareAnimatedImageView() { - if (pageView is AppCompatImageView) return - removeView(pageView) - pageView = if (isWebtoon) { - AppCompatImageView(context) - } else { - PhotoView(context) - }.apply { - adjustViewBounds = true - - if (this is PhotoView) { - setScaleLevels(1F, 2F, MAX_ZOOM_SCALE) - // Force 2 scale levels on double tap - setOnDoubleTapListener( - object : GestureDetector.SimpleOnGestureListener() { - override fun onDoubleTap(e: MotionEvent): Boolean { - if (scale > 1F) { - setScale(1F, e.x, e.y, true) - } else { - setScale(2F, e.x, e.y, true) - } - return true + private fun prepareAnimatedImageView() { + if (pageView is AppCompatImageView) return + removeView(pageView) + + pageView = + if (isWebtoon) { + AppCompatImageView(context) + } else { + PhotoView(context) + }.apply { + adjustViewBounds = true + + if (this is PhotoView) { + setScaleLevels(1F, 2F, MAX_ZOOM_SCALE) + // Force 2 scale levels on double tap + setOnDoubleTapListener( + object : GestureDetector.SimpleOnGestureListener() { + override fun onDoubleTap(e: MotionEvent): Boolean { + if (scale > 1F) { + setScale(1F, e.x, e.y, true) + } else { + setScale(2F, e.x, e.y, true) + } + return true + } + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + this@ReaderPageImageView.onViewClicked() + return super.onSingleTapConfirmed(e) + } + }, + ) + setOnScaleChangeListener { _, _, _ -> + this@ReaderPageImageView.onScaleChanged(scale) } - - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - this@ReaderPageImageView.onViewClicked() - return super.onSingleTapConfirmed(e) - } - }, - ) - setOnScaleChangeListener { _, _, _ -> - this@ReaderPageImageView.onScaleChanged(scale) + } } - } + addView(pageView, MATCH_PARENT, MATCH_PARENT) } - addView(pageView, MATCH_PARENT, MATCH_PARENT) - } - private fun setAnimatedImage( - data: Any, - config: Config, - ) = (pageView as? AppCompatImageView)?.apply { - if (this is PhotoView) { - setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration()) - } + private fun setAnimatedImage( + data: Any, + config: Config, + ) = (pageView as? AppCompatImageView)?.apply { + if (this is PhotoView) { + setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration()) + } - val request = ImageRequest.Builder(context) - .data(data) - .memoryCachePolicy(CachePolicy.DISABLED) - .diskCachePolicy(CachePolicy.DISABLED) - .target( - onSuccess = { result -> - val drawable = result.asDrawable(context.resources) - setImageDrawable(drawable) - (drawable as? Animatable)?.start() - isVisible = true - this@ReaderPageImageView.onImageLoaded() - }, - onError = { - this@ReaderPageImageView.onImageLoadError() - }, - ) - .crossfade(false) - .build() - context.imageLoader.enqueue(request) - } + val request = + ImageRequest + .Builder(context) + .data(data) + .memoryCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.DISABLED) + .target( + onSuccess = { result -> + val drawable = result.asDrawable(context.resources) + setImageDrawable(drawable) + (drawable as? Animatable)?.start() + isVisible = true + this@ReaderPageImageView.onImageLoaded() + }, + onError = { + this@ReaderPageImageView.onImageLoadError() + }, + ).crossfade(false) + .build() + context.imageLoader.enqueue(request) + } - private fun Int.getSystemScaledDuration(): Int { - return (this * context.animatorDurationScale).toInt().coerceAtLeast(1) - } + private fun Int.getSystemScaledDuration(): Int = (this * context.animatorDurationScale).toInt().coerceAtLeast(1) + + /** + * All of the config except [zoomDuration] will only be used for non-animated image. + */ + data class Config( + val zoomDuration: Int, + val minimumScaleType: Int = SCALE_TYPE_CENTER_INSIDE, + val cropBorders: Boolean = false, + val zoomStartPosition: ZoomStartPosition = ZoomStartPosition.CENTER, + val landscapeZoom: Boolean = false, + ) - /** - * All of the config except [zoomDuration] will only be used for non-animated image. - */ - data class Config( - val zoomDuration: Int, - val minimumScaleType: Int = SCALE_TYPE_CENTER_INSIDE, - val cropBorders: Boolean = false, - val zoomStartPosition: ZoomStartPosition = ZoomStartPosition.CENTER, - val landscapeZoom: Boolean = false, - ) - - enum class ZoomStartPosition { - LEFT, CENTER, RIGHT + enum class ZoomStartPosition { + LEFT, + CENTER, + RIGHT, + } } -} private const val MAX_ZOOM_SCALE = 5F diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt index e91b375ab9..739f849b74 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt @@ -23,35 +23,38 @@ import tachiyomi.presentation.core.components.CombinedCircularProgressIndicator * * By always rotating we give the feedback to the user that the application isn't 'stuck'. */ -class ReaderProgressIndicator @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, -) : AbstractComposeView(context, attrs, defStyleAttr) { - - init { - layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.CENTER) - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) - } +class ReaderProgressIndicator + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + ) : AbstractComposeView(context, attrs, defStyleAttr) { + init { + layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.CENTER) + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + } - private var progress by mutableFloatStateOf(0f) + private var progress by mutableFloatStateOf(0f) - @Composable - override fun Content() { - TachiyomiTheme { - CombinedCircularProgressIndicator(progress = { progress }) + @Composable + override fun Content() { + TachiyomiTheme { + CombinedCircularProgressIndicator(progress = { progress }) + } } - } - fun show() { - isVisible = true - } + fun show() { + isVisible = true + } - fun hide() { - isVisible = false - } + fun hide() { + isVisible = false + } - fun setProgress(@IntRange(from = 0, to = 100) progress: Int) { - this.progress = progress / 100f + fun setProgress( + @IntRange(from = 0, to = 100) progress: Int, + ) { + this.progress = progress / 100f + } } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt index 737b3633be..dabc02aa98 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt @@ -18,56 +18,66 @@ import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import tachiyomi.domain.manga.model.Manga import tachiyomi.source.local.isLocal -class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - AbstractComposeView(context, attrs) { +class ReaderTransitionView + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + ) : AbstractComposeView(context, attrs) { + private var data: Data? by mutableStateOf(null) - private var data: Data? by mutableStateOf(null) - - init { - layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) - } + init { + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + } - fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) { - data = if (manga != null) { - Data( - transition = transition, - currChapterDownloaded = transition.from.pageLoader?.isLocal == true, - goingToChapterDownloaded = manga.isLocal() || transition.to?.chapter?.let { goingToChapter -> - downloadManager.isChapterDownloaded( - chapterName = goingToChapter.name, - chapterScanlator = goingToChapter.scanlator, - mangaTitle = manga.title, - sourceId = manga.source, - skipCache = true, + fun bind( + transition: ChapterTransition, + downloadManager: DownloadManager, + manga: Manga?, + ) { + data = + if (manga != null) { + Data( + transition = transition, + currChapterDownloaded = transition.from.pageLoader?.isLocal == true, + goingToChapterDownloaded = + manga.isLocal() || + transition.to?.chapter?.let { goingToChapter -> + downloadManager.isChapterDownloaded( + chapterName = goingToChapter.name, + chapterScanlator = goingToChapter.scanlator, + mangaTitle = manga.title, + sourceId = manga.source, + skipCache = true, + ) + } ?: false, ) - } ?: false, - ) - } else { - null + } else { + null + } } - } - @Composable - override fun Content() { - data?.let { - TachiyomiTheme { - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.bodySmall, - LocalContentColor provides MaterialTheme.colorScheme.onBackground, - ) { - ChapterTransition( - transition = it.transition, - currChapterDownloaded = it.currChapterDownloaded, - goingToChapterDownloaded = it.goingToChapterDownloaded, - ) + @Composable + override fun Content() { + data?.let { + TachiyomiTheme { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodySmall, + LocalContentColor provides MaterialTheme.colorScheme.onBackground, + ) { + ChapterTransition( + transition = it.transition, + currChapterDownloaded = it.currChapterDownloaded, + goingToChapterDownloaded = it.goingToChapterDownloaded, + ) + } } } } - } - private data class Data( - val transition: ChapterTransition, - val currChapterDownloaded: Boolean, - val goingToChapterDownloaded: Boolean, - ) -} + private data class Data( + val transition: ChapterTransition, + val currChapterDownloaded: Boolean, + val goingToChapterDownloaded: Boolean, + ) + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt index 00834563c8..1f6f30667b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters * Interface for implementing a viewer. */ interface Viewer { - /** * Returns the view this viewer uses. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt index c84f8f51b3..c96a3fedee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt @@ -10,8 +10,10 @@ import tachiyomi.core.common.preference.Preference /** * Common configuration for all viewers. */ -abstract class ViewerConfig(readerPreferences: ReaderPreferences, private val scope: CoroutineScope) { - +abstract class ViewerConfig( + readerPreferences: ReaderPreferences, + private val scope: CoroutineScope, +) { var imagePropertyChangedListener: (() -> Unit)? = null var navigationModeChangedListener: (() -> Unit)? = null @@ -46,22 +48,28 @@ abstract class ViewerConfig(readerPreferences: ReaderPreferences, private val sc protected set init { - readerPreferences.readWithLongTap() + readerPreferences + .readWithLongTap() .register({ longTapEnabled = it }) - readerPreferences.pageTransitions() + readerPreferences + .pageTransitions() .register({ usePageTransitions = it }) - readerPreferences.doubleTapAnimSpeed() + readerPreferences + .doubleTapAnimSpeed() .register({ doubleTapAnimDuration = it }) - readerPreferences.readWithVolumeKeys() + readerPreferences + .readWithVolumeKeys() .register({ volumeKeysEnabled = it }) - readerPreferences.readWithVolumeKeysInverted() + readerPreferences + .readWithVolumeKeysInverted() .register({ volumeKeysInverted = it }) - readerPreferences.alwaysShowChapterTransition() + readerPreferences + .alwaysShowChapterTransition() .register({ alwaysShowChapterTransition = it }) forceNavigationOverlay = readerPreferences.showNavigationOverlayNewUser().get() @@ -69,7 +77,8 @@ abstract class ViewerConfig(readerPreferences: ReaderPreferences, private val sc readerPreferences.showNavigationOverlayNewUser().set(false) } - readerPreferences.showNavigationOverlayOnStart() + readerPreferences + .showNavigationOverlayOnStart() .register({ navigationOverlayOnStart = it }) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt index 48e4b680b2..034d0d5855 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt @@ -9,12 +9,18 @@ import eu.kanade.tachiyomi.util.lang.invert import tachiyomi.i18n.MR abstract class ViewerNavigation { - - sealed class NavigationRegion(val nameRes: StringResource, val color: Int) { + sealed class NavigationRegion( + val nameRes: StringResource, + val color: Int, + ) { data object MENU : NavigationRegion(MR.strings.action_menu, Color.argb(0xCC, 0x95, 0x81, 0x8D)) + data object PREV : NavigationRegion(MR.strings.nav_zone_prev, Color.argb(0xCC, 0xFF, 0x77, 0x33)) + data object NEXT : NavigationRegion(MR.strings.nav_zone_next, Color.argb(0xCC, 0x84, 0xE2, 0x96)) + data object LEFT : NavigationRegion(MR.strings.nav_zone_left, Color.argb(0xCC, 0x7D, 0x11, 0x28)) + data object RIGHT : NavigationRegion(MR.strings.nav_zone_right, Color.argb(0xCC, 0xA6, 0xCF, 0xD5)) } @@ -37,9 +43,7 @@ abstract class ViewerNavigation { protected abstract var regionList: List /** Returns regions with applied inversion. */ - fun getRegions(): List { - return regionList.map { it.invert(invertMode) } - } + fun getRegions(): List = regionList.map { it.invert(invertMode) } fun getAction(pos: PointF): NavigationRegion { val x = pos.x diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/DisabledNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/DisabledNavigation.kt index e6fac74d07..99a9f14812 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/DisabledNavigation.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/DisabledNavigation.kt @@ -13,6 +13,5 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation * +---+---+---+ */ class DisabledNavigation : ViewerNavigation() { - override var regionList: List = emptyList() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/EdgeNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/EdgeNavigation.kt index ebd69ee3bb..45819e044c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/EdgeNavigation.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/EdgeNavigation.kt @@ -14,19 +14,19 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation * +---+---+---+ */ class EdgeNavigation : ViewerNavigation() { - - override var regionList: List = listOf( - Region( - rectF = RectF(0f, 0f, 0.33f, 1f), - type = NavigationRegion.NEXT, - ), - Region( - rectF = RectF(0.33f, 0.66f, 0.66f, 1f), - type = NavigationRegion.PREV, - ), - Region( - rectF = RectF(0.66f, 0f, 1f, 1f), - type = NavigationRegion.NEXT, - ), - ) + override var regionList: List = + listOf( + Region( + rectF = RectF(0f, 0f, 0.33f, 1f), + type = NavigationRegion.NEXT, + ), + Region( + rectF = RectF(0.33f, 0.66f, 0.66f, 1f), + type = NavigationRegion.PREV, + ), + Region( + rectF = RectF(0.66f, 0f, 1f, 1f), + type = NavigationRegion.NEXT, + ), + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/KindlishNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/KindlishNavigation.kt index 9b6fa362a1..509f2b29fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/KindlishNavigation.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/KindlishNavigation.kt @@ -14,15 +14,15 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation * +---+---+---+ */ class KindlishNavigation : ViewerNavigation() { - - override var regionList: List = listOf( - Region( - rectF = RectF(0.33f, 0.33f, 1f, 1f), - type = NavigationRegion.NEXT, - ), - Region( - rectF = RectF(0f, 0.33f, 0.33f, 1f), - type = NavigationRegion.PREV, - ), - ) + override var regionList: List = + listOf( + Region( + rectF = RectF(0.33f, 0.33f, 1f, 1f), + type = NavigationRegion.NEXT, + ), + Region( + rectF = RectF(0f, 0.33f, 0.33f, 1f), + type = NavigationRegion.PREV, + ), + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/LNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/LNavigation.kt index c2a751fb32..bc6334faea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/LNavigation.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/LNavigation.kt @@ -14,23 +14,23 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation * +---+---+---+ */ open class LNavigation : ViewerNavigation() { - - override var regionList: List = listOf( - Region( - rectF = RectF(0f, 0.33f, 0.33f, 0.66f), - type = NavigationRegion.PREV, - ), - Region( - rectF = RectF(0f, 0f, 1f, 0.33f), - type = NavigationRegion.PREV, - ), - Region( - rectF = RectF(0.66f, 0.33f, 1f, 0.66f), - type = NavigationRegion.NEXT, - ), - Region( - rectF = RectF(0f, 0.66f, 1f, 1f), - type = NavigationRegion.NEXT, - ), - ) + override var regionList: List = + listOf( + Region( + rectF = RectF(0f, 0.33f, 0.33f, 0.66f), + type = NavigationRegion.PREV, + ), + Region( + rectF = RectF(0f, 0f, 1f, 0.33f), + type = NavigationRegion.PREV, + ), + Region( + rectF = RectF(0.66f, 0.33f, 1f, 0.66f), + type = NavigationRegion.NEXT, + ), + Region( + rectF = RectF(0f, 0.66f, 1f, 1f), + type = NavigationRegion.NEXT, + ), + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/RightAndLeftNavigation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/RightAndLeftNavigation.kt index 4c9bc6a77b..1bf1776152 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/RightAndLeftNavigation.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/navigation/RightAndLeftNavigation.kt @@ -14,15 +14,15 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation * +---+---+---+ */ class RightAndLeftNavigation : ViewerNavigation() { - - override var regionList: List = listOf( - Region( - rectF = RectF(0f, 0f, 0.33f, 1f), - type = NavigationRegion.LEFT, - ), - Region( - rectF = RectF(0.66f, 0f, 1f, 1f), - type = NavigationRegion.RIGHT, - ), - ) + override var regionList: List = + listOf( + Region( + rectF = RectF(0f, 0f, 0.33f, 1f), + type = NavigationRegion.LEFT, + ), + Region( + rectF = RectF(0.66f, 0f, 1f, 1f), + type = NavigationRegion.RIGHT, + ), + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt index 87c1e1678d..711cec3d3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt @@ -16,7 +16,6 @@ open class Pager( context: Context, isHorizontal: Boolean = true, ) : DirectionalViewPager(context, isHorizontal) { - /** * Tap listener function to execute when a tap is detected. */ @@ -30,19 +29,20 @@ open class Pager( /** * Gesture listener that implements tap and long tap events. */ - private val gestureListener = object : GestureDetectorWithLongTap.Listener() { - override fun onSingleTapConfirmed(ev: MotionEvent): Boolean { - tapListener?.invoke(ev) - return true - } + private val gestureListener = + object : GestureDetectorWithLongTap.Listener() { + override fun onSingleTapConfirmed(ev: MotionEvent): Boolean { + tapListener?.invoke(ev) + return true + } - override fun onLongTapConfirmed(ev: MotionEvent) { - val listener = longTapListener - if (listener != null && listener.invoke(ev)) { - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + override fun onLongTapConfirmed(ev: MotionEvent) { + val listener = longTapListener + if (listener != null && listener.invoke(ev)) { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } } } - } /** * Gesture detector which handles motion events. @@ -69,20 +69,19 @@ open class Pager( * Whether the given [ev] should be intercepted. Only used to prevent crashes when child * views manipulate [requestDisallowInterceptTouchEvent]. */ - override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { - return try { + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = + try { super.onInterceptTouchEvent(ev) } catch (e: IllegalArgumentException) { false } - } /** * Handles a touch event. Only used to prevent crashes when child views manipulate * [requestDisallowInterceptTouchEvent]. */ - override fun onTouchEvent(ev: MotionEvent): Boolean { - return try { + override fun onTouchEvent(ev: MotionEvent): Boolean = + try { super.onTouchEvent(ev) } catch (e: NullPointerException) { false @@ -91,7 +90,6 @@ open class Pager( } catch (e: IllegalArgumentException) { false } - } /** * Executes the given key event when this pager has focus. Just do nothing because the reader diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt index b319fd9287..6eb957b5db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt @@ -24,7 +24,6 @@ class PagerConfig( scope: CoroutineScope, readerPreferences: ReaderPreferences = Injekt.get(), ) : ViewerConfig(readerPreferences, scope) { - var theme = readerPreferences.readerTheme().get() private set @@ -49,7 +48,8 @@ class PagerConfig( private set init { - readerPreferences.readerTheme() + readerPreferences + .readerTheme() .register( { theme = it @@ -58,32 +58,42 @@ class PagerConfig( { imagePropertyChangedListener?.invoke() }, ) - readerPreferences.imageScaleType() + readerPreferences + .imageScaleType() .register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.zoomStart() + readerPreferences + .zoomStart() .register({ zoomTypeFromPreference(it) }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.cropBorders() + readerPreferences + .cropBorders() .register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.navigateToPan() + readerPreferences + .navigateToPan() .register({ navigateToPan = it }) - readerPreferences.landscapeZoom() + readerPreferences + .landscapeZoom() .register({ landscapeZoom = it }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.navigationModePager() + readerPreferences + .navigationModePager() .register({ navigationMode = it }, { updateNavigation(navigationMode) }) - readerPreferences.pagerNavInverted() + readerPreferences + .pagerNavInverted() .register({ tappingInverted = it }, { navigator.invertMode = it }) - readerPreferences.pagerNavInverted().changes() + readerPreferences + .pagerNavInverted() + .changes() .drop(1) .onEach { navigationModeChangedListener?.invoke() } .launchIn(scope) - readerPreferences.dualPageSplitPaged() + readerPreferences + .dualPageSplitPaged() .register( { dualPageSplit = it }, { @@ -92,16 +102,19 @@ class PagerConfig( }, ) - readerPreferences.dualPageInvertPaged() + readerPreferences + .dualPageInvertPaged() .register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.dualPageRotateToFit() + readerPreferences + .dualPageRotateToFit() .register( { dualPageRotateToFit = it }, { imagePropertyChangedListener?.invoke() }, ) - readerPreferences.dualPageRotateToFitInvert() + readerPreferences + .dualPageRotateToFitInvert() .register( { dualPageRotateToFitInvert = it }, { imagePropertyChangedListener?.invoke() }, @@ -109,20 +122,22 @@ class PagerConfig( } private fun zoomTypeFromPreference(value: Int) { - imageZoomType = when (value) { - // Auto - 1 -> when (viewer) { - is L2RPagerViewer -> ReaderPageImageView.ZoomStartPosition.LEFT - is R2LPagerViewer -> ReaderPageImageView.ZoomStartPosition.RIGHT + imageZoomType = + when (value) { + // Auto + 1 -> + when (viewer) { + is L2RPagerViewer -> ReaderPageImageView.ZoomStartPosition.LEFT + is R2LPagerViewer -> ReaderPageImageView.ZoomStartPosition.RIGHT + else -> ReaderPageImageView.ZoomStartPosition.CENTER + } + // Left + 2 -> ReaderPageImageView.ZoomStartPosition.LEFT + // Right + 3 -> ReaderPageImageView.ZoomStartPosition.RIGHT + // Center else -> ReaderPageImageView.ZoomStartPosition.CENTER } - // Left - 2 -> ReaderPageImageView.ZoomStartPosition.LEFT - // Right - 3 -> ReaderPageImageView.ZoomStartPosition.RIGHT - // Center - else -> ReaderPageImageView.ZoomStartPosition.CENTER - } } override var navigator: ViewerNavigation = defaultNavigation() @@ -130,23 +145,23 @@ class PagerConfig( field = value.also { it.invertMode = this.tappingInverted } } - override fun defaultNavigation(): ViewerNavigation { - return when (viewer) { + override fun defaultNavigation(): ViewerNavigation = + when (viewer) { is VerticalPagerViewer -> LNavigation() else -> RightAndLeftNavigation() } - } override fun updateNavigation(navigationMode: Int) { - navigator = when (navigationMode) { - 0 -> defaultNavigation() - 1 -> LNavigation() - 2 -> KindlishNavigation() - 3 -> EdgeNavigation() - 4 -> RightAndLeftNavigation() - 5 -> DisabledNavigation() - else -> defaultNavigation() - } + navigator = + when (navigationMode) { + 0 -> defaultNavigation() + 1 -> LNavigation() + 2 -> KindlishNavigation() + 3 -> EdgeNavigation() + 4 -> RightAndLeftNavigation() + 5 -> DisabledNavigation() + else -> defaultNavigation() + } navigationModeChangedListener?.invoke() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 71d499c38b..cdaa6d20fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -34,8 +34,8 @@ class PagerPageHolder( readerThemedContext: Context, val viewer: PagerViewer, val page: ReaderPage, -) : ReaderPageImageView(readerThemedContext), ViewPagerAdapter.PositionableView { - +) : ReaderPageImageView(readerThemedContext), + ViewPagerAdapter.PositionableView { /** * Item that identifies this view. Needed by the adapter to not recreate views. */ @@ -138,16 +138,18 @@ class PagerPageHolder( val streamFn = page.stream ?: return try { - val (source, isAnimated, background) = withIOContext { - val source = streamFn().use { process(item, Buffer().readFrom(it)) } - val isAnimated = ImageUtil.isAnimatedAndSupported(source) - val background = if (!isAnimated && viewer.config.automaticBackground) { - ImageUtil.chooseBackground(context, source.peek().inputStream()) - } else { - null + val (source, isAnimated, background) = + withIOContext { + val source = streamFn().use { process(item, Buffer().readFrom(it)) } + val isAnimated = ImageUtil.isAnimatedAndSupported(source) + val background = + if (!isAnimated && viewer.config.automaticBackground) { + ImageUtil.chooseBackground(context, source.peek().inputStream()) + } else { + null + } + Triple(source, isAnimated, background) } - Triple(source, isAnimated, background) - } withUIContext { setImage( source, @@ -173,7 +175,10 @@ class PagerPageHolder( } } - private fun process(page: ReaderPage, imageSource: BufferedSource): BufferedSource { + private fun process( + page: ReaderPage, + imageSource: BufferedSource, + ): BufferedSource { if (viewer.config.dualPageRotateToFit) { return rotateDualPage(imageSource) } @@ -207,19 +212,21 @@ class PagerPageHolder( } private fun splitInHalf(imageSource: BufferedSource): BufferedSource { - var side = when { - viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT - viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT - viewer is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.LEFT - viewer !is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.RIGHT - else -> error("We should choose a side!") - } + var side = + when { + viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT + viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT + viewer is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.LEFT + viewer !is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.RIGHT + else -> error("We should choose a side!") + } if (viewer.config.dualPageInvert) { - side = when (side) { - ImageUtil.Side.RIGHT -> ImageUtil.Side.LEFT - ImageUtil.Side.LEFT -> ImageUtil.Side.RIGHT - } + side = + when (side) { + ImageUtil.Side.RIGHT -> ImageUtil.Side.LEFT + ImageUtil.Side.LEFT -> ImageUtil.Side.RIGHT + } } return ImageUtil.splitInHalf(imageSource, side) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index 4569f43700..9785424d2f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -31,8 +31,8 @@ class PagerTransitionHolder( readerThemedContext: Context, val viewer: PagerViewer, val transition: ChapterTransition, -) : LinearLayout(readerThemedContext), ViewPagerAdapter.PositionableView { - +) : LinearLayout(readerThemedContext), + ViewPagerAdapter.PositionableView { private val scope = MainScope() private var stateJob: Job? = null @@ -46,11 +46,12 @@ class PagerTransitionHolder( * View container of the current status of the transition page. Child views will be added * dynamically. */ - private var pagesContainer = LinearLayout(context).apply { - layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT) - orientation = VERTICAL - gravity = Gravity.CENTER - } + private var pagesContainer = + LinearLayout(context).apply { + layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT) + orientation = VERTICAL + gravity = Gravity.CENTER + } init { orientation = VERTICAL @@ -81,19 +82,20 @@ class PagerTransitionHolder( */ private fun observeStatus(chapter: ReaderChapter) { stateJob?.cancel() - stateJob = scope.launch { - chapter.stateFlow - .collectLatest { state -> - pagesContainer.removeAllViews() - when (state) { - is ReaderChapter.State.Loading -> setLoading() - is ReaderChapter.State.Error -> setError(state.error) - is ReaderChapter.State.Wait, is ReaderChapter.State.Loaded -> { - // No additional view is added + stateJob = + scope.launch { + chapter.stateFlow + .collectLatest { state -> + pagesContainer.removeAllViews() + when (state) { + is ReaderChapter.State.Loading -> setLoading() + is ReaderChapter.State.Error -> setError(state.error) + is ReaderChapter.State.Wait, is ReaderChapter.State.Loaded -> { + // No additional view is added + } } } - } - } + } } /** @@ -103,10 +105,11 @@ class PagerTransitionHolder( val progress = CircularProgressIndicator(context) progress.isIndeterminate = true - val textView = AppCompatTextView(context).apply { - wrapContent() - text = context.stringResource(MR.strings.transition_pages_loading) - } + val textView = + AppCompatTextView(context).apply { + wrapContent() + text = context.stringResource(MR.strings.transition_pages_loading) + } pagesContainer.addView(progress) pagesContainer.addView(textView) @@ -116,22 +119,24 @@ class PagerTransitionHolder( * Sets the error state on the pages container. */ private fun setError(error: Throwable) { - val textView = AppCompatTextView(context).apply { - wrapContent() - text = context.stringResource(MR.strings.transition_pages_error, error.message ?: "") - } + val textView = + AppCompatTextView(context).apply { + wrapContent() + text = context.stringResource(MR.strings.transition_pages_error, error.message ?: "") + } - val retryBtn = ReaderButton(context).apply { - viewer = this@PagerTransitionHolder.viewer - wrapContent() - text = context.stringResource(MR.strings.action_retry) - setOnClickListener { - val toChapter = transition.to - if (toChapter != null) { - this@PagerTransitionHolder.viewer.activity.requestPreloadChapter(toChapter) + val retryBtn = + ReaderButton(context).apply { + viewer = this@PagerTransitionHolder.viewer + wrapContent() + text = context.stringResource(MR.strings.action_retry) + setOnClickListener { + val toChapter = transition.to + if (toChapter != null) { + this@PagerTransitionHolder.viewer.activity.requestPreloadChapter(toChapter) + } } } - } pagesContainer.addView(textView) pagesContainer.addView(retryBtn) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index ffa38bfbaf..a5698661fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -29,8 +29,9 @@ import kotlin.math.min * Implementation of a [Viewer] to display pages with a [ViewPager]. */ @Suppress("LeakingThis") -abstract class PagerViewer(val activity: ReaderActivity) : Viewer { - +abstract class PagerViewer( + val activity: ReaderActivity, +) : Viewer { val downloadManager: DownloadManager by injectLazy() private val scope = MainScope() @@ -106,10 +107,11 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { pager.getLocationOnScreen(viewPosition) val viewPositionRelativeToWindow = IntArray(2) pager.getLocationInWindow(viewPositionRelativeToWindow) - val pos = PointF( - (event.rawX - viewPosition[0] + viewPositionRelativeToWindow[0]) / pager.width, - (event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / pager.height, - ) + val pos = + PointF( + (event.rawX - viewPosition[0] + viewPositionRelativeToWindow[0]) / pager.width, + (event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / pager.height, + ) when (config.navigator.getAction(pos)) { NavigationRegion.MENU -> activity.toggleMenu() NavigationRegion.NEXT -> moveToNext() @@ -158,9 +160,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { /** * Returns the view this viewer uses. */ - override fun getView(): View { - return pager - } + override fun getView(): View = pager /** * Returns the PagerPageHolder for the provided page @@ -177,20 +177,21 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { val page = adapter.items.getOrNull(position) if (page != null && currentPage != page) { val allowPreload = checkAllowPreload(page as? ReaderPage) - val forward = when { - currentPage is ReaderPage && page is ReaderPage -> { - // if both pages have the same number, it's a split page with an InsertPage - if (page.number == (currentPage as ReaderPage).number) { - // the InsertPage is always the second in the reading direction - page is InsertPage - } else { - page.number > (currentPage as ReaderPage).number + val forward = + when { + currentPage is ReaderPage && page is ReaderPage -> { + // if both pages have the same number, it's a split page with an InsertPage + if (page.number == (currentPage as ReaderPage).number) { + // the InsertPage is always the second in the reading direction + page is InsertPage + } else { + page.number > (currentPage as ReaderPage).number + } } + currentPage is ChapterTransition.Prev && page is ReaderPage -> + false + else -> true } - currentPage is ChapterTransition.Prev && page is ReaderPage -> - false - else -> true - } currentPage = page when (page) { is ReaderPage -> onReaderPageSelected(page, allowPreload, forward) @@ -222,7 +223,11 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { * Called when a [ReaderPage] is marked as active. It notifies the * activity of the change and requests the preload of the next chapter if this is the last page. */ - private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean, forward: Boolean) { + private fun onReaderPageSelected( + page: ReaderPage, + allowPreload: Boolean, + forward: Boolean, + ) { val pages = page.chapter.pages ?: return logcat { "onReaderPageSelected: ${page.number}/${pages.size}" } activity.onPageSelected(page) @@ -275,8 +280,9 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { * Sets the active [chapters] on this pager. */ private fun setChaptersInternal(chapters: ViewerChapters) { - val forceTransition = config.alwaysShowChapterTransition || - adapter.items.getOrNull(pager.currentItem) is ChapterTransition + val forceTransition = + config.alwaysShowChapterTransition || + adapter.items.getOrNull(pager.currentItem) is ChapterTransition adapter.setChapters(chapters, forceTransition) // Layout the pager once a chapter is being set @@ -435,7 +441,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { return false } - fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) { + fun onPageSplit( + currentPage: ReaderPage, + newPage: InsertPage, + ) { activity.runOnUiThread { // Need to insert on UI thread else images will go blank adapter.onPageSplit(currentPage, newPage) 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 5ff4ecaae8..8af63cde2d 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 @@ -15,8 +15,9 @@ import tachiyomi.core.common.util.system.logcat /** * Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted. */ -class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { - +class PagerViewerAdapter( + private val viewer: PagerViewer, +) : ViewPagerAdapter() { /** * List of currently set items. */ @@ -44,7 +45,10 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { * next/previous chapter to allow seamless transitions and inverting the pages if the viewer * has R2L direction. */ - fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) { + fun setChapters( + chapters: ViewerChapters, + forceTransition: Boolean, + ) { val newItems = mutableListOf() // Forces chapter transition if there is missing chapters @@ -76,7 +80,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { val lastPage = pages.last() // Insert preprocessed pages into current page list - preprocessed.keys.sortedDescending() + preprocessed.keys + .sortedDescending() .forEach { key -> if (lastPage.index == key) { insertPageLastPage = preprocessed[key] @@ -90,14 +95,17 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { currentChapter = chapters.currChapter // Add next chapter transition and pages. - nextTransition = ChapterTransition.Next(chapters.currChapter, chapters.nextChapter) - .also { - if (nextHasMissingChapters || forceTransition || - chapters.nextChapter?.state !is ReaderChapter.State.Loaded - ) { - newItems.add(it) + nextTransition = + ChapterTransition + .Next(chapters.currChapter, chapters.nextChapter) + .also { + if (nextHasMissingChapters || + forceTransition || + chapters.nextChapter?.state !is ReaderChapter.State.Loaded + ) { + newItems.add(it) + } } - } if (chapters.nextChapter != null) { // Add at most two pages, because this chapter will be selected before the user can @@ -128,20 +136,20 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { /** * Returns the amount of items of the adapter. */ - override fun getCount(): Int { - return items.size - } + override fun getCount(): Int = items.size /** * Creates a new view for the item at the given [position]. */ - override fun createView(container: ViewGroup, position: Int): View { - return when (val item = items[position]) { + override fun createView( + container: ViewGroup, + position: Int, + ): View = + when (val item = items[position]) { is ReaderPage -> PagerPageHolder(readerThemedContext, viewer, item) is ChapterTransition -> PagerTransitionHolder(readerThemedContext, viewer, item) else -> throw NotImplementedError("Holder for ${item.javaClass} not implemented") } - } /** * Returns the current position of the given [view] on the adapter. @@ -158,7 +166,10 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { return POSITION_NONE } - fun onPageSplit(currentPage: Any?, newPage: InsertPage) { + fun onPageSplit( + currentPage: Any?, + newPage: InsertPage, + ) { if (currentPage !is ReaderPage) return val currentIndex = items.indexOf(currentPage) @@ -169,12 +180,13 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { return } - val placeAtIndex = when (viewer) { - is L2RPagerViewer, - is VerticalPagerViewer, - -> currentIndex + 1 - else -> currentIndex - } + val placeAtIndex = + when (viewer) { + is L2RPagerViewer, + is VerticalPagerViewer, + -> currentIndex + 1 + else -> currentIndex + } // It will enter a endless cycle of insert pages if (viewer is R2LPagerViewer && placeAtIndex - 1 >= 0 && items[placeAtIndex - 1] is InsertPage) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt index a79f858142..9da1b1c090 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt @@ -5,25 +5,25 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity /** * Implementation of a left to right PagerViewer. */ -class L2RPagerViewer(activity: ReaderActivity) : PagerViewer(activity) { +class L2RPagerViewer( + activity: ReaderActivity, +) : PagerViewer(activity) { /** * Creates a new left to right pager. */ - override fun createPager(): Pager { - return Pager(activity) - } + override fun createPager(): Pager = Pager(activity) } /** * Implementation of a right to left PagerViewer. */ -class R2LPagerViewer(activity: ReaderActivity) : PagerViewer(activity) { +class R2LPagerViewer( + activity: ReaderActivity, +) : PagerViewer(activity) { /** * Creates a new right to left pager. */ - override fun createPager(): Pager { - return Pager(activity) - } + override fun createPager(): Pager = Pager(activity) /** * Moves to the next page. On a R2L pager the next page is the one at the left. @@ -43,11 +43,11 @@ class R2LPagerViewer(activity: ReaderActivity) : PagerViewer(activity) { /** * Implementation of a vertical (top to bottom) PagerViewer. */ -class VerticalPagerViewer(activity: ReaderActivity) : PagerViewer(activity) { +class VerticalPagerViewer( + activity: ReaderActivity, +) : PagerViewer(activity) { /** * Creates a new vertical pager. */ - override fun createPager(): Pager { - return Pager(activity, isHorizontal = false) - } + override fun createPager(): Pager = Pager(activity, isHorizontal = false) } 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 036f767f8c..ba94818f6e 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 @@ -15,8 +15,9 @@ import eu.kanade.tachiyomi.util.system.createReaderThemeContext /** * RecyclerView Adapter used by this [viewer] to where [ViewerChapters] updates are posted. */ -class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter() { - +class WebtoonAdapter( + val viewer: WebtoonViewer, +) : RecyclerView.Adapter() { /** * List of currently set items. */ @@ -35,7 +36,10 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter() // Forces chapter transition if there is missing chapters @@ -95,26 +99,26 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter PAGE_VIEW is ChapterTransition -> TRANSITION_VIEW else -> error("Unknown view type for ${item.javaClass}") } - } /** * Creates a new view holder for an item with the given [viewType]. */ - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): RecyclerView.ViewHolder = + when (viewType) { PAGE_VIEW -> { val view = ReaderPageImageView(readerThemedContext, isWebtoon = true) WebtoonPageHolder(view, viewer) @@ -125,12 +129,14 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter error("Unknown view type") } - } /** * Binds an existing view [holder] with the item at the given [position]. */ - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + ) { val item = items[position] when (holder) { is WebtoonPageHolder -> holder.bind(item as ReaderPage) @@ -155,11 +161,13 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter, private val newItems: List, ) : DiffUtil.Callback() { - /** * Returns true if these two items are the same. */ - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + override fun areItemsTheSame( + oldItemPosition: Int, + newItemPosition: Int, + ): Boolean { val oldItem = oldItems[oldItemPosition] val newItem = newItems[newItemPosition] @@ -169,23 +177,20 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter Unit)? = null var imageCropBorders = false @@ -45,53 +44,67 @@ class WebtoonConfig( val theme = readerPreferences.readerTheme().get() init { - readerPreferences.cropBordersWebtoon() + readerPreferences + .cropBordersWebtoon() .register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.webtoonSidePadding() + readerPreferences + .webtoonSidePadding() .register({ sidePadding = it }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.navigationModeWebtoon() + readerPreferences + .navigationModeWebtoon() .register({ navigationMode = it }, { updateNavigation(it) }) - readerPreferences.webtoonNavInverted() + readerPreferences + .webtoonNavInverted() .register({ tappingInverted = it }, { navigator.invertMode = it }) - readerPreferences.webtoonNavInverted().changes() + readerPreferences + .webtoonNavInverted() + .changes() .drop(1) .onEach { navigationModeChangedListener?.invoke() } .launchIn(scope) - readerPreferences.dualPageSplitWebtoon() + readerPreferences + .dualPageSplitWebtoon() .register({ dualPageSplit = it }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.dualPageInvertWebtoon() + readerPreferences + .dualPageInvertWebtoon() .register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.dualPageRotateToFitWebtoon() + readerPreferences + .dualPageRotateToFitWebtoon() .register( { dualPageRotateToFit = it }, { imagePropertyChangedListener?.invoke() }, ) - readerPreferences.dualPageRotateToFitInvertWebtoon() + readerPreferences + .dualPageRotateToFitInvertWebtoon() .register( { dualPageRotateToFitInvert = it }, { imagePropertyChangedListener?.invoke() }, ) - readerPreferences.webtoonDisableZoomOut() + readerPreferences + .webtoonDisableZoomOut() .register( { zoomOutDisabled = it }, - { zoomPropertyChangedListener?.invoke(it) } + { zoomPropertyChangedListener?.invoke(it) }, ) - readerPreferences.webtoonDoubleTapZoomEnabled() + readerPreferences + .webtoonDoubleTapZoomEnabled() .register( { doubleTapZoom = it }, { doubleTapZoomChangedListener?.invoke(it) }, ) - readerPreferences.readerTheme().changes() + readerPreferences + .readerTheme() + .changes() .drop(1) .distinctUntilChanged() .onEach { themeChangedListener?.invoke() } @@ -103,20 +116,19 @@ class WebtoonConfig( field = value.also { it.invertMode = tappingInverted } } - override fun defaultNavigation(): ViewerNavigation { - return LNavigation() - } + override fun defaultNavigation(): ViewerNavigation = LNavigation() override fun updateNavigation(navigationMode: Int) { - this.navigator = when (navigationMode) { - 0 -> defaultNavigation() - 1 -> LNavigation() - 2 -> KindlishNavigation() - 3 -> EdgeNavigation() - 4 -> RightAndLeftNavigation() - 5 -> DisabledNavigation() - else -> defaultNavigation() - } + this.navigator = + when (navigationMode) { + 0 -> defaultNavigation() + 1 -> LNavigation() + 2 -> KindlishNavigation() + 3 -> EdgeNavigation() + 4 -> RightAndLeftNavigation() + 5 -> DisabledNavigation() + else -> defaultNavigation() + } navigationModeChangedListener?.invoke() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt index 17aafc6fd1..6224f13d56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt @@ -14,8 +14,9 @@ import android.widget.FrameLayout * * TODO consider integrating this class into [WebtoonViewer]. */ -class WebtoonFrame(context: Context) : FrameLayout(context) { - +class WebtoonFrame( + context: Context, +) : FrameLayout(context) { /** * Scale detector, either with pinch or quick scale. */ @@ -93,17 +94,13 @@ class WebtoonFrame(context: Context) : FrameLayout(context) { * Fling listener used to delegate events to the recycler view. */ inner class FlingListener : GestureDetector.SimpleOnGestureListener() { - override fun onDown(e: MotionEvent): Boolean { - return true - } + override fun onDown(e: MotionEvent): Boolean = true override fun onFling( e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float, - ): Boolean { - return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false - } + ): Boolean = recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt index 852cd76e12..4d3a23edd0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt @@ -13,8 +13,9 @@ import androidx.recyclerview.widget.RecyclerView.NO_POSITION * This layout manager uses the same package name as the support library in order to use a package * protected method. */ -class WebtoonLayoutManager(context: Context) : LinearLayoutManager(context) { - +class WebtoonLayoutManager( + context: Context, +) : LinearLayoutManager(context) { /** * Extra layout space is set to half the screen height. */ @@ -27,20 +28,19 @@ class WebtoonLayoutManager(context: Context) : LinearLayoutManager(context) { /** * Returns the custom extra layout space. */ - override fun getExtraLayoutSpace(state: RecyclerView.State): Int { - return extraLayoutSpace - } + override fun getExtraLayoutSpace(state: RecyclerView.State): Int = extraLayoutSpace /** * Returns the position of the last item whose end side is visible on screen. */ fun findLastEndVisibleItemPosition(): Int { ensureLayoutState() - val callback = if (mOrientation == HORIZONTAL) { - mHorizontalBoundCheck - } else { - mVerticalBoundCheck - }.mCallback + val callback = + if (mOrientation == HORIZONTAL) { + mHorizontalBoundCheck + } else { + mVerticalBoundCheck + }.mCallback val start = callback.parentStart val end = callback.parentEnd for (i in childCount - 1 downTo 0) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 488db7bb6a..ea7c4ce141 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -42,7 +42,6 @@ class WebtoonPageHolder( private val frame: ReaderPageImageView, viewer: WebtoonViewer, ) : WebtoonBaseHolder(frame, viewer) { - /** * Loading progress bar to indicate the current progress. */ @@ -96,15 +95,16 @@ class WebtoonPageHolder( } private fun refreshLayoutParams() { - frame.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { - if (!viewer.isContinuous) { - bottomMargin = 15.dpToPx - } + frame.layoutParams = + FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { + if (!viewer.isContinuous) { + bottomMargin = 15.dpToPx + } - val margin = Resources.getSystem().displayMetrics.widthPixels * (viewer.config.sidePadding / 100f) - marginEnd = margin.toInt() - marginStart = margin.toInt() - } + val margin = Resources.getSystem().displayMetrics.widthPixels * (viewer.config.sidePadding / 100f) + marginEnd = margin.toInt() + marginStart = margin.toInt() + } } /** @@ -187,11 +187,12 @@ class WebtoonPageHolder( val streamFn = page?.stream ?: return try { - val (source, isAnimated) = withIOContext { - val source = streamFn().use { process(Buffer().readFrom(it)) } - val isAnimated = ImageUtil.isAnimatedAndSupported(source) - Pair(source, isAnimated) - } + val (source, isAnimated) = + withIOContext { + val source = streamFn().use { process(Buffer().readFrom(it)) } + val isAnimated = ImageUtil.isAnimatedAndSupported(source) + Pair(source, isAnimated) + } withUIContext { frame.setImage( source, @@ -261,11 +262,12 @@ class WebtoonPageHolder( progressContainer = FrameLayout(context) frame.addView(progressContainer, MATCH_PARENT, parentHeight) - val progress = ReaderProgressIndicator(context).apply { - updateLayoutParams { - updateMargins(top = parentHeight / 4) + val progress = + ReaderProgressIndicator(context).apply { + updateLayoutParams { + updateMargins(top = parentHeight / 4) + } } - } progressContainer.addView(progress) return progress } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt index fe232fe878..447c09577b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt @@ -17,319 +17,340 @@ import kotlin.math.abs /** * Implementation of a [RecyclerView] used by the webtoon reader. */ -class WebtoonRecyclerView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0, -) : RecyclerView(context, attrs, defStyle) { - - private var isZooming = false - private var atLastPosition = false - private var atFirstPosition = false - private var halfWidth = 0 - private var halfHeight = 0 - var originalHeight = 0 - private set - private var heightSet = false - private var firstVisibleItemPosition = 0 - private var lastVisibleItemPosition = 0 - private var currentScale = DEFAULT_RATE - var zoomOutDisabled = false - set(value) { - field = value - if (value && currentScale < DEFAULT_RATE) { - zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f) +class WebtoonRecyclerView + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, + ) : RecyclerView(context, attrs, defStyle) { + private var isZooming = false + private var atLastPosition = false + private var atFirstPosition = false + private var halfWidth = 0 + private var halfHeight = 0 + var originalHeight = 0 + private set + private var heightSet = false + private var firstVisibleItemPosition = 0 + private var lastVisibleItemPosition = 0 + private var currentScale = DEFAULT_RATE + var zoomOutDisabled = false + set(value) { + field = value + if (value && currentScale < DEFAULT_RATE) { + zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f) + } + } + private val minRate + get() = if (zoomOutDisabled) DEFAULT_RATE else MIN_RATE + + private val listener = GestureListener() + private val detector = Detector() + + var doubleTapZoom = true + + var tapListener: ((MotionEvent) -> Unit)? = null + var longTapListener: ((MotionEvent) -> Boolean)? = null + + override fun onMeasure( + widthSpec: Int, + heightSpec: Int, + ) { + halfWidth = MeasureSpec.getSize(widthSpec) / 2 + halfHeight = MeasureSpec.getSize(heightSpec) / 2 + if (!heightSet) { + originalHeight = MeasureSpec.getSize(heightSpec) + heightSet = true } + super.onMeasure(widthSpec, heightSpec) } - private val minRate - get() = if (zoomOutDisabled) DEFAULT_RATE else MIN_RATE - - private val listener = GestureListener() - private val detector = Detector() - - var doubleTapZoom = true - var tapListener: ((MotionEvent) -> Unit)? = null - var longTapListener: ((MotionEvent) -> Boolean)? = null - - override fun onMeasure(widthSpec: Int, heightSpec: Int) { - halfWidth = MeasureSpec.getSize(widthSpec) / 2 - halfHeight = MeasureSpec.getSize(heightSpec) / 2 - if (!heightSet) { - originalHeight = MeasureSpec.getSize(heightSpec) - heightSet = true + override fun onTouchEvent(e: MotionEvent): Boolean { + detector.onTouchEvent(e) + return super.onTouchEvent(e) } - super.onMeasure(widthSpec, heightSpec) - } - - override fun onTouchEvent(e: MotionEvent): Boolean { - detector.onTouchEvent(e) - return super.onTouchEvent(e) - } - override fun onScrolled(dx: Int, dy: Int) { - super.onScrolled(dx, dy) - val layoutManager = layoutManager - lastVisibleItemPosition = - (layoutManager as LinearLayoutManager).findLastVisibleItemPosition() - firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() - } - - override fun onScrollStateChanged(state: Int) { - super.onScrollStateChanged(state) - val layoutManager = layoutManager - val visibleItemCount = layoutManager?.childCount ?: 0 - val totalItemCount = layoutManager?.itemCount ?: 0 - atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1 - atFirstPosition = firstVisibleItemPosition == 0 - } - - private fun getPositionX(positionX: Float): Float { - if (currentScale < 1) { - return 0f + override fun onScrolled( + dx: Int, + dy: Int, + ) { + super.onScrolled(dx, dy) + val layoutManager = layoutManager + lastVisibleItemPosition = + (layoutManager as LinearLayoutManager).findLastVisibleItemPosition() + firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() } - val maxPositionX = halfWidth * (currentScale - 1) - return positionX.coerceIn(-maxPositionX, maxPositionX) - } - private fun getPositionY(positionY: Float): Float { - if (currentScale < 1) { - return (originalHeight / 2 - halfHeight).toFloat() + override fun onScrollStateChanged(state: Int) { + super.onScrollStateChanged(state) + val layoutManager = layoutManager + val visibleItemCount = layoutManager?.childCount ?: 0 + val totalItemCount = layoutManager?.itemCount ?: 0 + atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1 + atFirstPosition = firstVisibleItemPosition == 0 } - val maxPositionY = halfHeight * (currentScale - 1) - return positionY.coerceIn(-maxPositionY, maxPositionY) - } - private fun zoom( - fromRate: Float, - toRate: Float, - fromX: Float, - toX: Float, - fromY: Float, - toY: Float, - ) { - isZooming = true - val animatorSet = AnimatorSet() - val translationXAnimator = ValueAnimator.ofFloat(fromX, toX) - translationXAnimator.addUpdateListener { animation -> x = animation.animatedValue as Float } - - val translationYAnimator = ValueAnimator.ofFloat(fromY, toY) - translationYAnimator.addUpdateListener { animation -> y = animation.animatedValue as Float } - - val scaleAnimator = ValueAnimator.ofFloat(fromRate, toRate) - scaleAnimator.addUpdateListener { animation -> - currentScale = animation.animatedValue as Float - setScaleRate(currentScale) - } - animatorSet.playTogether(translationXAnimator, translationYAnimator, scaleAnimator) - animatorSet.duration = ANIMATOR_DURATION_TIME.toLong() - animatorSet.interpolator = DecelerateInterpolator() - animatorSet.start() - animatorSet.doOnEnd { - isZooming = false - currentScale = toRate + private fun getPositionX(positionX: Float): Float { + if (currentScale < 1) { + return 0f + } + val maxPositionX = halfWidth * (currentScale - 1) + return positionX.coerceIn(-maxPositionX, maxPositionX) } - } - - fun zoomFling(velocityX: Int, velocityY: Int): Boolean { - if (currentScale <= 1f) return false - - val distanceTimeFactor = 0.4f - val animatorSet = AnimatorSet() - if (velocityX != 0) { - val dx = (distanceTimeFactor * velocityX / 2) - val newX = getPositionX(x + dx) - val translationXAnimator = ValueAnimator.ofFloat(x, newX) - translationXAnimator.addUpdateListener { animation -> x = getPositionX(animation.animatedValue as Float) } - animatorSet.play(translationXAnimator) + private fun getPositionY(positionY: Float): Float { + if (currentScale < 1) { + return (originalHeight / 2 - halfHeight).toFloat() + } + val maxPositionY = halfHeight * (currentScale - 1) + return positionY.coerceIn(-maxPositionY, maxPositionY) } - if (velocityY != 0 && (atFirstPosition || atLastPosition)) { - val dy = (distanceTimeFactor * velocityY / 2) - val newY = getPositionY(y + dy) - val translationYAnimator = ValueAnimator.ofFloat(y, newY) - translationYAnimator.addUpdateListener { animation -> y = getPositionY(animation.animatedValue as Float) } - animatorSet.play(translationYAnimator) + + private fun zoom( + fromRate: Float, + toRate: Float, + fromX: Float, + toX: Float, + fromY: Float, + toY: Float, + ) { + isZooming = true + val animatorSet = AnimatorSet() + val translationXAnimator = ValueAnimator.ofFloat(fromX, toX) + translationXAnimator.addUpdateListener { animation -> x = animation.animatedValue as Float } + + val translationYAnimator = ValueAnimator.ofFloat(fromY, toY) + translationYAnimator.addUpdateListener { animation -> y = animation.animatedValue as Float } + + val scaleAnimator = ValueAnimator.ofFloat(fromRate, toRate) + scaleAnimator.addUpdateListener { animation -> + currentScale = animation.animatedValue as Float + setScaleRate(currentScale) + } + animatorSet.playTogether(translationXAnimator, translationYAnimator, scaleAnimator) + animatorSet.duration = ANIMATOR_DURATION_TIME.toLong() + animatorSet.interpolator = DecelerateInterpolator() + animatorSet.start() + animatorSet.doOnEnd { + isZooming = false + currentScale = toRate + } } - animatorSet.duration = 400 - animatorSet.interpolator = DecelerateInterpolator() - animatorSet.start() + fun zoomFling( + velocityX: Int, + velocityY: Int, + ): Boolean { + if (currentScale <= 1f) return false + + val distanceTimeFactor = 0.4f + val animatorSet = AnimatorSet() + + if (velocityX != 0) { + val dx = (distanceTimeFactor * velocityX / 2) + val newX = getPositionX(x + dx) + val translationXAnimator = ValueAnimator.ofFloat(x, newX) + translationXAnimator.addUpdateListener { animation -> + x = getPositionX(animation.animatedValue as Float) + } + animatorSet.play(translationXAnimator) + } + if (velocityY != 0 && (atFirstPosition || atLastPosition)) { + val dy = (distanceTimeFactor * velocityY / 2) + val newY = getPositionY(y + dy) + val translationYAnimator = ValueAnimator.ofFloat(y, newY) + translationYAnimator.addUpdateListener { animation -> + y = getPositionY(animation.animatedValue as Float) + } + animatorSet.play(translationYAnimator) + } - return true - } + animatorSet.duration = 400 + animatorSet.interpolator = DecelerateInterpolator() + animatorSet.start() - private fun zoomScrollBy(dx: Int, dy: Int) { - if (dx != 0) { - x = getPositionX(x + dx) + return true } - if (dy != 0) { - y = getPositionY(y + dy) + + private fun zoomScrollBy( + dx: Int, + dy: Int, + ) { + if (dx != 0) { + x = getPositionX(x + dx) + } + if (dy != 0) { + y = getPositionY(y + dy) + } } - } - private fun setScaleRate(rate: Float) { - scaleX = rate - scaleY = rate - } + private fun setScaleRate(rate: Float) { + scaleX = rate + scaleY = rate + } - fun onScale(scaleFactor: Float) { - currentScale *= scaleFactor - currentScale = currentScale.coerceIn( - minRate, - MAX_SCALE_RATE, - ) + fun onScale(scaleFactor: Float) { + currentScale *= scaleFactor + currentScale = + currentScale.coerceIn( + minRate, + MAX_SCALE_RATE, + ) - setScaleRate(currentScale) + setScaleRate(currentScale) - layoutParams.height = if (currentScale < 1) { (originalHeight / currentScale).toInt() } else { originalHeight } - halfHeight = layoutParams.height / 2 + layoutParams.height = + if (currentScale < 1) { + (originalHeight / currentScale).toInt() + } else { + originalHeight + } + halfHeight = layoutParams.height / 2 + + if (currentScale != DEFAULT_RATE) { + x = getPositionX(x) + y = getPositionY(y) + } else { + x = 0f + y = 0f + } - if (currentScale != DEFAULT_RATE) { - x = getPositionX(x) - y = getPositionY(y) - } else { - x = 0f - y = 0f + requestLayout() } - requestLayout() - } - - fun onScaleBegin() { - if (detector.isDoubleTapping) { - detector.isQuickScaling = true + fun onScaleBegin() { + if (detector.isDoubleTapping) { + detector.isQuickScaling = true + } } - } - fun onScaleEnd() { - if (scaleX < minRate) { - zoom(currentScale, minRate, x, 0f, y, 0f) + fun onScaleEnd() { + if (scaleX < minRate) { + zoom(currentScale, minRate, x, 0f, y, 0f) + } } - } - - inner class GestureListener : GestureDetectorWithLongTap.Listener() { - override fun onSingleTapConfirmed(ev: MotionEvent): Boolean { - tapListener?.invoke(ev) - return false - } + inner class GestureListener : GestureDetectorWithLongTap.Listener() { + override fun onSingleTapConfirmed(ev: MotionEvent): Boolean { + tapListener?.invoke(ev) + return false + } - override fun onDoubleTap(ev: MotionEvent): Boolean { - detector.isDoubleTapping = true - return false - } + override fun onDoubleTap(ev: MotionEvent): Boolean { + detector.isDoubleTapping = true + return false + } - fun onDoubleTapConfirmed(ev: MotionEvent) { - if (!isZooming && doubleTapZoom) { - if (scaleX != DEFAULT_RATE) { - zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f) - layoutParams.height = originalHeight - halfHeight = layoutParams.height / 2 - requestLayout() - } else { - val toScale = 2f - val toX = (halfWidth - ev.x) * (toScale - 1) - val toY = (halfHeight - ev.y) * (toScale - 1) - zoom(DEFAULT_RATE, toScale, 0f, toX, 0f, toY) + fun onDoubleTapConfirmed(ev: MotionEvent) { + if (!isZooming && doubleTapZoom) { + if (scaleX != DEFAULT_RATE) { + zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f) + layoutParams.height = originalHeight + halfHeight = layoutParams.height / 2 + requestLayout() + } else { + val toScale = 2f + val toX = (halfWidth - ev.x) * (toScale - 1) + val toY = (halfHeight - ev.y) * (toScale - 1) + zoom(DEFAULT_RATE, toScale, 0f, toX, 0f, toY) + } } } - } - override fun onLongTapConfirmed(ev: MotionEvent) { - if (longTapListener?.invoke(ev) == true) { - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + override fun onLongTapConfirmed(ev: MotionEvent) { + if (longTapListener?.invoke(ev) == true) { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } } } - } - inner class Detector : GestureDetectorWithLongTap(context, listener) { - - private var scrollPointerId = 0 - private var downX = 0 - private var downY = 0 - private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop - private var isZoomDragging = false - var isDoubleTapping = false - var isQuickScaling = false - - override fun onTouchEvent(ev: MotionEvent): Boolean { - val action = ev.actionMasked - val actionIndex = ev.actionIndex - - when (action) { - MotionEvent.ACTION_DOWN -> { - scrollPointerId = ev.getPointerId(0) - downX = (ev.x + 0.5f).toInt() - downY = (ev.y + 0.5f).toInt() - } - MotionEvent.ACTION_POINTER_DOWN -> { - scrollPointerId = ev.getPointerId(actionIndex) - downX = (ev.getX(actionIndex) + 0.5f).toInt() - downY = (ev.getY(actionIndex) + 0.5f).toInt() - } - MotionEvent.ACTION_MOVE -> { - if (isDoubleTapping && isQuickScaling) { - return true + inner class Detector : GestureDetectorWithLongTap(context, listener) { + private var scrollPointerId = 0 + private var downX = 0 + private var downY = 0 + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + private var isZoomDragging = false + var isDoubleTapping = false + var isQuickScaling = false + + override fun onTouchEvent(ev: MotionEvent): Boolean { + val action = ev.actionMasked + val actionIndex = ev.actionIndex + + when (action) { + MotionEvent.ACTION_DOWN -> { + scrollPointerId = ev.getPointerId(0) + downX = (ev.x + 0.5f).toInt() + downY = (ev.y + 0.5f).toInt() } - - val index = ev.findPointerIndex(scrollPointerId) - if (index < 0) { - return false + MotionEvent.ACTION_POINTER_DOWN -> { + scrollPointerId = ev.getPointerId(actionIndex) + downX = (ev.getX(actionIndex) + 0.5f).toInt() + downY = (ev.getY(actionIndex) + 0.5f).toInt() } + MotionEvent.ACTION_MOVE -> { + if (isDoubleTapping && isQuickScaling) { + return true + } - val x = (ev.getX(index) + 0.5f).toInt() - val y = (ev.getY(index) + 0.5f).toInt() - var dx = x - downX - var dy = if (atFirstPosition || atLastPosition) y - downY else 0 - - if (!isZoomDragging && currentScale > 1f) { - var startScroll = false + val index = ev.findPointerIndex(scrollPointerId) + if (index < 0) { + return false + } - if (abs(dx) > touchSlop) { - if (dx < 0) { - dx += touchSlop - } else { - dx -= touchSlop + val x = (ev.getX(index) + 0.5f).toInt() + val y = (ev.getY(index) + 0.5f).toInt() + var dx = x - downX + var dy = if (atFirstPosition || atLastPosition) y - downY else 0 + + if (!isZoomDragging && currentScale > 1f) { + var startScroll = false + + if (abs(dx) > touchSlop) { + if (dx < 0) { + dx += touchSlop + } else { + dx -= touchSlop + } + startScroll = true } - startScroll = true - } - if (abs(dy) > touchSlop) { - if (dy < 0) { - dy += touchSlop - } else { - dy -= touchSlop + if (abs(dy) > touchSlop) { + if (dy < 0) { + dy += touchSlop + } else { + dy -= touchSlop + } + startScroll = true + } + + if (startScroll) { + isZoomDragging = true } - startScroll = true } - if (startScroll) { - isZoomDragging = true + if (isZoomDragging) { + zoomScrollBy(dx, dy) } } - - if (isZoomDragging) { - zoomScrollBy(dx, dy) + MotionEvent.ACTION_UP -> { + if (isDoubleTapping && !isQuickScaling) { + listener.onDoubleTapConfirmed(ev) + } + isZoomDragging = false + isDoubleTapping = false + isQuickScaling = false } - } - MotionEvent.ACTION_UP -> { - if (isDoubleTapping && !isQuickScaling) { - listener.onDoubleTapConfirmed(ev) + MotionEvent.ACTION_CANCEL -> { + isZoomDragging = false + isDoubleTapping = false + isQuickScaling = false } - isZoomDragging = false - isDoubleTapping = false - isQuickScaling = false - } - MotionEvent.ACTION_CANCEL -> { - isZoomDragging = false - isDoubleTapping = false - isQuickScaling = false } + return super.onTouchEvent(ev) } - return super.onTouchEvent(ev) } } -} private const val ANIMATOR_DURATION_TIME = 200 private const val MIN_RATE = 0.5f diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt index 328d6725a5..35250c8d6e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt @@ -9,12 +9,11 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView * Implementation of subsampling scale image view that ignores all touch events, because the * webtoon viewer handles all the gestures. */ -class WebtoonSubsamplingImageView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, -) : SubsamplingScaleImageView(context, attrs) { - - override fun onTouchEvent(event: MotionEvent): Boolean { - return false +class WebtoonSubsamplingImageView + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + ) : SubsamplingScaleImageView(context, attrs) { + override fun onTouchEvent(event: MotionEvent): Boolean = false } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index 57036d070d..cff0741b02 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -27,7 +27,6 @@ class WebtoonTransitionHolder( val layout: LinearLayout, viewer: WebtoonViewer, ) : WebtoonBaseHolder(layout, viewer) { - private val scope = MainScope() private var stateJob: Job? = null @@ -37,10 +36,11 @@ class WebtoonTransitionHolder( * View container of the current status of the transition page. Child views will be added * dynamically. */ - private var pagesContainer = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - gravity = Gravity.CENTER - } + private var pagesContainer = + LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER + } init { layout.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) @@ -52,9 +52,10 @@ class WebtoonTransitionHolder( layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical) val childMargins = 16.dpToPx - val childParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { - setMargins(0, childMargins, 0, childMargins) - } + val childParams = + LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { + setMargins(0, childMargins, 0, childMargins) + } layout.addView(transitionView) layout.addView(pagesContainer, childParams) @@ -80,22 +81,26 @@ class WebtoonTransitionHolder( * Observes the status of the page list of the next/previous chapter. Whenever there's a new * state, the pages container is cleaned up before setting the new state. */ - private fun observeStatus(chapter: ReaderChapter, transition: ChapterTransition) { + private fun observeStatus( + chapter: ReaderChapter, + transition: ChapterTransition, + ) { stateJob?.cancel() - stateJob = scope.launch { - chapter.stateFlow - .collectLatest { state -> - pagesContainer.removeAllViews() - when (state) { - is ReaderChapter.State.Loading -> setLoading() - is ReaderChapter.State.Error -> setError(state.error, transition) - is ReaderChapter.State.Wait, is ReaderChapter.State.Loaded -> { - // No additional view is added + stateJob = + scope.launch { + chapter.stateFlow + .collectLatest { state -> + pagesContainer.removeAllViews() + when (state) { + is ReaderChapter.State.Loading -> setLoading() + is ReaderChapter.State.Error -> setError(state.error, transition) + is ReaderChapter.State.Wait, is ReaderChapter.State.Loaded -> { + // No additional view is added + } } + pagesContainer.isVisible = pagesContainer.isNotEmpty() } - pagesContainer.isVisible = pagesContainer.isNotEmpty() - } - } + } } /** @@ -105,10 +110,11 @@ class WebtoonTransitionHolder( val progress = CircularProgressIndicator(context) progress.isIndeterminate = true - val textView = AppCompatTextView(context).apply { - wrapContent() - text = context.stringResource(MR.strings.transition_pages_loading) - } + val textView = + AppCompatTextView(context).apply { + wrapContent() + text = context.stringResource(MR.strings.transition_pages_loading) + } pagesContainer.addView(progress) pagesContainer.addView(textView) @@ -117,22 +123,27 @@ class WebtoonTransitionHolder( /** * Sets the error state on the pages container. */ - private fun setError(error: Throwable, transition: ChapterTransition) { - val textView = AppCompatTextView(context).apply { - wrapContent() - text = context.stringResource(MR.strings.transition_pages_error, error.message ?: "") - } + private fun setError( + error: Throwable, + transition: ChapterTransition, + ) { + val textView = + AppCompatTextView(context).apply { + wrapContent() + text = context.stringResource(MR.strings.transition_pages_error, error.message ?: "") + } - val retryBtn = AppCompatButton(context).apply { - wrapContent() - text = context.stringResource(MR.strings.action_retry) - setOnClickListener { - val toChapter = transition.to - if (toChapter != null) { - viewer.activity.requestPreloadChapter(toChapter) + val retryBtn = + AppCompatButton(context).apply { + wrapContent() + text = context.stringResource(MR.strings.action_retry) + setOnClickListener { + val toChapter = transition.to + if (toChapter != null) { + viewer.activity.requestPreloadChapter(toChapter) + } } } - } pagesContainer.addView(textView) pagesContainer.addView(retryBtn) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index a965631fce..07aa21070f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -31,8 +31,10 @@ import kotlin.math.min /** * Implementation of a [Viewer] to display pages with a [RecyclerView]. */ -class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : Viewer { - +class WebtoonViewer( + val activity: ReaderActivity, + val isContinuous: Boolean = true, +) : Viewer { val downloadManager: DownloadManager by injectLazy() private val scope = MainScope() @@ -73,7 +75,8 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr private var currentPage: Any? = null private val threshold: Int = - Injekt.get() + Injekt + .get() .readerHideThreshold() .get() .threshold @@ -87,7 +90,11 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr recycler.adapter = adapter recycler.addOnScrollListener( object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + override fun onScrolled( + recyclerView: RecyclerView, + dx: Int, + dy: Int, + ) { onScrolled() if ((dy > threshold || dy < -threshold) && activity.viewModel.state.value.menuVisible) { @@ -115,10 +122,11 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr recycler.getLocationOnScreen(viewPosition) val viewPositionRelativeToWindow = IntArray(2) recycler.getLocationInWindow(viewPositionRelativeToWindow) - val pos = PointF( - (event.rawX - viewPosition[0] + viewPositionRelativeToWindow[0]) / recycler.width, - (event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / recycler.originalHeight, - ) + val pos = + PointF( + (event.rawX - viewPosition[0] + viewPositionRelativeToWindow[0]) / recycler.width, + (event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / recycler.originalHeight, + ) when (config.navigator.getAction(pos)) { NavigationRegion.MENU -> activity.toggleMenu() NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown() @@ -188,9 +196,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr /** * Returns the view this viewer uses. */ - override fun getView(): View { - return frame - } + override fun getView(): View = frame /** * Destroys this viewer. Called when leaving the reader or swapping viewers. @@ -204,7 +210,10 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr * Called from the RecyclerView listener when a [page] is marked as active. It notifies the * activity of the change and requests the preload of the next chapter if this is the last page. */ - private fun onPageSelected(page: ReaderPage, allowPreload: Boolean) { + private fun onPageSelected( + page: ReaderPage, + allowPreload: Boolean, + ) { val pages = page.chapter.pages ?: return logcat { "onPageSelected: ${page.number}/${pages.size}" } activity.onPageSelected(page) @@ -342,9 +351,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr * Called from the containing activity when a generic motion [event] is received. It should * return true if the event was handled, false otherwise. */ - override fun handleGenericMotionEvent(event: MotionEvent): Boolean { - return false - } + override fun handleGenericMotionEvent(event: MotionEvent): Boolean = false /** * Notifies adapter of changes around the current page to trigger a relayout in the recycler. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt index 00d1e136ac..613026f52f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt @@ -16,32 +16,32 @@ import tachiyomi.i18n.MR * Blank activity with a BiometricPrompt. */ class UnlockActivity : BaseActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) startAuthentication( stringResource(MR.strings.unlock_app_title, stringResource(MR.strings.app_name)), confirmationRequired = false, - callback = object : AuthenticatorUtil.AuthenticationCallback() { - override fun onAuthenticationError( - activity: FragmentActivity?, - errorCode: Int, - errString: CharSequence, - ) { - super.onAuthenticationError(activity, errorCode, errString) - logcat(LogPriority.ERROR) { errString.toString() } - finishAffinity() - } + callback = + object : AuthenticatorUtil.AuthenticationCallback() { + override fun onAuthenticationError( + activity: FragmentActivity?, + errorCode: Int, + errString: CharSequence, + ) { + super.onAuthenticationError(activity, errorCode, errString) + logcat(LogPriority.ERROR) { errString.toString() } + finishAffinity() + } - override fun onAuthenticationSucceeded( - activity: FragmentActivity?, - result: BiometricPrompt.AuthenticationResult, - ) { - super.onAuthenticationSucceeded(activity, result) - SecureActivityDelegate.unlock() - finish() - } - }, + override fun onAuthenticationSucceeded( + activity: FragmentActivity?, + result: BiometricPrompt.AuthenticationResult, + ) { + super.onAuthenticationSucceeded(activity, result) + SecureActivityDelegate.unlock() + finish() + } + }, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt index 3d396ace9a..3c5d112d61 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt @@ -26,7 +26,6 @@ import tachiyomi.presentation.core.components.TwoPanelBox class SettingsScreen( private val destination: Int? = null, ) : Screen() { - constructor(destination: Destination) : this(destination.id) @Composable @@ -34,12 +33,13 @@ class SettingsScreen( val parentNavigator = LocalNavigator.currentOrThrow if (!isTabletUi()) { Navigator( - screen = when (destination) { - Destination.About.id -> AboutScreen - Destination.DataAndStorage.id -> SettingsDataScreen - Destination.Tracking.id -> SettingsTrackingScreen - else -> SettingsMainScreen - }, + screen = + when (destination) { + Destination.About.id -> AboutScreen + Destination.DataAndStorage.id -> SettingsDataScreen + Destination.Tracking.id -> SettingsTrackingScreen + else -> SettingsMainScreen + }, content = { val pop: () -> Unit = { if (it.canPop) { @@ -55,18 +55,20 @@ class SettingsScreen( ) } else { Navigator( - screen = when (destination) { - Destination.About.id -> AboutScreen - Destination.DataAndStorage.id -> SettingsDataScreen - Destination.Tracking.id -> SettingsTrackingScreen - else -> SettingsAppearanceScreen - }, + screen = + when (destination) { + Destination.About.id -> AboutScreen + Destination.DataAndStorage.id -> SettingsDataScreen + Destination.Tracking.id -> SettingsTrackingScreen + else -> SettingsAppearanceScreen + }, ) { val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal) TwoPanelBox( - modifier = Modifier - .windowInsetsPadding(insets) - .consumeWindowInsets(insets), + modifier = + Modifier + .windowInsetsPadding(insets) + .consumeWindowInsets(insets), startContent = { CompositionLocalProvider(LocalBackPress provides parentNavigator::pop) { SettingsMainScreen.Content(twoPane = true) @@ -78,9 +80,13 @@ class SettingsScreen( } } - sealed class Destination(val id: Int) { + sealed class Destination( + val id: Int, + ) { data object About : Destination(0) + data object DataAndStorage : Destination(1) + data object Tracking : Destination(2) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt index 92302d7181..62ab9fef4b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt @@ -11,7 +11,6 @@ import tachiyomi.presentation.core.screens.LoadingScreen import uy.kohesive.injekt.injectLazy abstract class BaseOAuthLoginActivity : BaseActivity() { - internal val trackerManager: TrackerManager by injectLazy() abstract fun handleResult(data: Uri?) @@ -29,9 +28,10 @@ abstract class BaseOAuthLoginActivity : BaseActivity() { internal fun returnToSettings() { finish() - val intent = Intent(this, MainActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) - } + val intent = + Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } startActivity(intent) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt index 3f742cfe05..46301beea1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.lifecycleScope import tachiyomi.core.common.util.lang.launchIO class TrackLoginActivity : BaseOAuthLoginActivity() { - override fun handleResult(data: Uri?) { when (data?.host) { "anilist-auth" -> handleAnilist(data) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt index 82f1f2ab2d..4564b4f54f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt @@ -16,7 +16,6 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.LoadingScreen class StatsScreen : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt index 0d553ab4b9..8a6ffc7faf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt @@ -35,7 +35,6 @@ class StatsScreenModel( private val preferences: LibraryPreferences = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), ) : StateScreenModel(StatsScreenState.Loading) { - private val loggedInTrackers by lazy { trackerManager.trackers.fastFilter { it.isLoggedIn } } init { @@ -49,31 +48,36 @@ class StatsScreenModel( val meanScore = getTrackMeanScore(scoredMangaTrackerMap) - val overviewStatData = StatsData.Overview( - libraryMangaCount = distinctLibraryManga.size, - completedMangaCount = distinctLibraryManga.count { - it.manga.status.toInt() == SManga.COMPLETED && it.unreadCount == 0L - }, - totalReadDuration = getTotalReadDuration.await(), - ) - - val titlesStatData = StatsData.Titles( - globalUpdateItemCount = getGlobalUpdateItemCount(libraryManga), - startedMangaCount = distinctLibraryManga.count { it.hasStarted }, - localMangaCount = distinctLibraryManga.count { it.manga.isLocal() }, - ) - - val chaptersStatData = StatsData.Chapters( - totalChapterCount = distinctLibraryManga.sumOf { it.totalChapters }.toInt(), - readChapterCount = distinctLibraryManga.sumOf { it.readCount }.toInt(), - downloadCount = downloadManager.getDownloadCount(), - ) - - val trackersStatData = StatsData.Trackers( - trackedTitleCount = mangaTrackMap.count { it.value.isNotEmpty() }, - meanScore = meanScore, - trackerCount = loggedInTrackers.size, - ) + val overviewStatData = + StatsData.Overview( + libraryMangaCount = distinctLibraryManga.size, + completedMangaCount = + distinctLibraryManga.count { + it.manga.status.toInt() == SManga.COMPLETED && it.unreadCount == 0L + }, + totalReadDuration = getTotalReadDuration.await(), + ) + + val titlesStatData = + StatsData.Titles( + globalUpdateItemCount = getGlobalUpdateItemCount(libraryManga), + startedMangaCount = distinctLibraryManga.count { it.hasStarted }, + localMangaCount = distinctLibraryManga.count { it.manga.isLocal() }, + ) + + val chaptersStatData = + StatsData.Chapters( + totalChapterCount = distinctLibraryManga.sumOf { it.totalChapters }.toInt(), + readChapterCount = distinctLibraryManga.sumOf { it.readCount }.toInt(), + downloadCount = downloadManager.getDownloadCount(), + ) + + val trackersStatData = + StatsData.Trackers( + trackedTitleCount = mangaTrackMap.count { it.value.isNotEmpty() }, + meanScore = meanScore, + trackerCount = loggedInTrackers.size, + ) mutableState.update { StatsScreenState.Success( @@ -88,20 +92,22 @@ class StatsScreenModel( private fun getGlobalUpdateItemCount(libraryManga: List): Int { val includedCategories = preferences.updateCategories().get().map { it.toLong() } - val includedManga = if (includedCategories.isNotEmpty()) { - libraryManga.filter { it.category in includedCategories } - } else { - libraryManga - } + val includedManga = + if (includedCategories.isNotEmpty()) { + libraryManga.filter { it.category in includedCategories } + } else { + libraryManga + } val excludedCategories = preferences.updateCategoriesExclude().get().map { it.toLong() } - val excludedMangaIds = if (excludedCategories.isNotEmpty()) { - libraryManga.fastMapNotNull { manga -> - manga.id.takeIf { manga.category in excludedCategories } + val excludedMangaIds = + if (excludedCategories.isNotEmpty()) { + libraryManga.fastMapNotNull { manga -> + manga.id.takeIf { manga.category in excludedCategories } + } + } else { + emptyList() } - } else { - emptyList() - } val updateRestrictions = preferences.autoUpdateMangaRestrictions().get() return includedManga @@ -117,31 +123,33 @@ class StatsScreenModel( private suspend fun getMangaTrackMap(libraryManga: List): Map> { val loggedInTrackerIds = loggedInTrackers.map { it.id }.toHashSet() return libraryManga.associate { manga -> - val tracks = getTracks.await(manga.id) - .fastFilter { it.trackerId in loggedInTrackerIds } + val tracks = + getTracks + .await(manga.id) + .fastFilter { it.trackerId in loggedInTrackerIds } manga.id to tracks } } private fun getScoredMangaTrackMap(mangaTrackMap: Map>): Map> { - return mangaTrackMap.mapNotNull { (mangaId, tracks) -> - val trackList = tracks.mapNotNull { track -> - track.takeIf { it.score > 0.0 } - } - if (trackList.isEmpty()) return@mapNotNull null - mangaId to trackList - }.toMap() + return mangaTrackMap + .mapNotNull { (mangaId, tracks) -> + val trackList = + tracks.mapNotNull { track -> + track.takeIf { it.score > 0.0 } + } + if (trackList.isEmpty()) return@mapNotNull null + mangaId to trackList + }.toMap() } - private fun getTrackMeanScore(scoredMangaTrackMap: Map>): Double { - return scoredMangaTrackMap + private fun getTrackMeanScore(scoredMangaTrackMap: Map>): Double = + scoredMangaTrackMap .map { (_, tracks) -> tracks.map(::get10PointScore).average() - } - .fastFilter { !it.isNaN() } + }.fastFilter { !it.isNaN() } .average() - } private fun get10PointScore(track: Track): Double { val service = trackerManager.get(track.trackerId)!! 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 c5385d1f04..86efe4263e 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 @@ -59,7 +59,6 @@ class UpdatesScreenModel( private val libraryPreferences: LibraryPreferences = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), ) : StateScreenModel(State()) { - private val _events: Channel = Channel(Int.MAX_VALUE) val events: Flow = _events.receiveAsFlow() @@ -82,8 +81,7 @@ class UpdatesScreenModel( .catch { logcat(LogPriority.ERROR, it) _events.send(Event.InternalError) - } - .collectLatest { updates -> + }.collectLatest { updates -> mutableState.update { it.copy( isLoading = false, @@ -100,30 +98,30 @@ class UpdatesScreenModel( } } - private fun List.toUpdateItems(): PersistentList { - return this + private fun List.toUpdateItems(): PersistentList = + this .map { update -> val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId) - val downloaded = downloadManager.isChapterDownloaded( - update.chapterName, - update.scanlator, - update.mangaTitle, - update.sourceId, - ) - val downloadState = when { - activeDownload != null -> activeDownload.status - downloaded -> Download.State.DOWNLOADED - else -> Download.State.NOT_DOWNLOADED - } + val downloaded = + downloadManager.isChapterDownloaded( + update.chapterName, + update.scanlator, + update.mangaTitle, + update.sourceId, + ) + val downloadState = + when { + activeDownload != null -> activeDownload.status + downloaded -> Download.State.DOWNLOADED + else -> Download.State.NOT_DOWNLOADED + } UpdatesItem( update = update, downloadStateProvider = { downloadState }, downloadProgressProvider = { activeDownload?.progress ?: 0 }, selected = update.chapterId in selectedChapterIds, ) - } - .toPersistentList() - } + }.toPersistentList() fun updateLibrary(): Boolean { val started = LibraryUpdateJob.startNow(Injekt.get()) @@ -140,21 +138,26 @@ class UpdatesScreenModel( */ private fun updateDownloadState(download: Download) { mutableState.update { state -> - val newItems = state.items.mutate { list -> - val modifiedIndex = list.indexOfFirst { it.update.chapterId == download.chapter.id } - if (modifiedIndex < 0) return@mutate - - val item = list[modifiedIndex] - list[modifiedIndex] = item.copy( - downloadStateProvider = { download.status }, - downloadProgressProvider = { download.progress }, - ) - } + val newItems = + state.items.mutate { list -> + val modifiedIndex = list.indexOfFirst { it.update.chapterId == download.chapter.id } + if (modifiedIndex < 0) return@mutate + + val item = list[modifiedIndex] + list[modifiedIndex] = + item.copy( + downloadStateProvider = { download.status }, + downloadProgressProvider = { download.progress }, + ) + } state.copy(items = newItems) } } - fun downloadChapters(items: List, action: ChapterDownloadAction) { + fun downloadChapters( + items: List, + action: ChapterDownloadAction, + ) { if (items.isEmpty()) return screenModelScope.launch { when (action) { @@ -195,13 +198,17 @@ class UpdatesScreenModel( * @param updates the list of selected updates. * @param read whether to mark chapters as read or unread. */ - fun markUpdatesRead(updates: List, read: Boolean) { + fun markUpdatesRead( + updates: List, + read: Boolean, + ) { screenModelScope.launchIO { setReadStatus.await( read = read, - chapters = updates - .mapNotNull { getChapter.await(it.update.chapterId) } - .toTypedArray(), + chapters = + updates + .mapNotNull { getChapter.await(it.update.chapterId) } + .toTypedArray(), ) } toggleAllSelection(false) @@ -211,7 +218,10 @@ class UpdatesScreenModel( * Bookmarks the given list of chapters. * @param updates the list of chapters to bookmark. */ - fun bookmarkUpdates(updates: List, bookmark: Boolean) { + fun bookmarkUpdates( + updates: List, + bookmark: Boolean, + ) { screenModelScope.launchIO { updates .filterNot { it.update.bookmark == bookmark } @@ -270,69 +280,71 @@ class UpdatesScreenModel( fromLongPress: Boolean = false, ) { mutableState.update { state -> - 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) - - 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.. selectedPositions[1]) { - range = (selectedPositions[1] + 1).. selectedPositions[1]) { + range = (selectedPositions[1] + 1).. selectedPositions[1]) { - selectedPositions[1] = selectedIndex + } 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 + } } } } - } state.copy(items = newItems.toPersistentList()) } } fun toggleAllSelection(selected: Boolean) { mutableState.update { state -> - val newItems = state.items.map { - selectedChapterIds.addOrRemove(it.update.chapterId, selected) - it.copy(selected = selected) - } + val newItems = + state.items.map { + selectedChapterIds.addOrRemove(it.update.chapterId, selected) + it.copy(selected = selected) + } state.copy(items = newItems.toPersistentList()) } @@ -342,10 +354,11 @@ class UpdatesScreenModel( fun invertSelection() { mutableState.update { state -> - val newItems = state.items.map { - selectedChapterIds.addOrRemove(it.update.chapterId, !it.selected) - it.copy(selected = !it.selected) - } + val newItems = + state.items.map { + selectedChapterIds.addOrRemove(it.update.chapterId, !it.selected) + it.copy(selected = !it.selected) + } state.copy(items = newItems.toPersistentList()) } selectedPositions[0] = -1 @@ -369,28 +382,42 @@ class UpdatesScreenModel( val selected = items.filter { it.selected } val selectionMode = selected.isNotEmpty() - fun getUiModel(): List { - return items + fun getUiModel(): List = + items .map { UpdatesUiModel.Item(it) } .insertSeparators { before, after -> - val beforeDate = before?.item?.update?.dateFetch?.toLocalDate() - val afterDate = after?.item?.update?.dateFetch?.toLocalDate() + val beforeDate = + before + ?.item + ?.update + ?.dateFetch + ?.toLocalDate() + val afterDate = + after + ?.item + ?.update + ?.dateFetch + ?.toLocalDate() when { beforeDate != afterDate && afterDate != null -> UpdatesUiModel.Header(afterDate) // Return null to avoid adding a separator between two items. else -> null } } - } } sealed interface Dialog { - data class DeleteConfirmation(val toDelete: List) : Dialog + data class DeleteConfirmation( + val toDelete: List, + ) : Dialog } sealed interface Event { data object InternalError : Event - data class LibraryUpdateTriggered(val started: Boolean) : Event + + data class LibraryUpdateTriggered( + val started: Boolean, + ) : Event } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index 8064d123a7..0e943d61ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -32,7 +32,6 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource object UpdatesTab : Tab { - override val options: TabOptions @Composable get() { @@ -90,15 +89,17 @@ object UpdatesTab : Tab { LaunchedEffect(Unit) { screenModel.events.collectLatest { event -> when (event) { - Event.InternalError -> screenModel.snackbarHostState.showSnackbar( - context.stringResource(MR.strings.internal_error), - ) + Event.InternalError -> + screenModel.snackbarHostState.showSnackbar( + context.stringResource(MR.strings.internal_error), + ) is Event.LibraryUpdateTriggered -> { - val msg = if (event.started) { - MR.strings.updating_library - } else { - MR.strings.update_already_running - } + val msg = + if (event.started) { + MR.strings.updating_library + } else { + MR.strings.update_already_running + } screenModel.snackbarHostState.showSnackbar(context.stringResource(msg)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index e21fc0b763..55e2883766 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -26,7 +26,6 @@ import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy class WebViewActivity : BaseActivity() { - private val sourceManager: SourceManager by injectLazy() private val network: NetworkHelper by injectLazy() @@ -122,13 +121,17 @@ class WebViewActivity : BaseActivity() { private const val SOURCE_KEY = "source_key" private const val TITLE_KEY = "title_key" - fun newIntent(context: Context, url: String, sourceId: Long? = null, title: String? = null): Intent { - return Intent(context, WebViewActivity::class.java).apply { + fun newIntent( + context: Context, + url: String, + sourceId: Long? = null, + title: String? = null, + ): Intent = + Intent(context, WebViewActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) putExtra(URL_KEY, url) putExtra(SOURCE_KEY, sourceId) putExtra(TITLE_KEY, title) } - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt index dbb5147947..db5e136041 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt @@ -13,8 +13,8 @@ class WebViewScreen( private val url: String, private val initialTitle: String? = null, private val sourceId: Long? = null, -) : Screen(), AssistContentScreen { - +) : Screen(), + AssistContentScreen { private var assistUrl: String? = null override fun onProvideAssistUrl() = assistUrl diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreenModel.kt index 331d62b590..344f9e543e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreenModel.kt @@ -21,7 +21,6 @@ class WebViewScreenModel( private val sourceManager: SourceManager = Injekt.get(), private val network: NetworkHelper = Injekt.get(), ) : StateScreenModel(StatsScreenState.Loading) { - var headers = emptyMap() init { @@ -34,7 +33,10 @@ class WebViewScreenModel( } } - fun shareWebpage(context: Context, url: String) { + fun shareWebpage( + context: Context, + url: String, + ) { try { context.startActivity(url.toUri().toShareIntent(context, type = "text/plain")) } catch (e: Exception) { @@ -42,7 +44,10 @@ class WebViewScreenModel( } } - fun openInBrowser(context: Context, url: String) { + fun openInBrowser( + context: Context, + url: String, + ) { context.openInBrowser(url, forceDefaultBrowser = true) } 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 ed27314ef2..5d0c7f37c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -18,53 +18,53 @@ class CrashLogUtil( private val context: Context, private val extensionManager: ExtensionManager = Injekt.get(), ) { + suspend fun dumpLogs(exception: Throwable? = null) = + withNonCancellableContext { + try { + val file = context.createFileInCacheDir("mihon_crash_logs.txt") - suspend fun dumpLogs(exception: Throwable? = null) = withNonCancellableContext { - try { - val file = context.createFileInCacheDir("mihon_crash_logs.txt") + file.appendText(getDebugInfo() + "\n\n") + getExtensionsInfo()?.let { file.appendText("$it\n\n") } + exception?.let { file.appendText("$it\n\n") } - file.appendText(getDebugInfo() + "\n\n") - getExtensionsInfo()?.let { file.appendText("$it\n\n") } - exception?.let { file.appendText("$it\n\n") } + Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor() - Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor() - - val uri = file.getUriCompat(context) - context.startActivity(uri.toShareIntent(context, "text/plain")) - } catch (e: Throwable) { - withUIContext { context.toast("Failed to get logs") } + val uri = file.getUriCompat(context) + context.startActivity(uri.toShareIntent(context, "text/plain")) + } catch (e: Throwable) { + withUIContext { context.toast("Failed to get logs") } + } } - } - fun getDebugInfo(): String { - return """ - App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE}, ${BuildConfig.BUILD_TIME}) - Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}; build ${Build.DISPLAY}) - Device brand: ${Build.BRAND} - Device manufacturer: ${Build.MANUFACTURER} - Device name: ${Build.DEVICE} (${Build.PRODUCT}) - Device model: ${Build.MODEL} - WebView: ${WebViewUtil.getVersion(context)} + fun getDebugInfo(): String = + """ + App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE}, ${BuildConfig.BUILD_TIME}) + Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}; build ${Build.DISPLAY}) + Device brand: ${Build.BRAND} + Device manufacturer: ${Build.MANUFACTURER} + Device name: ${Build.DEVICE} (${Build.PRODUCT}) + Device model: ${Build.MODEL} + WebView: ${WebViewUtil.getVersion(context)} """.trimIndent() - } private fun getExtensionsInfo(): String? { val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName } - val extensionInfoList = extensionManager.installedExtensionsFlow.value - .sortedBy { it.name } - .mapNotNull { - val availableExtension = availableExtensions[it.pkgName] - val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode + val extensionInfoList = + extensionManager.installedExtensionsFlow.value + .sortedBy { it.name } + .mapNotNull { + val availableExtension = availableExtensions[it.pkgName] + val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode - if (!hasUpdate && !it.isObsolete) return@mapNotNull null + if (!hasUpdate && !it.isObsolete) return@mapNotNull null - """ + """ - ${it.name} Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"} Obsolete: ${it.isObsolete} - """.trimIndent() - } + """.trimIndent() + } return if (extensionInfoList.isNotEmpty()) { (listOf("Problematic extensions:") + extensionInfoList) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 50834eadbd..e8099978cc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -17,7 +17,11 @@ import java.time.Instant /** * Call before updating [Manga.thumbnail_url] to ensure old cover can be cleared from cache */ -fun Manga.prepUpdateCover(coverCache: CoverCache, remoteManga: SManga, refreshSameUrl: Boolean): Manga { +fun Manga.prepUpdateCover( + coverCache: CoverCache, + remoteManga: SManga, + refreshSameUrl: Boolean, +): Manga { // Never refresh covers if the new url is null, as the current url has possibly become invalid val newUrl = remoteManga.thumbnail_url ?: return this @@ -50,7 +54,10 @@ fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga { } } -fun Manga.shouldDownloadNewChapters(dbCategories: List, preferences: DownloadPreferences): Boolean { +fun Manga.shouldDownloadNewChapters( + dbCategories: List, + preferences: DownloadPreferences, +): Boolean { if (!favorite) return false val categories = dbCategories.ifEmpty { listOf(0L) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt index 9699a7e9f5..7b7d12cd49 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt @@ -4,11 +4,11 @@ import java.security.SecureRandom import java.util.Base64 object PkceUtil { - fun generateCodeVerifier(): String { val codeVerifier = ByteArray(50) SecureRandom().nextBytes(codeVerifier) - return Base64.getUrlEncoder() + return Base64 + .getUrlEncoder() .withoutPadding() .encodeToString(codeVerifier) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt index a7accb08e6..e98ed44a8b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt @@ -9,25 +9,27 @@ import tachiyomi.domain.manga.model.Manga /** * Gets next unread chapter with filters and sorting applied */ -fun List.getNextUnread(manga: Manga, downloadManager: DownloadManager): Chapter? { - return applyFilters(manga, downloadManager).let { chapters -> +fun List.getNextUnread( + manga: Manga, + downloadManager: DownloadManager, +): Chapter? = + applyFilters(manga, downloadManager).let { chapters -> if (manga.sortDescending()) { chapters.findLast { !it.read } } else { chapters.find { !it.read } } } -} /** * Gets next unread chapter with filters and sorting applied */ -fun List.getNextUnread(manga: Manga): Chapter? { - return applyFilters(manga).let { chapters -> - if (manga.sortDescending()) { - chapters.findLast { !it.chapter.read } - } else { - chapters.find { !it.chapter.read } - } - }?.chapter -} +fun List.getNextUnread(manga: Manga): Chapter? = + applyFilters(manga) + .let { chapters -> + if (manga.sortDescending()) { + chapters.findLast { !it.chapter.read } + } else { + chapters.find { !it.chapter.read } + } + }?.chapter diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRemoveDuplicates.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRemoveDuplicates.kt index d6e026f8de..4226317ca9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRemoveDuplicates.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRemoveDuplicates.kt @@ -5,11 +5,10 @@ import tachiyomi.domain.chapter.model.Chapter /** * Returns a copy of the list with duplicate chapters removed */ -fun List.removeDuplicates(currentChapter: Chapter): List { - return groupBy { it.chapterNumber } +fun List.removeDuplicates(currentChapter: Chapter): List = + groupBy { it.chapterNumber } .map { (_, chapters) -> chapters.find { it.id == currentChapter.id } ?: chapters.find { it.scanlator == currentChapter.scanlator } ?: chapters.first() } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/CloseableExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/CloseableExtensions.kt index 647eaab396..63ab2af230 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/CloseableExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/CloseableExtensions.kt @@ -19,13 +19,14 @@ inline fun Array.use(block: () -> Unit) { } finally { when (blockException) { null -> forEach { it?.close() } - else -> forEach { - try { - it?.close() - } catch (closeException: Throwable) { - blockException.addSuppressed(closeException) + else -> + forEach { + try { + it?.close() + } catch (closeException: Throwable) { + blockException.addSuppressed(closeException) + } } - } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt index 8827ae4a60..3d6bc4797e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt @@ -21,27 +21,21 @@ fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): S return "$date $time" } -fun Date.toTimestampString(): String { - return DateFormat.getTimeInstance(DateFormat.SHORT).format(this) -} +fun Date.toTimestampString(): String = DateFormat.getTimeInstance(DateFormat.SHORT).format(this) fun Long.convertEpochMillisZone( from: ZoneId, to: ZoneId, -): Long { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), from) +): Long = + LocalDateTime + .ofInstant(Instant.ofEpochMilli(this), from) .atZone(to) .toInstant() .toEpochMilli() -} -fun Long.toLocalDate(): LocalDate { - return LocalDate.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault()) -} +fun Long.toLocalDate(): LocalDate = LocalDate.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault()) -fun Instant.toLocalDate(zoneId: ZoneId = ZoneId.systemDefault()): LocalDate { - return LocalDate.ofInstant(this, zoneId) -} +fun Instant.toLocalDate(zoneId: ZoneId = ZoneId.systemDefault()): LocalDate = LocalDate.ofInstant(this, zoneId) fun LocalDate.toRelativeString( context: Context, @@ -55,17 +49,19 @@ fun LocalDate.toRelativeString( val difference = ChronoUnit.DAYS.between(this, now) return when { difference < -7 -> dateFormat.format(this) - difference < 0 -> context.pluralStringResource( - MR.plurals.upcoming_relative_time, - difference.toInt().absoluteValue, - difference.toInt().absoluteValue, - ) + difference < 0 -> + context.pluralStringResource( + MR.plurals.upcoming_relative_time, + difference.toInt().absoluteValue, + difference.toInt().absoluteValue, + ) difference < 1 -> context.stringResource(MR.strings.relative_time_today) - difference < 7 -> context.pluralStringResource( - MR.plurals.relative_time, - difference.toInt(), - difference.toInt(), - ) + difference < 7 -> + context.pluralStringResource( + MR.plurals.relative_time, + difference.toInt(), + difference.toInt(), + ) else -> dateFormat.format(this) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt index c0575c0d51..ec3c6d16df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt @@ -14,16 +14,19 @@ val Context.cacheImageDir: File * * @param context context of application */ -fun File.getUriCompat(context: Context): Uri { - return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this) -} +fun File.getUriCompat(context: Context): Uri = + FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this) /** * Copies this file to the given [target] file while marking the file as read-only. * * @see File.copyTo */ -fun File.copyAndSetReadOnlyTo(target: File, overwrite: Boolean = false, bufferSize: Int = DEFAULT_BUFFER_SIZE): File { +fun File.copyAndSetReadOnlyTo( + target: File, + overwrite: Boolean = false, + bufferSize: Int = DEFAULT_BUFFER_SIZE, +): File { if (!this.exists()) { throw NoSuchFileException(file = this, reason = "The source file doesn't exist.") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/AuthenticatorUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/AuthenticatorUtil.kt index 164a1075ca..83828f31ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/AuthenticatorUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/AuthenticatorUtil.kt @@ -16,7 +16,6 @@ import tachiyomi.i18n.MR import kotlin.coroutines.resume object AuthenticatorUtil { - /** * A check to avoid double authentication on older APIs when confirming settings changes since * the biometric prompt is launched in a separate activity outside of the app. @@ -50,36 +49,38 @@ object AuthenticatorUtil { suspend fun FragmentActivity.authenticate( title: String, subtitle: String? = stringResource(MR.strings.confirm_lock_change), - ): Boolean = suspendCancellableCoroutine { cont -> - if (!isAuthenticationSupported()) { - cont.resume(true) - return@suspendCancellableCoroutine - } + ): Boolean = + suspendCancellableCoroutine { cont -> + if (!isAuthenticationSupported()) { + cont.resume(true) + return@suspendCancellableCoroutine + } - startAuthentication( - title, - subtitle, - callback = object : AuthenticationCallback() { - override fun onAuthenticationSucceeded( - activity: FragmentActivity?, - result: BiometricPrompt.AuthenticationResult, - ) { - super.onAuthenticationSucceeded(activity, result) - cont.resume(true) - } + startAuthentication( + title, + subtitle, + callback = + object : AuthenticationCallback() { + override fun onAuthenticationSucceeded( + activity: FragmentActivity?, + result: BiometricPrompt.AuthenticationResult, + ) { + super.onAuthenticationSucceeded(activity, result) + cont.resume(true) + } - override fun onAuthenticationError( - activity: FragmentActivity?, - errorCode: Int, - errString: CharSequence, - ) { - super.onAuthenticationError(activity, errorCode, errString) - activity?.toast(errString.toString()) - cont.resume(false) - } - }, - ) - } + override fun onAuthenticationError( + activity: FragmentActivity?, + errorCode: Int, + errString: CharSequence, + ) { + super.onAuthenticationError(activity, errorCode, errString) + activity?.toast(errString.toString()) + cont.resume(false) + } + }, + ) + } /** * Returns true if Class 2 biometric or credential lock is set and available to use diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ChildFirstPathClassLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ChildFirstPathClassLoader.kt index b63dfd0328..fac69450a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ChildFirstPathClassLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ChildFirstPathClassLoader.kt @@ -15,26 +15,30 @@ import java.util.Enumeration class ChildFirstPathClassLoader( dexPath: String, librarySearchPath: String?, - parent: ClassLoader + parent: ClassLoader, ) : PathClassLoader(dexPath, librarySearchPath, parent) { - private val systemClassLoader: ClassLoader? = getSystemClassLoader() - override fun loadClass(name: String?, resolve: Boolean): Class<*> { + override fun loadClass( + name: String?, + resolve: Boolean, + ): Class<*> { var c = findLoadedClass(name) if (c == null && systemClassLoader != null) { try { c = systemClassLoader.loadClass(name) - } catch (_: ClassNotFoundException) {} + } catch (_: ClassNotFoundException) { + } } if (c == null) { - c = try { - findClass(name) - } catch (_: ClassNotFoundException) { - super.loadClass(name, resolve) - } + c = + try { + findClass(name) + } catch (_: ClassNotFoundException) { + super.loadClass(name, resolve) + } } if (resolve) { @@ -44,34 +48,35 @@ class ChildFirstPathClassLoader( return c } - override fun getResource(name: String?): URL? { - return systemClassLoader?.getResource(name) + override fun getResource(name: String?): URL? = + systemClassLoader?.getResource(name) ?: findResource(name) ?: super.getResource(name) - } override fun getResources(name: String?): Enumeration { val systemUrls = systemClassLoader?.getResources(name) val localUrls = findResources(name) val parentUrls = parent?.getResources(name) - val urls = buildList { - while (systemUrls?.hasMoreElements() == true) { - add(systemUrls.nextElement()) - } + val urls = + buildList { + while (systemUrls?.hasMoreElements() == true) { + add(systemUrls.nextElement()) + } - while (localUrls?.hasMoreElements() == true) { - add(localUrls.nextElement()) - } + while (localUrls?.hasMoreElements() == true) { + add(localUrls.nextElement()) + } - while (parentUrls?.hasMoreElements() == true) { - add(parentUrls.nextElement()) + while (parentUrls?.hasMoreElements() == true) { + add(parentUrls.nextElement()) + } } - } return object : Enumeration { val iterator = urls.iterator() override fun hasMoreElements() = iterator.hasNext() + override fun nextElement() = iterator.next() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 190764631d..ca4165596a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -35,7 +35,10 @@ import java.io.File * @param label Label to show to the user describing the content * @param content the actual text to copy to the board */ -fun Context.copyToClipboard(label: String, content: String) { +fun Context.copyToClipboard( + label: String, + content: String, +) { if (content.isBlank()) return try { @@ -56,18 +59,25 @@ fun Context.copyToClipboard(label: String, content: String) { val Context.powerManager: PowerManager get() = getSystemService()!! -fun Context.openInBrowser(url: String, forceDefaultBrowser: Boolean = false) { +fun Context.openInBrowser( + url: String, + forceDefaultBrowser: Boolean = false, +) { this.openInBrowser(url.toUri(), forceDefaultBrowser) } -fun Context.openInBrowser(uri: Uri, forceDefaultBrowser: Boolean = false) { +fun Context.openInBrowser( + uri: Uri, + forceDefaultBrowser: Boolean = false, +) { try { - val intent = Intent(Intent.ACTION_VIEW, uri).apply { - // Force default browser so that verified extensions don't re-open Tachiyomi - if (forceDefaultBrowser) { - defaultBrowserPackageName()?.let { setPackage(it) } + val intent = + Intent(Intent.ACTION_VIEW, uri).apply { + // Force default browser so that verified extensions don't re-open Tachiyomi + if (forceDefaultBrowser) { + defaultBrowserPackageName()?.let { setPackage(it) } + } } - } startActivity(intent) } catch (e: Exception) { toast(e.message) @@ -76,16 +86,18 @@ fun Context.openInBrowser(uri: Uri, forceDefaultBrowser: Boolean = false) { private fun Context.defaultBrowserPackageName(): String? { val browserIntent = Intent(Intent.ACTION_VIEW, "http://".toUri()) - val resolveInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.resolveActivity( - browserIntent, - PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()), - ) - } else { - packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) - } + val resolveInfo = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.resolveActivity( + browserIntent, + PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()), + ) + } else { + packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) + } return resolveInfo - ?.activityInfo?.packageName + ?.activityInfo + ?.packageName ?.takeUnless { it in DeviceUtil.invalidDefaultBrowsers } } @@ -107,11 +119,12 @@ fun Context.createFileInCacheDir(name: String): File { fun Context.createReaderThemeContext(): Context { val preferences = Injekt.get() val readerPreferences = Injekt.get() - val isDarkBackground = when (readerPreferences.readerTheme().get()) { - 1, 2 -> true // Black, Gray - 3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default - else -> false // White - } + val isDarkBackground = + when (readerPreferences.readerTheme().get()) { + 1, 2 -> true // Black, Gray + 3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default + else -> false // White + } val expected = if (isDarkBackground) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK != expected) { val overrideConf = Configuration() @@ -120,7 +133,8 @@ fun Context.createReaderThemeContext(): Context { val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi) wrappedContext.applyOverrideConfiguration(overrideConf) - ThemingDelegate.getThemeResIds(preferences.appTheme().get(), preferences.themeDarkAmoled().get()) + ThemingDelegate + .getThemeResIds(preferences.appTheme().get(), preferences.themeDarkAmoled().get()) .forEach { wrappedContext.theme.applyStyle(it, true) } return wrappedContext } @@ -132,37 +146,35 @@ fun Context.createReaderThemeContext(): Context { * * @return document size of [uri] or null if size can't be obtained */ -fun Context.getUriSize(uri: Uri): Long? { - return UniFile.fromUri(this, uri)?.length()?.takeIf { it >= 0 } -} +fun Context.getUriSize(uri: Uri): Long? = UniFile.fromUri(this, uri)?.length()?.takeIf { it >= 0 } /** * Returns true if [packageName] is installed. */ -fun Context.isPackageInstalled(packageName: String): Boolean { - return try { +fun Context.isPackageInstalled(packageName: String): Boolean = + try { packageManager.getApplicationInfo(packageName, 0) true } catch (e: PackageManager.NameNotFoundException) { false } -} val Context.hasMiuiPackageInstaller get() = isPackageInstalled("com.miui.packageinstaller") val Context.isShizukuInstalled get() = isPackageInstalled("moe.shizuku.privileged.api") || Sui.isSui() fun Context.isInstalledFromFDroid(): Boolean { - val installerPackageName = try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - packageManager.getInstallSourceInfo(packageName).installingPackageName - } else { - @Suppress("DEPRECATION") - packageManager.getInstallerPackageName(packageName) + val installerPackageName = + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + packageManager.getInstallSourceInfo(packageName).installingPackageName + } else { + @Suppress("DEPRECATION") + packageManager.getInstallerPackageName(packageName) + } + } catch (e: Exception) { + null } - } catch (e: Exception) { - null - } return installerPackageName == "org.fdroid.fdroid" || // F-Droid builds typically disable the updater diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt index f051ccbc86..0afdf76852 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt @@ -17,31 +17,32 @@ private const val TABLET_UI_MIN_SCREEN_WIDTH_PORTRAIT_DP = 700 // make sure icons on the nav rail fit private const val TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP = 600 -fun Configuration.isTabletUi(): Boolean { - return smallestScreenWidthDp >= TABLET_UI_REQUIRED_SCREEN_WIDTH_DP -} +fun Configuration.isTabletUi(): Boolean = smallestScreenWidthDp >= TABLET_UI_REQUIRED_SCREEN_WIDTH_DP // TODO: move the logic to `isTabletUi()` when main activity is rewritten in Compose fun Context.prepareTabletUiContext(): Context { val configuration = resources.configuration - val expected = when (Injekt.get().tabletUiMode().get()) { - TabletUiMode.AUTOMATIC -> - configuration.smallestScreenWidthDp >= when (configuration.orientation) { - Configuration.ORIENTATION_PORTRAIT -> TABLET_UI_MIN_SCREEN_WIDTH_PORTRAIT_DP - else -> TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP - } - TabletUiMode.ALWAYS -> true - TabletUiMode.LANDSCAPE -> configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - TabletUiMode.NEVER -> false - } + val expected = + when (Injekt.get().tabletUiMode().get()) { + TabletUiMode.AUTOMATIC -> + configuration.smallestScreenWidthDp >= + when (configuration.orientation) { + Configuration.ORIENTATION_PORTRAIT -> TABLET_UI_MIN_SCREEN_WIDTH_PORTRAIT_DP + else -> TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP + } + TabletUiMode.ALWAYS -> true + TabletUiMode.LANDSCAPE -> configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + TabletUiMode.NEVER -> false + } if (configuration.isTabletUi() != expected) { val overrideConf = Configuration() overrideConf.setTo(configuration) - overrideConf.smallestScreenWidthDp = if (expected) { - overrideConf.smallestScreenWidthDp.coerceAtLeast(TABLET_UI_REQUIRED_SCREEN_WIDTH_DP) - } else { - overrideConf.smallestScreenWidthDp.coerceAtMost(TABLET_UI_REQUIRED_SCREEN_WIDTH_DP - 1) - } + overrideConf.smallestScreenWidthDp = + if (expected) { + overrideConf.smallestScreenWidthDp.coerceAtLeast(TABLET_UI_REQUIRED_SCREEN_WIDTH_DP) + } else { + overrideConf.smallestScreenWidthDp.coerceAtMost(TABLET_UI_REQUIRED_SCREEN_WIDTH_DP - 1) + } return createConfigurationContext(overrideConf) } return this @@ -50,24 +51,21 @@ fun Context.prepareTabletUiContext(): Context { /** * Returns true if current context is in night mode */ -fun Context.isNightMode(): Boolean { - return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES -} +fun Context.isNightMode(): Boolean = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES /** * Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.). * * Only works in Android 9+. */ -fun Activity.hasDisplayCutout(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && +fun Activity.hasDisplayCutout(): Boolean = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && window.decorView.rootWindowInsets?.displayCutout != null -} /** * Gets system's config_navBarNeedsScrim boolean flag added in Android 10, defaults to true. */ -fun Context.isNavigationBarNeedsScrim(): Boolean { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || +fun Context.isNavigationBarNeedsScrim(): Boolean = + Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || InternalResourceHelper.getBoolean(this, "config_navBarNeedsScrim", true) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DrawableExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/DrawableExtensions.kt index 872552ef9a..4d52a7edde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/DrawableExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/DrawableExtensions.kt @@ -6,8 +6,9 @@ import android.graphics.drawable.Drawable import androidx.core.graphics.drawable.toBitmap import coil3.size.ScaleDrawable -fun Drawable.getBitmapOrNull(): Bitmap? = when (this) { - is BitmapDrawable -> bitmap - is ScaleDrawable -> child.toBitmap() - else -> null -} +fun Drawable.getBitmapOrNull(): Bitmap? = + when (this) { + is BitmapDrawable -> bitmap + is ScaleDrawable -> child.toBitmap() + else -> null + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/IntentExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/IntentExtensions.kt index 65acbed18f..96bf741c56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/IntentExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/IntentExtensions.kt @@ -10,38 +10,41 @@ import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import java.io.Serializable -fun Uri.toShareIntent(context: Context, type: String = "image/*", message: String? = null): Intent { +fun Uri.toShareIntent( + context: Context, + type: String = "image/*", + message: String? = null, +): Intent { val uri = this - val shareIntent = Intent(Intent.ACTION_SEND).apply { - when (uri.scheme) { - "http", "https" -> { - putExtra(Intent.EXTRA_TEXT, uri.toString()) - } - "content" -> { - message?.let { putExtra(Intent.EXTRA_TEXT, it) } - putExtra(Intent.EXTRA_STREAM, uri) + val shareIntent = + Intent(Intent.ACTION_SEND).apply { + when (uri.scheme) { + "http", "https" -> { + putExtra(Intent.EXTRA_TEXT, uri.toString()) + } + "content" -> { + message?.let { putExtra(Intent.EXTRA_TEXT, it) } + putExtra(Intent.EXTRA_STREAM, uri) + } } + clipData = ClipData.newRawUri(null, uri) + setType(type) + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION } - clipData = ClipData.newRawUri(null, uri) - setType(type) - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - } return Intent.createChooser(shareIntent, context.stringResource(MR.strings.action_share)).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } } -inline fun Intent.getParcelableExtraCompat(name: String): T? { - return IntentCompat.getParcelableExtra(this, name, T::class.java) -} +inline fun Intent.getParcelableExtraCompat(name: String): T? = + IntentCompat.getParcelableExtra(this, name, T::class.java) -inline fun Intent.getSerializableExtraCompat(name: String): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +inline fun Intent.getSerializableExtraCompat(name: String): T? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { getSerializableExtra(name, T::class.java) } else { @Suppress("DEPRECATION") getSerializableExtra(name) as? T } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/InternalResourceHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/InternalResourceHelper.kt index 12529a19c6..6c619be224 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/InternalResourceHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/InternalResourceHelper.kt @@ -5,8 +5,11 @@ import android.content.Context import android.content.res.Resources object InternalResourceHelper { - - fun getBoolean(context: Context, resName: String, defaultValue: Boolean): Boolean { + fun getBoolean( + context: Context, + resName: String, + defaultValue: Boolean, + ): Boolean { val id = getResourceId(resName, "bool") return if (id != 0) { context.createPackageContext("android", 0).resources.getBoolean(id) @@ -22,7 +25,8 @@ object InternalResourceHelper { * @return 0 if not available */ @SuppressLint("DiscouragedApi") - private fun getResourceId(resName: String, type: String): Int { - return Resources.getSystem().getIdentifier(resName, type, "android") - } + private fun getResourceId( + resName: String, + type: String, + ): Int = Resources.getSystem().getIdentifier(resName, type, "android") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt index 827eb739e6..16efb684d3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt @@ -11,7 +11,6 @@ import java.util.Locale * Utility class to change the application's language in runtime. */ object LocaleHelper { - /** * Sorts by display name, except keeps the "all" (displayed as "Multi") locale at the top. */ @@ -28,22 +27,25 @@ object LocaleHelper { /** * Returns display name of a string language code. */ - fun getSourceDisplayName(lang: String?, context: Context): String { - return when (lang) { + fun getSourceDisplayName( + lang: String?, + context: Context, + ): String = + when (lang) { SourcesScreenModel.LAST_USED_KEY -> context.stringResource(MR.strings.last_used_source) SourcesScreenModel.PINNED_KEY -> context.stringResource(MR.strings.pinned_sources) "other" -> context.stringResource(MR.strings.other_source) "all" -> context.stringResource(MR.strings.multi_lang) else -> getLocalizedDisplayName(lang) } - } fun getDisplayName(lang: String): String { - val normalizedLang = when (lang) { - "zh-CN" -> "zh-Hans" - "zh-TW" -> "zh-Hant" - else -> lang - } + val normalizedLang = + when (lang) { + "zh-CN" -> "zh-Hans" + "zh-TW" -> "zh-Hant" + else -> lang + } return Locale.forLanguageTag(normalizedLang).displayName } @@ -58,19 +60,18 @@ object LocaleHelper { return "" } - val locale = when (lang) { - "" -> LocaleListCompat.getAdjustedDefault()[0] - "zh-CN" -> Locale.forLanguageTag("zh-Hans") - "zh-TW" -> Locale.forLanguageTag("zh-Hant") - else -> Locale.forLanguageTag(lang) - } + val locale = + when (lang) { + "" -> LocaleListCompat.getAdjustedDefault()[0] + "zh-CN" -> Locale.forLanguageTag("zh-Hans") + "zh-TW" -> Locale.forLanguageTag("zh-Hant") + else -> Locale.forLanguageTag(lang) + } return locale!!.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) } } /** * Return the default languages enabled for the sources. */ - fun getDefaultEnabledLanguages(): Set { - return setOf("all", "en", Locale.getDefault().language) - } + fun getDefaultEnabledLanguages(): Set = setOf("all", "en", Locale.getDefault().language) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkExtensions.kt index 18e16baac7..c71715b02f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkExtensions.kt @@ -16,10 +16,11 @@ val Context.wifiManager: WifiManager fun Context.isOnline(): Boolean { val activeNetwork = connectivityManager.activeNetwork ?: return false val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false - val maxTransport = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> NetworkCapabilities.TRANSPORT_LOWPAN - else -> NetworkCapabilities.TRANSPORT_WIFI_AWARE - } + val maxTransport = + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> NetworkCapabilities.TRANSPORT_LOWPAN + else -> NetworkCapabilities.TRANSPORT_WIFI_AWARE + } return (NetworkCapabilities.TRANSPORT_CELLULAR..maxTransport).any(networkCapabilities::hasTransport) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkStateTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkStateTracker.kt index 0c309ae55e..ae9f757267 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkStateTracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkStateTracker.kt @@ -25,18 +25,24 @@ fun Context.activeNetworkState(): NetworkState { ) } -fun Context.networkStateFlow() = callbackFlow { - val networkCallback = object : NetworkCallback() { - override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - trySend(activeNetworkState()) - } - override fun onLost(network: Network) { - trySend(activeNetworkState()) - } - } +fun Context.networkStateFlow() = + callbackFlow { + val networkCallback = + object : NetworkCallback() { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities, + ) { + trySend(activeNetworkState()) + } - connectivityManager.registerDefaultNetworkCallback(networkCallback) - awaitClose { - connectivityManager.unregisterNetworkCallback(networkCallback) + override fun onLost(network: Network) { + trySend(activeNetworkState()) + } + } + + connectivityManager.registerDefaultNetworkCallback(networkCallback) + awaitClose { + connectivityManager.unregisterNetworkCallback(networkCallback) + } } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt index 7a72a5173c..c880aa3d4f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt @@ -17,12 +17,19 @@ import eu.kanade.tachiyomi.R val Context.notificationManager: NotificationManager get() = getSystemService()!! -fun Context.notify(id: Int, channelId: String, block: (NotificationCompat.Builder.() -> Unit)? = null) { +fun Context.notify( + id: Int, + channelId: String, + block: (NotificationCompat.Builder.() -> Unit)? = null, +) { val notification = notificationBuilder(channelId, block).build() this.notify(id, notification) } -fun Context.notify(id: Int, notification: Notification) { +fun Context.notify( + id: Int, + notification: Notification, +) { if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionChecker.checkSelfPermission( @@ -65,8 +72,10 @@ fun Context.notificationBuilder( channelId: String, block: (NotificationCompat.Builder.() -> Unit)? = null, ): NotificationCompat.Builder { - val builder = NotificationCompat.Builder(this, channelId) - .setColor(getColor(R.color.accent_blue)) + val builder = + NotificationCompat + .Builder(this, channelId) + .setColor(getColor(R.color.accent_blue)) if (block != null) { builder.block() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt index 4428fb9ae1..43985f3258 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/EditTextPreferenceExtensions.kt @@ -5,6 +5,4 @@ package androidx.preference /** * Returns package-private [EditTextPreference.getOnBindEditTextListener] */ -fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? { - return onBindEditTextListener -} +fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? = onBindEditTextListener diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 60d357a53b..3bf67cca4e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -39,9 +39,7 @@ inline fun ComponentActivity.setComposeContent( } } -fun ComposeView.setComposeContent( - content: @Composable () -> Unit, -) { +fun ComposeView.setComposeContent(content: @Composable () -> Unit) { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { TachiyomiTheme { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiTextInputEditText.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiTextInputEditText.kt index a571b68fa9..a8e370963b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiTextInputEditText.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiTextInputEditText.kt @@ -23,41 +23,45 @@ import uy.kohesive.injekt.api.get * * @see setIncognito */ -class TachiyomiTextInputEditText @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.editTextStyle, -) : TextInputEditText(context, attrs, defStyleAttr) { +class TachiyomiTextInputEditText + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.editTextStyle, + ) : TextInputEditText(context, attrs, defStyleAttr) { + private var scope: CoroutineScope? = null - private var scope: CoroutineScope? = null - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - setIncognito(scope!!) - } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + setIncognito(scope!!) + } - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - scope?.cancel() - scope = null - } + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + scope?.cancel() + scope = null + } - companion object { - /** - * Sets Flow to this [EditText] that sets [EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING] to imeOptions - * if [BasePreferences.incognitoMode] is true. Some IMEs may not respect this flag. - */ - fun EditText.setIncognito(viewScope: CoroutineScope) { - Injekt.get().incognitoMode().changes() - .onEach { - imeOptions = if (it) { - imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - } else { - imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv() - } - } - .launchIn(viewScope) + companion object { + /** + * Sets Flow to this [EditText] that sets [EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING] to imeOptions + * if [BasePreferences.incognitoMode] is true. Some IMEs may not respect this flag. + */ + fun EditText.setIncognito(viewScope: CoroutineScope) { + Injekt + .get() + .incognitoMode() + .changes() + .onEach { + imeOptions = + if (it) { + imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING + } else { + imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv() + } + }.launchIn(viewScope) + } } } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt index 4e35f4c866..4da8c3b84b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt @@ -5,27 +5,41 @@ import android.view.ViewGroup import androidx.viewpager.widget.PagerAdapter abstract class ViewPagerAdapter : PagerAdapter() { - - protected abstract fun createView(container: ViewGroup, position: Int): View - - protected open fun destroyView(container: ViewGroup, position: Int, view: View) { + protected abstract fun createView( + container: ViewGroup, + position: Int, + ): View + + protected open fun destroyView( + container: ViewGroup, + position: Int, + view: View, + ) { } - override fun instantiateItem(container: ViewGroup, position: Int): Any { + override fun instantiateItem( + container: ViewGroup, + position: Int, + ): Any { val view = createView(container, position) container.addView(view) return view } - override fun destroyItem(container: ViewGroup, position: Int, obj: Any) { + override fun destroyItem( + container: ViewGroup, + position: Int, + obj: Any, + ) { val view = obj as View destroyView(container, position, view) container.removeView(view) } - override fun isViewFromObject(view: View, obj: Any): Boolean { - return view === obj - } + override fun isViewFromObject( + view: View, + obj: Any, + ): Boolean = view === obj interface PositionableView { val item: Any diff --git a/app/src/main/java/eu/kanade/test/DummyTracker.kt b/app/src/main/java/eu/kanade/test/DummyTracker.kt index 56092b440a..889f6f1d0b 100644 --- a/app/src/main/java/eu/kanade/test/DummyTracker.kt +++ b/app/src/main/java/eu/kanade/test/DummyTracker.kt @@ -26,7 +26,6 @@ data class DummyTracker( val val10PointScore: Double = 5.4, val valSearchResults: List = listOf(), ) : Tracker { - override val client: OkHttpClient get() = TODO("Not yet implemented") @@ -36,15 +35,16 @@ data class DummyTracker( override fun getStatusList(): List = valStatuses - override fun getStatus(status: Long): StringResource? = when (status) { - 1L -> MR.strings.reading - 2L -> MR.strings.plan_to_read - 3L -> MR.strings.completed - 4L -> MR.strings.on_hold - 5L -> MR.strings.dropped - 6L -> MR.strings.repeating - else -> null - } + override fun getStatus(status: Long): StringResource? = + when (status) { + 1L -> MR.strings.reading + 2L -> MR.strings.plan_to_read + 3L -> MR.strings.completed + 4L -> MR.strings.on_hold + 5L -> MR.strings.dropped + 6L -> MR.strings.repeating + else -> null + } override fun getReadingStatus(): Long = valReadingStatus @@ -58,8 +58,7 @@ data class DummyTracker( override fun indexToScore(index: Int): Double = getScoreList()[index].toDouble() - override fun displayScore(track: Track): String = - track.score.toString() + override fun displayScore(track: Track): String = track.score.toString() override suspend fun update( track: eu.kanade.tachiyomi.data.database.models.Track, @@ -77,7 +76,10 @@ data class DummyTracker( track: eu.kanade.tachiyomi.data.database.models.Track, ): eu.kanade.tachiyomi.data.database.models.Track = track - override suspend fun login(username: String, password: String) = Unit + override suspend fun login( + username: String, + password: String, + ) = Unit override fun logout() = Unit @@ -85,7 +87,10 @@ data class DummyTracker( override fun getPassword(): String = "passw0rd" - override fun saveCredentials(username: String, password: String) = Unit + override fun saveCredentials( + username: String, + password: String, + ) = Unit override suspend fun register( item: eu.kanade.tachiyomi.data.database.models.Track, diff --git a/app/src/main/java/mihon/core/migration/Migration.kt b/app/src/main/java/mihon/core/migration/Migration.kt index 2c7283bb2a..eaf26faa39 100644 --- a/app/src/main/java/mihon/core/migration/Migration.kt +++ b/app/src/main/java/mihon/core/migration/Migration.kt @@ -11,12 +11,15 @@ interface Migration { companion object { const val ALWAYS = -1f - fun of(version: Float, action: suspend (MigrationContext) -> Boolean): Migration = object : Migration { - override val version: Float = version + fun of( + version: Float, + action: suspend (MigrationContext) -> Boolean, + ): Migration = + object : Migration { + override val version: Float = version - override suspend operator fun invoke(migrationContext: MigrationContext): Boolean { - return action(migrationContext) + override suspend operator fun invoke(migrationContext: MigrationContext): Boolean = + action(migrationContext) } - } } } diff --git a/app/src/main/java/mihon/core/migration/MigrationContext.kt b/app/src/main/java/mihon/core/migration/MigrationContext.kt index 3d0473f27f..dd387cc397 100644 --- a/app/src/main/java/mihon/core/migration/MigrationContext.kt +++ b/app/src/main/java/mihon/core/migration/MigrationContext.kt @@ -2,9 +2,8 @@ package mihon.core.migration import uy.kohesive.injekt.Injekt -class MigrationContext(val dryrun: Boolean) { - - inline fun get(): T? { - return Injekt.getInstanceOrNull(T::class.java) - } +class MigrationContext( + val dryrun: Boolean, +) { + inline fun get(): T? = Injekt.getInstanceOrNull(T::class.java) } diff --git a/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt b/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt index 801411013a..6e5065a45c 100644 --- a/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt +++ b/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt @@ -9,23 +9,28 @@ import tachiyomi.core.common.util.system.logcat class MigrationJobFactory( private val migrationContext: MigrationContext, - private val scope: CoroutineScope + private val scope: CoroutineScope, ) { - @SuppressWarnings("MaxLineLength") - fun create(migrations: List): Deferred = with(scope) { - return migrations.sortedBy { it.version } - .fold(CompletableDeferred(true)) { acc: Deferred, migration: Migration -> - if (!migrationContext.dryrun) { - logcat { "Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" } - async(start = CoroutineStart.UNDISPATCHED) { - val prev = acc.await() - migration(migrationContext) || prev + fun create(migrations: List): Deferred = + with(scope) { + return migrations + .sortedBy { it.version } + .fold(CompletableDeferred(true)) { acc: Deferred, migration: Migration -> + if (!migrationContext.dryrun) { + logcat { + "Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" + } + async(start = CoroutineStart.UNDISPATCHED) { + val prev = acc.await() + migration(migrationContext) || prev + } + } else { + logcat { + "(Dry-run) Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" + } + CompletableDeferred(true) } - } else { - logcat { "(Dry-run) Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" } - CompletableDeferred(true) } - } - } + } } diff --git a/app/src/main/java/mihon/core/migration/MigrationStrategy.kt b/app/src/main/java/mihon/core/migration/MigrationStrategy.kt index 9fd5f4f919..4ed36b0f0d 100644 --- a/app/src/main/java/mihon/core/migration/MigrationStrategy.kt +++ b/app/src/main/java/mihon/core/migration/MigrationStrategy.kt @@ -12,44 +12,41 @@ interface MigrationStrategy { class DefaultMigrationStrategy( private val migrationJobFactory: MigrationJobFactory, private val migrationCompletedListener: MigrationCompletedListener, - private val scope: CoroutineScope + private val scope: CoroutineScope, ) : MigrationStrategy { + override operator fun invoke(migrations: List): Deferred = + with(scope) { + if (migrations.isEmpty()) { + return@with CompletableDeferred(false) + } - override operator fun invoke(migrations: List): Deferred = with(scope) { - if (migrations.isEmpty()) { - return@with CompletableDeferred(false) - } - - val chain = migrationJobFactory.create(migrations) + val chain = migrationJobFactory.create(migrations) - launch { - if (chain.await()) migrationCompletedListener() - }.start() + launch { + if (chain.await()) migrationCompletedListener() + }.start() - chain - } + chain + } } -class InitialMigrationStrategy(private val strategy: DefaultMigrationStrategy) : MigrationStrategy { - - override operator fun invoke(migrations: List): Deferred { - return strategy(migrations.filter { it.isAlways }) - } +class InitialMigrationStrategy( + private val strategy: DefaultMigrationStrategy, +) : MigrationStrategy { + override operator fun invoke(migrations: List): Deferred = + strategy(migrations.filter { it.isAlways }) } -class NoopMigrationStrategy(val state: Boolean) : MigrationStrategy { - - override fun invoke(migrations: List): Deferred { - return CompletableDeferred(state) - } +class NoopMigrationStrategy( + val state: Boolean, +) : MigrationStrategy { + override fun invoke(migrations: List): Deferred = CompletableDeferred(state) } class VersionRangeMigrationStrategy( private val versions: IntRange, - private val strategy: DefaultMigrationStrategy + private val strategy: DefaultMigrationStrategy, ) : MigrationStrategy { - - override operator fun invoke(migrations: List): Deferred { - return strategy(migrations.filter { it.isAlways || it.version.toInt() in versions }) - } + override operator fun invoke(migrations: List): Deferred = + strategy(migrations.filter { it.isAlways || it.version.toInt() in versions }) } diff --git a/app/src/main/java/mihon/core/migration/MigrationStrategyFactory.kt b/app/src/main/java/mihon/core/migration/MigrationStrategyFactory.kt index 7e06fecb3d..6c3dedf7f1 100644 --- a/app/src/main/java/mihon/core/migration/MigrationStrategyFactory.kt +++ b/app/src/main/java/mihon/core/migration/MigrationStrategyFactory.kt @@ -4,20 +4,25 @@ class MigrationStrategyFactory( private val factory: MigrationJobFactory, private val migrationCompletedListener: MigrationCompletedListener, ) { - - fun create(old: Int, new: Int): MigrationStrategy { + fun create( + old: Int, + new: Int, + ): MigrationStrategy { val versions = (old + 1)..new - val strategy = when { - old == 0 -> InitialMigrationStrategy( - strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope), - ) + val strategy = + when { + old == 0 -> + InitialMigrationStrategy( + strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope), + ) - old >= new -> NoopMigrationStrategy(false) - else -> VersionRangeMigrationStrategy( - versions = versions, - strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope), - ) - } + old >= new -> NoopMigrationStrategy(false) + else -> + VersionRangeMigrationStrategy( + versions = versions, + strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope), + ) + } return strategy } } diff --git a/app/src/main/java/mihon/core/migration/Migrator.kt b/app/src/main/java/mihon/core/migration/Migrator.kt index 11f22a8c9b..28af3bb120 100644 --- a/app/src/main/java/mihon/core/migration/Migrator.kt +++ b/app/src/main/java/mihon/core/migration/Migrator.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking object Migrator { - private var result: Deferred? = null val scope = CoroutineScope(Dispatchers.Main + Job()) @@ -17,7 +16,7 @@ object Migrator { new: Int, migrations: List, dryrun: Boolean = false, - onMigrationComplete: () -> Unit + onMigrationComplete: () -> Unit, ) { val migrationContext = MigrationContext(dryrun) val migrationJobFactory = MigrationJobFactory(migrationContext, scope) @@ -35,7 +34,8 @@ object Migrator { result = null } - fun awaitAndRelease(): Boolean = runBlocking { - await().also { release() } - } + fun awaitAndRelease(): Boolean = + runBlocking { + await().also { release() } + } } 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 821434e2ee..b0bb49c0fe 100644 --- a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt +++ b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt @@ -3,8 +3,9 @@ package mihon.core.migration.migrations import mihon.core.migration.Migration val migrations: List - get() = listOf( - SetupBackupCreateMigration(), - SetupLibraryUpdateMigration(), - TrustExtensionRepositoryMigration(), - ) + get() = + listOf( + SetupBackupCreateMigration(), + SetupLibraryUpdateMigration(), + TrustExtensionRepositoryMigration(), + ) 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 cacba046f2..82d8f0782e 100644 --- a/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt @@ -12,24 +12,25 @@ import tachiyomi.core.common.util.system.logcat class TrustExtensionRepositoryMigration : Migration { override val version: Float = 7f - override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext { - val sourcePreferences = migrationContext.get() ?: return@withIOContext false - val extensionRepositoryRepository = - 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}", - ) - } catch (e: SaveExtensionRepoException) { - logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" } + override suspend fun invoke(migrationContext: MigrationContext): Boolean = + withIOContext { + val sourcePreferences = migrationContext.get() ?: return@withIOContext false + val extensionRepositoryRepository = + 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}", + ) + } catch (e: SaveExtensionRepoException) { + logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" } + } } + sourcePreferences.extensionRepos().delete() + return@withIOContext true } - sourcePreferences.extensionRepos().delete() - return@withIOContext true - } } diff --git a/app/src/main/java/mihon/feature/upcoming/UpcomingScreen.kt b/app/src/main/java/mihon/feature/upcoming/UpcomingScreen.kt index 981c3d5401..d64f50d2cc 100644 --- a/app/src/main/java/mihon/feature/upcoming/UpcomingScreen.kt +++ b/app/src/main/java/mihon/feature/upcoming/UpcomingScreen.kt @@ -10,7 +10,6 @@ import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.ui.manga.MangaScreen class UpcomingScreen : Screen() { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow diff --git a/app/src/main/java/mihon/feature/upcoming/UpcomingScreenModel.kt b/app/src/main/java/mihon/feature/upcoming/UpcomingScreenModel.kt index b404a7f966..2a07f59ca0 100644 --- a/app/src/main/java/mihon/feature/upcoming/UpcomingScreenModel.kt +++ b/app/src/main/java/mihon/feature/upcoming/UpcomingScreenModel.kt @@ -25,7 +25,6 @@ import java.time.YearMonth class UpcomingScreenModel( private val getUpcomingManga: GetUpcomingManga = Injekt.get(), ) : StateScreenModel(State()) { - init { screenModelScope.launch { getUpcomingManga.subscribe().collectLatest { @@ -41,8 +40,8 @@ class UpcomingScreenModel( } } - private fun List.toUpcomingUIModels(): ImmutableList { - return fastMap { UpcomingUIModel.Item(it) } + private fun List.toUpcomingUIModels(): ImmutableList = + fastMap { UpcomingUIModel.Item(it) } .insertSeparators { before, after -> val beforeDate = before?.manga?.expectedNextUpdate?.toLocalDate() val afterDate = after?.manga?.expectedNextUpdate?.toLocalDate() @@ -52,27 +51,22 @@ class UpcomingScreenModel( } else { null } - } - .toImmutableList() - } + }.toImmutableList() - private fun List.toEvents(): ImmutableMap { - return groupBy { it.expectedNextUpdate?.toLocalDate() ?: LocalDate.MAX } + private fun List.toEvents(): ImmutableMap = + groupBy { it.expectedNextUpdate?.toLocalDate() ?: LocalDate.MAX } .mapValues { it.value.size } .toImmutableMap() - } - private fun List.getHeaderIndexes(): ImmutableMap { - return fastMapIndexedNotNull { index, upcomingUIModel -> + private fun List.getHeaderIndexes(): ImmutableMap = + fastMapIndexedNotNull { index, upcomingUIModel -> if (upcomingUIModel is UpcomingUIModel.Header) { upcomingUIModel.date to index } else { null } - } - .toMap() + }.toMap() .toImmutableMap() - } fun setSelectedYearMonth(yearMonth: YearMonth) { mutableState.update { it.copy(selectedYearMonth = yearMonth) } diff --git a/app/src/main/java/mihon/feature/upcoming/UpcomingUIModel.kt b/app/src/main/java/mihon/feature/upcoming/UpcomingUIModel.kt index c394f45f6f..b5a849dfdd 100644 --- a/app/src/main/java/mihon/feature/upcoming/UpcomingUIModel.kt +++ b/app/src/main/java/mihon/feature/upcoming/UpcomingUIModel.kt @@ -4,6 +4,11 @@ import tachiyomi.domain.manga.model.Manga import java.time.LocalDate sealed interface UpcomingUIModel { - data class Header(val date: LocalDate) : UpcomingUIModel - data class Item(val manga: Manga) : UpcomingUIModel + data class Header( + val date: LocalDate, + ) : UpcomingUIModel + + data class Item( + val manga: Manga, + ) : UpcomingUIModel } diff --git a/app/src/main/java/mihon/feature/upcoming/components/UpcomingItem.kt b/app/src/main/java/mihon/feature/upcoming/components/UpcomingItem.kt index bf82c73cd1..4bbf73c93b 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/UpcomingItem.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/UpcomingItem.kt @@ -28,13 +28,14 @@ fun UpcomingItem( modifier: Modifier = Modifier, ) { Row( - modifier = modifier - .clickable(onClick = onClick) - .height(UpcomingItemHeight) - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ), + modifier = + modifier + .clickable(onClick = onClick) + .height(UpcomingItemHeight) + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.large), ) { diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt index da511ddada..1ffe0b7375 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt @@ -51,10 +51,11 @@ fun Calendar( yearMonth = selectedYearMonth, onPreviousClick = { setSelectedYearMonth(selectedYearMonth.minusMonths(1L)) }, onNextClick = { setSelectedYearMonth(selectedYearMonth.plusMonths(1L)) }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = MaterialTheme.padding.small) - .padding(start = MaterialTheme.padding.medium) + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = MaterialTheme.padding.small) + .padding(start = MaterialTheme.padding.medium), ) CalendarGrid( selectedYearMonth = selectedYearMonth, @@ -71,29 +72,32 @@ private fun CalendarGrid( onClickDay: (day: LocalDate) -> Unit, ) { val localeFirstDayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek.value - val weekDays = remember { - (0 until DaysOfWeek) - .map { DayOfWeek.of((localeFirstDayOfWeek - 1 + it) % DaysOfWeek + 1) } - .toImmutableList() - } + val weekDays = + remember { + (0 until DaysOfWeek) + .map { DayOfWeek.of((localeFirstDayOfWeek - 1 + it) % DaysOfWeek + 1) } + .toImmutableList() + } val emptyFieldCount = weekDays.indexOf(selectedYearMonth.atDay(1).dayOfWeek) val daysInMonth = selectedYearMonth.lengthOfMonth() VerticalGrid( columns = SimpleGridCells.Fixed(DaysOfWeek), - modifier = if (isMediumWidthWindow() && !isExpandedWidthWindow()) { - Modifier.widthIn(max = 360.dp) - } else { - Modifier - } + modifier = + if (isMediumWidthWindow() && !isExpandedWidthWindow()) { + Modifier.widthIn(max = 360.dp) + } else { + Modifier + }, ) { weekDays.fastForEach { item -> Text( - text = item.getDisplayName( - TextStyle.NARROW, - Locale.getDefault(), - ), + text = + item.getDisplayName( + TextStyle.NARROW, + Locale.getDefault(), + ), textAlign = TextAlign.Center, fontWeight = FontWeight.SemiBold, fontSize = FontSize, diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt index 46ed355abb..4ff068cee7 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt @@ -33,34 +33,36 @@ fun CalendarDay( val today = remember { LocalDate.now() } Box( - modifier = modifier - .then( - if (today == date) { - Modifier.border( - border = BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.onBackground - ), - shape = CircleShape, - ) - } else { - Modifier - }, - ) - .clip(shape = CircleShape) - .clickable(onClick = onDayClick) - .circleLayout(), + modifier = + modifier + .then( + if (today == date) { + Modifier.border( + border = + BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.onBackground, + ), + shape = CircleShape, + ) + } else { + Modifier + }, + ).clip(shape = CircleShape) + .clickable(onClick = onDayClick) + .circleLayout(), contentAlignment = Alignment.Center, ) { Text( text = date.dayOfMonth.toString(), textAlign = TextAlign.Center, fontSize = 16.sp, - color = if (date.isBefore(today)) { - MaterialTheme.colorScheme.onBackground.copy(alpha = 0.38f) - } else { - MaterialTheme.colorScheme.onBackground - }, + color = + if (date.isBefore(today)) { + MaterialTheme.colorScheme.onBackground.copy(alpha = 0.38f) + } else { + MaterialTheme.colorScheme.onBackground + }, fontWeight = FontWeight.SemiBold, ) Row(Modifier.offset(y = 12.dp)) { @@ -76,17 +78,18 @@ fun CalendarDay( } } -private fun Modifier.circleLayout() = layout { measurable, constraints -> - val placeable = measurable.measure(constraints) +private fun Modifier.circleLayout() = + layout { measurable, constraints -> + val placeable = measurable.measure(constraints) - val currentHeight = placeable.height - val currentWidth = placeable.width - val newDiameter = maxOf(currentHeight, currentWidth) + val currentHeight = placeable.height + val currentWidth = placeable.width + val newDiameter = maxOf(currentHeight, currentWidth) - layout(newDiameter, newDiameter) { - placeable.placeRelative( - x = (newDiameter - currentWidth) / 2, - y = (newDiameter - currentHeight) / 2, - ) + layout(newDiameter, newDiameter) { + placeable.placeRelative( + x = (newDiameter - currentWidth) / 2, + y = (newDiameter - currentHeight) / 2, + ) + } } -} diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt index 55498ebb90..3b5ccb97f8 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt @@ -68,16 +68,20 @@ private const val MonthYearChangeAnimationDuration = 200 private fun AnimatedContentTransitionScope.getAnimation(): ContentTransform { val movingForward = targetState > initialState - val enterTransition = slideInVertically( - animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), - ) { height -> if (movingForward) height else -height } + fadeIn( - animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), - ) - val exitTransition = slideOutVertically( - animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), - ) { height -> if (movingForward) -height else height } + fadeOut( - animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), - ) + val enterTransition = + slideInVertically( + animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), + ) { height -> if (movingForward) height else -height } + + fadeIn( + animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), + ) + val exitTransition = + slideOutVertically( + animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), + ) { height -> if (movingForward) -height else height } + + fadeOut( + animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), + ) return (enterTransition togetherWith exitTransition) .using(SizeTransform(clip = false)) } diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarIndicator.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarIndicator.kt index 9aaca69de0..b1c075fd24 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarIndicator.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarIndicator.kt @@ -23,10 +23,11 @@ fun CalendarIndicator( modifier: Modifier = Modifier, ) { Box( - modifier = modifier - .padding(horizontal = 1.dp) - .clip(shape = CircleShape) - .background(color = color.copy(alpha = (index + 1) * IndicatorAlphaMultiplier)) - .size(size = size.div(IndicatorScale)), + modifier = + modifier + .padding(horizontal = 1.dp) + .clip(shape = CircleShape) + .background(color = color.copy(alpha = (index + 1) * IndicatorAlphaMultiplier)) + .size(size = size.div(IndicatorScale)), ) } diff --git a/app/src/test/java/mihon/core/migration/MigratorTest.kt b/app/src/test/java/mihon/core/migration/MigratorTest.kt index a805b56302..9c1accb535 100644 --- a/app/src/test/java/mihon/core/migration/MigratorTest.kt +++ b/app/src/test/java/mihon/core/migration/MigratorTest.kt @@ -19,7 +19,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class MigratorTest { - lateinit var migrationCompletedListener: MigrationCompletedListener lateinit var migrationContext: MigrationContext lateinit var migrationJobFactory: MigrationJobFactory @@ -34,112 +33,119 @@ class MigratorTest { } @Test - fun initialVersion() = runBlocking { - val strategy = migrationStrategyFactory.create(0, 1) - assertInstanceOf(InitialMigrationStrategy::class.java, strategy) + fun initialVersion() = + runBlocking { + val strategy = migrationStrategyFactory.create(0, 1) + assertInstanceOf(InitialMigrationStrategy::class.java, strategy) - val migrations = slot>() - val execute = strategy(listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { false })) + val migrations = slot>() + val execute = strategy(listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { false })) - execute.await() + execute.await() - verify { migrationJobFactory.create(capture(migrations)) } - assertEquals(1, migrations.captured.size) - verify { migrationCompletedListener() } - } + verify { migrationJobFactory.create(capture(migrations)) } + assertEquals(1, migrations.captured.size) + verify { migrationCompletedListener() } + } @Test - fun sameVersion() = runBlocking { - val strategy = migrationStrategyFactory.create(1, 1) - assertInstanceOf(NoopMigrationStrategy::class.java, strategy) + fun sameVersion() = + runBlocking { + val strategy = migrationStrategyFactory.create(1, 1) + assertInstanceOf(NoopMigrationStrategy::class.java, strategy) - val execute = strategy(listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { false })) + val execute = strategy(listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { false })) - val result = execute.await() - assertFalse(result) + val result = execute.await() + assertFalse(result) - verify(exactly = 0) { migrationJobFactory.create(any()) } - } + verify(exactly = 0) { migrationJobFactory.create(any()) } + } @Test - fun noMigrations() = runBlocking { - val strategy = migrationStrategyFactory.create(1, 2) - assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) + fun noMigrations() = + runBlocking { + val strategy = migrationStrategyFactory.create(1, 2) + assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) - val execute = strategy(emptyList()) + val execute = strategy(emptyList()) - val result = execute.await() - assertFalse(result) + val result = execute.await() + assertFalse(result) - verify(exactly = 0) { migrationJobFactory.create(any()) } - } + verify(exactly = 0) { migrationJobFactory.create(any()) } + } @Test - fun smallMigration() = runBlocking { - val strategy = migrationStrategyFactory.create(1, 2) - assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) + fun smallMigration() = + runBlocking { + val strategy = migrationStrategyFactory.create(1, 2) + assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) - val migrations = slot>() - val execute = strategy(listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { true })) + val migrations = slot>() + val execute = strategy(listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { true })) - execute.await() + execute.await() - verify { migrationJobFactory.create(capture(migrations)) } - assertEquals(2, migrations.captured.size) - verify { migrationCompletedListener() } - } + verify { migrationJobFactory.create(capture(migrations)) } + assertEquals(2, migrations.captured.size) + verify { migrationCompletedListener() } + } @Test - fun largeMigration() = runBlocking { - val input = listOf( - Migration.of(Migration.ALWAYS) { true }, - Migration.of(2f) { true }, - Migration.of(3f) { true }, - Migration.of(4f) { true }, - Migration.of(5f) { true }, - Migration.of(6f) { true }, - Migration.of(7f) { true }, - Migration.of(8f) { true }, - Migration.of(9f) { true }, - Migration.of(10f) { true }, - ) - - val strategy = migrationStrategyFactory.create(1, 10) - assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) - - val migrations = slot>() - val execute = strategy(input) - - execute.await() - - verify { migrationJobFactory.create(capture(migrations)) } - assertEquals(10, migrations.captured.size) - verify { migrationCompletedListener() } - } + fun largeMigration() = + runBlocking { + val input = + listOf( + Migration.of(Migration.ALWAYS) { true }, + Migration.of(2f) { true }, + Migration.of(3f) { true }, + Migration.of(4f) { true }, + Migration.of(5f) { true }, + Migration.of(6f) { true }, + Migration.of(7f) { true }, + Migration.of(8f) { true }, + Migration.of(9f) { true }, + Migration.of(10f) { true }, + ) + + val strategy = migrationStrategyFactory.create(1, 10) + assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) + + val migrations = slot>() + val execute = strategy(input) + + execute.await() + + verify { migrationJobFactory.create(capture(migrations)) } + assertEquals(10, migrations.captured.size) + verify { migrationCompletedListener() } + } @Test - fun withinRangeMigration() = runBlocking { - val strategy = migrationStrategyFactory.create(1, 2) - assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) - - val migrations = slot>() - val execute = strategy( - listOf( - Migration.of(Migration.ALWAYS) { true }, - Migration.of(2f) { true }, - Migration.of(3f) { false } - ) - ) - - execute.await() - - verify { migrationJobFactory.create(capture(migrations)) } - assertEquals(2, migrations.captured.size) - verify { migrationCompletedListener() } - } + fun withinRangeMigration() = + runBlocking { + val strategy = migrationStrategyFactory.create(1, 2) + assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) + + val migrations = slot>() + val execute = + strategy( + listOf( + Migration.of(Migration.ALWAYS) { true }, + Migration.of(2f) { true }, + Migration.of(3f) { false }, + ), + ) + + execute.await() + + verify { migrationJobFactory.create(capture(migrations)) } + assertEquals(2, migrations.captured.size) + verify { migrationCompletedListener() } + } companion object { - val mainThreadSurrogate = newSingleThreadContext("UI thread") @BeforeAll diff --git a/buildSrc/src/main/kotlin/mihon/buildlogic/Commands.kt b/buildSrc/src/main/kotlin/mihon/buildlogic/Commands.kt index faa75e2b42..13e518dfd5 100644 --- a/buildSrc/src/main/kotlin/mihon/buildlogic/Commands.kt +++ b/buildSrc/src/main/kotlin/mihon/buildlogic/Commands.kt @@ -21,9 +21,7 @@ fun Project.getGitSha(): String { private val BUILD_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") @Suppress("UnusedReceiverParameter") -fun Project.getBuildTime(): String { - return LocalDateTime.now(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER) -} +fun Project.getBuildTime(): String = LocalDateTime.now(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER) private fun Project.runCommand(command: String): String { val byteOut = ByteArrayOutputStream() diff --git a/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt b/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt index 4b9e762b9a..ffc0b4525b 100644 --- a/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt +++ b/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt @@ -51,7 +51,6 @@ internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *, // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties val warningsAsErrors: String? by project allWarningsAsErrors.set(warningsAsErrors.toBoolean()) - } } @@ -61,7 +60,11 @@ internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *, } internal fun Project.configureCompose(commonExtension: CommonExtension<*, *, *, *, *, *>) { - pluginManager.apply(kotlinx.plugins.compose.compiler.get().pluginId) + pluginManager.apply( + kotlinx.plugins.compose.compiler + .get() + .pluginId, + ) commonExtension.apply { buildFeatures { @@ -81,10 +84,20 @@ internal fun Project.configureCompose(commonExtension: CommonExtension<*, *, *, // https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.9 enableNonSkippingGroupOptimization.set(true) - val enableMetrics = project.providers.gradleProperty("enableComposeCompilerMetrics").orNull.toBoolean() - val enableReports = project.providers.gradleProperty("enableComposeCompilerReports").orNull.toBoolean() - - val rootProjectDir = rootProject.layout.buildDirectory.asFile.get() + val enableMetrics = + project.providers + .gradleProperty("enableComposeCompilerMetrics") + .orNull + .toBoolean() + val enableReports = + project.providers + .gradleProperty("enableComposeCompilerReports") + .orNull + .toBoolean() + + val rootProjectDir = + rootProject.layout.buildDirectory.asFile + .get() val relativePath = projectDir.relativeTo(rootDir) if (enableMetrics) { val buildDirPath = rootProjectDir.resolve("compose-metrics").resolve(relativePath) @@ -95,7 +108,6 @@ internal fun Project.configureCompose(commonExtension: CommonExtension<*, *, *, reportsDestination.set(buildDirPath) } } - } internal fun Project.configureTest() { diff --git a/buildSrc/src/main/kotlin/mihon/buildlogic/tasks/LocalesConfigPlugin.kt b/buildSrc/src/main/kotlin/mihon/buildlogic/tasks/LocalesConfigPlugin.kt index d84d2cb64e..aa87e86006 100644 --- a/buildSrc/src/main/kotlin/mihon/buildlogic/tasks/LocalesConfigPlugin.kt +++ b/buildSrc/src/main/kotlin/mihon/buildlogic/tasks/LocalesConfigPlugin.kt @@ -6,32 +6,31 @@ import org.gradle.api.tasks.TaskProvider private val emptyResourcesElement = "\\s*|".toRegex() -fun Project.getLocalesConfigTask(): TaskProvider { - return tasks.register("generateLocalesConfig") { - val locales = fileTree("$projectDir/src/commonMain/moko-resources/") - .matching { include("**/strings.xml") } - .filterNot { it.readText().contains(emptyResourcesElement) } - .map { - it.parentFile.name - .replace("base", "en") - .replace("-r", "-") - .replace("+", "-") - .takeIf(String::isNotBlank) ?: "en" - } - .sorted() - .joinToString("\n") { "| " } +fun Project.getLocalesConfigTask(): TaskProvider = + tasks.register("generateLocalesConfig") { + val locales = + fileTree("$projectDir/src/commonMain/moko-resources/") + .matching { include("**/strings.xml") } + .filterNot { it.readText().contains(emptyResourcesElement) } + .map { + it.parentFile.name + .replace("base", "en") + .replace("-r", "-") + .replace("+", "-") + .takeIf(String::isNotBlank) ?: "en" + }.sorted() + .joinToString("\n") { "| " } - val content = """ + val content = + """ | | $locales | - """.trimMargin() + """.trimMargin() file("$projectDir/src/androidMain/res/xml/locales_config.xml").apply { parentFile.mkdirs() writeText(content) } } -} - diff --git a/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt index 5d4d922771..88861595f3 100644 --- a/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt +++ b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt @@ -8,27 +8,29 @@ import nl.adaptivity.xmlutil.serialization.XmlValue const val COMIC_INFO_FILE = "ComicInfo.xml" -fun SManga.getComicInfo() = ComicInfo( - series = ComicInfo.Series(title), - summary = description?.let { ComicInfo.Summary(it) }, - writer = author?.let { ComicInfo.Writer(it) }, - penciller = artist?.let { ComicInfo.Penciller(it) }, - genre = genre?.let { ComicInfo.Genre(it) }, - publishingStatus = ComicInfo.PublishingStatusTachiyomi( - ComicInfoPublishingStatus.toComicInfoValue(status.toLong()), - ), - title = null, - number = null, - web = null, - translator = null, - inker = null, - colorist = null, - letterer = null, - coverArtist = null, - tags = null, - categories = null, - source = null, -) +fun SManga.getComicInfo() = + ComicInfo( + series = ComicInfo.Series(title), + summary = description?.let { ComicInfo.Summary(it) }, + writer = author?.let { ComicInfo.Writer(it) }, + penciller = artist?.let { ComicInfo.Penciller(it) }, + genre = genre?.let { ComicInfo.Genre(it) }, + publishingStatus = + ComicInfo.PublishingStatusTachiyomi( + ComicInfoPublishingStatus.toComicInfoValue(status.toLong()), + ), + title = null, + number = null, + web = null, + translator = null, + inker = null, + colorist = null, + letterer = null, + coverArtist = null, + tags = null, + categories = null, + source = null, + ) fun SManga.copyFromComicInfo(comicInfo: ComicInfo) { comicInfo.series?.let { title = it.value } @@ -39,8 +41,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) { comicInfo.genre?.value, comicInfo.tags?.value, comicInfo.categories?.value, - ) - .distinct() + ).distinct() .joinToString(", ") { it.trim() } .takeIf { it.isNotEmpty() } ?.let { genre = it } @@ -51,8 +52,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) { comicInfo.colorist?.value, comicInfo.letterer?.value, comicInfo.coverArtist?.value, - ) - .flatMap { it.split(", ") } + ).flatMap { it.split(", ") } .distinct() .joinToString(", ") { it.trim() } .takeIf { it.isNotEmpty() } @@ -94,72 +94,106 @@ data class ComicInfo( @Serializable @XmlSerialName("Title", "", "") - data class Title(@XmlValue(true) val value: String = "") + data class Title( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Series", "", "") - data class Series(@XmlValue(true) val value: String = "") + data class Series( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Number", "", "") - data class Number(@XmlValue(true) val value: String = "") + data class Number( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Summary", "", "") - data class Summary(@XmlValue(true) val value: String = "") + data class Summary( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Writer", "", "") - data class Writer(@XmlValue(true) val value: String = "") + data class Writer( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Penciller", "", "") - data class Penciller(@XmlValue(true) val value: String = "") + data class Penciller( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Inker", "", "") - data class Inker(@XmlValue(true) val value: String = "") + data class Inker( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Colorist", "", "") - data class Colorist(@XmlValue(true) val value: String = "") + data class Colorist( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Letterer", "", "") - data class Letterer(@XmlValue(true) val value: String = "") + data class Letterer( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("CoverArtist", "", "") - data class CoverArtist(@XmlValue(true) val value: String = "") + data class CoverArtist( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Translator", "", "") - data class Translator(@XmlValue(true) val value: String = "") + data class Translator( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Genre", "", "") - data class Genre(@XmlValue(true) val value: String = "") + data class Genre( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Tags", "", "") - data class Tags(@XmlValue(true) val value: String = "") + data class Tags( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Web", "", "") - data class Web(@XmlValue(true) val value: String = "") + data class Web( + @XmlValue(true) val value: String = "", + ) // The spec doesn't have a good field for this @Serializable @XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty") - data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "") + data class PublishingStatusTachiyomi( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty") - data class CategoriesTachiyomi(@XmlValue(true) val value: String = "") + data class CategoriesTachiyomi( + @XmlValue(true) val value: String = "", + ) @Serializable @XmlSerialName("SourceMihon", "http://www.w3.org/2001/XMLSchema", "mh") - data class SourceMihon(@XmlValue(true) val value: String = "") + data class SourceMihon( + @XmlValue(true) val value: String = "", + ) } enum class ComicInfoPublishingStatus( @@ -176,14 +210,12 @@ enum class ComicInfoPublishingStatus( ; companion object { - fun toComicInfoValue(value: Long): String { - return entries.firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue + fun toComicInfoValue(value: Long): String = + entries.firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue ?: UNKNOWN.comicInfoValue - } - fun toSMangaValue(value: String?): Int { - return entries.firstOrNull { it.comicInfoValue == value }?.sMangaModelValue + fun toSMangaValue(value: String?): Int = + entries.firstOrNull { it.comicInfoValue == value }?.sMangaModelValue ?: UNKNOWN.sMangaModelValue - } } } diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index d00fec6822..452013ef13 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -8,11 +8,12 @@ android { namespace = "eu.kanade.tachiyomi.core.common" kotlinOptions { - freeCompilerArgs += listOf( - "-Xcontext-receivers", - "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", - "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", - ) + freeCompilerArgs += + listOf( + "-Xcontext-receivers", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", + ) } } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt index 274f0c5928..2962fd02ab 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt @@ -9,7 +9,6 @@ import tachiyomi.i18n.MR class SecurityPreferences( private val preferenceStore: PreferenceStore, ) { - fun useAuthenticator() = preferenceStore.getBoolean("use_biometric_lock", false) fun lockAppAfter() = preferenceStore.getInt("lock_app_after", 0) @@ -22,12 +21,15 @@ class SecurityPreferences( * For app lock. Will be set when there is a pending timed lock. * Otherwise this pref should be deleted. */ - fun lastAppClosed() = preferenceStore.getLong( - Preference.appStateKey("last_app_closed"), - 0, - ) - - enum class SecureScreenMode(val titleRes: StringResource) { + fun lastAppClosed() = + preferenceStore.getLong( + Preference.appStateKey("last_app_closed"), + 0, + ) + + enum class SecureScreenMode( + val titleRes: StringResource, + ) { ALWAYS(MR.strings.lock_always), INCOGNITO(MR.strings.pref_incognito_mode), NEVER(MR.strings.lock_never), diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt index f9322e840f..731699102a 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt @@ -6,18 +6,18 @@ import okhttp3.CookieJar import okhttp3.HttpUrl class AndroidCookieJar : CookieJar { - private val manager = CookieManager.getInstance() - override fun saveFromResponse(url: HttpUrl, cookies: List) { + override fun saveFromResponse( + url: HttpUrl, + cookies: List, + ) { val urlString = url.toString() cookies.forEach { manager.setCookie(urlString, it.toString()) } } - override fun loadForRequest(url: HttpUrl): List { - return get(url) - } + override fun loadForRequest(url: HttpUrl): List = get(url) fun get(url: HttpUrl): List { val cookies = manager.getCookie(url.toString()) @@ -29,19 +29,23 @@ class AndroidCookieJar : CookieJar { } } - fun remove(url: HttpUrl, cookieNames: List? = null, maxAge: Int = -1): Int { + fun remove( + url: HttpUrl, + cookieNames: List? = null, + maxAge: Int = -1, + ): Int { val urlString = url.toString() val cookies = manager.getCookie(urlString) ?: return 0 - fun List.filterNames(): List { - return if (cookieNames != null) { + fun List.filterNames(): List = + if (cookieNames != null) { this.filter { it in cookieNames } } else { this } - } - return cookies.split(";") + return cookies + .split(";") .map { it.substringBefore("=") } .filterNames() .onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt index e6995448bf..9dae10a128 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt @@ -22,164 +22,188 @@ const val PREF_DOH_CONTROLD = 10 const val PREF_DOH_NJALLA = 11 const val PREF_DOH_SHECAN = 12 -fun OkHttpClient.Builder.dohCloudflare() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://cloudflare-dns.com/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("162.159.36.1"), - InetAddress.getByName("162.159.46.1"), - InetAddress.getByName("1.1.1.1"), - InetAddress.getByName("1.0.0.1"), - InetAddress.getByName("162.159.132.53"), - InetAddress.getByName("2606:4700:4700::1111"), - InetAddress.getByName("2606:4700:4700::1001"), - InetAddress.getByName("2606:4700:4700::0064"), - InetAddress.getByName("2606:4700:4700::6400"), - ) - .build(), -) +fun OkHttpClient.Builder.dohCloudflare() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://cloudflare-dns.com/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("162.159.36.1"), + InetAddress.getByName("162.159.46.1"), + InetAddress.getByName("1.1.1.1"), + InetAddress.getByName("1.0.0.1"), + InetAddress.getByName("162.159.132.53"), + InetAddress.getByName("2606:4700:4700::1111"), + InetAddress.getByName("2606:4700:4700::1001"), + InetAddress.getByName("2606:4700:4700::0064"), + InetAddress.getByName("2606:4700:4700::6400"), + ).build(), + ) -fun OkHttpClient.Builder.dohGoogle() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://dns.google/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("8.8.4.4"), - InetAddress.getByName("8.8.8.8"), - InetAddress.getByName("2001:4860:4860::8888"), - InetAddress.getByName("2001:4860:4860::8844"), - ) - .build(), -) +fun OkHttpClient.Builder.dohGoogle() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://dns.google/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("8.8.4.4"), + InetAddress.getByName("8.8.8.8"), + InetAddress.getByName("2001:4860:4860::8888"), + InetAddress.getByName("2001:4860:4860::8844"), + ).build(), + ) // AdGuard "Default" DNS works too but for the sake of making sure no site is blacklisted, // we use "Unfiltered" -fun OkHttpClient.Builder.dohAdGuard() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://dns-unfiltered.adguard.com/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("94.140.14.140"), - InetAddress.getByName("94.140.14.141"), - InetAddress.getByName("2a10:50c0::1:ff"), - InetAddress.getByName("2a10:50c0::2:ff"), - ) - .build(), -) +fun OkHttpClient.Builder.dohAdGuard() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://dns-unfiltered.adguard.com/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("94.140.14.140"), + InetAddress.getByName("94.140.14.141"), + InetAddress.getByName("2a10:50c0::1:ff"), + InetAddress.getByName("2a10:50c0::2:ff"), + ).build(), + ) -fun OkHttpClient.Builder.dohQuad9() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://dns.quad9.net/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("9.9.9.9"), - InetAddress.getByName("149.112.112.112"), - InetAddress.getByName("2620:fe::fe"), - InetAddress.getByName("2620:fe::9"), - ) - .build(), -) +fun OkHttpClient.Builder.dohQuad9() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://dns.quad9.net/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("9.9.9.9"), + InetAddress.getByName("149.112.112.112"), + InetAddress.getByName("2620:fe::fe"), + InetAddress.getByName("2620:fe::9"), + ).build(), + ) -fun OkHttpClient.Builder.dohAliDNS() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://dns.alidns.com/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("223.5.5.5"), - InetAddress.getByName("223.6.6.6"), - InetAddress.getByName("2400:3200::1"), - InetAddress.getByName("2400:3200:baba::1"), - ) - .build(), -) +fun OkHttpClient.Builder.dohAliDNS() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://dns.alidns.com/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("223.5.5.5"), + InetAddress.getByName("223.6.6.6"), + InetAddress.getByName("2400:3200::1"), + InetAddress.getByName("2400:3200:baba::1"), + ).build(), + ) -fun OkHttpClient.Builder.dohDNSPod() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://doh.pub/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("1.12.12.12"), - InetAddress.getByName("120.53.53.53"), - ) - .build(), -) +fun OkHttpClient.Builder.dohDNSPod() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://doh.pub/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("1.12.12.12"), + InetAddress.getByName("120.53.53.53"), + ).build(), + ) -fun OkHttpClient.Builder.doh360() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://doh.360.cn/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("101.226.4.6"), - InetAddress.getByName("218.30.118.6"), - InetAddress.getByName("123.125.81.6"), - InetAddress.getByName("140.207.198.6"), - InetAddress.getByName("180.163.249.75"), - InetAddress.getByName("101.199.113.208"), - InetAddress.getByName("36.99.170.86"), - ) - .build(), -) +fun OkHttpClient.Builder.doh360() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://doh.360.cn/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("101.226.4.6"), + InetAddress.getByName("218.30.118.6"), + InetAddress.getByName("123.125.81.6"), + InetAddress.getByName("140.207.198.6"), + InetAddress.getByName("180.163.249.75"), + InetAddress.getByName("101.199.113.208"), + InetAddress.getByName("36.99.170.86"), + ).build(), + ) -fun OkHttpClient.Builder.dohQuad101() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://dns.twnic.tw/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("101.101.101.101"), - InetAddress.getByName("2001:de4::101"), - InetAddress.getByName("2001:de4::102"), - ) - .build(), -) +fun OkHttpClient.Builder.dohQuad101() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://dns.twnic.tw/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("101.101.101.101"), + InetAddress.getByName("2001:de4::101"), + InetAddress.getByName("2001:de4::102"), + ).build(), + ) /* * Mullvad DoH * without ad blocking option * Source: https://mullvad.net/en/help/dns-over-https-and-dns-over-tls */ -fun OkHttpClient.Builder.dohMullvad() = dns( - DnsOverHttps.Builder().client(build()) - .url(" https://dns.mullvad.net/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("194.242.2.2"), - InetAddress.getByName("2a07:e340::2"), - ) - .build(), -) +fun OkHttpClient.Builder.dohMullvad() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url(" https://dns.mullvad.net/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("194.242.2.2"), + InetAddress.getByName("2a07:e340::2"), + ).build(), + ) /* * Control D * unfiltered option * Source: https://controld.com/free-dns/? */ -fun OkHttpClient.Builder.dohControlD() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://freedns.controld.com/p0".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("76.76.2.0"), - InetAddress.getByName("76.76.10.0"), - InetAddress.getByName("2606:1a40::"), - InetAddress.getByName("2606:1a40:1::"), - ) - .build(), -) +fun OkHttpClient.Builder.dohControlD() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://freedns.controld.com/p0".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("76.76.2.0"), + InetAddress.getByName("76.76.10.0"), + InetAddress.getByName("2606:1a40::"), + InetAddress.getByName("2606:1a40:1::"), + ).build(), + ) /* * Njalla * Non logging and uncensored */ -fun OkHttpClient.Builder.dohNajalla() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://dns.njal.la/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("95.215.19.53"), - InetAddress.getByName("2001:67c:2354:2::53"), - ) - .build(), -) +fun OkHttpClient.Builder.dohNajalla() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://dns.njal.la/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("95.215.19.53"), + InetAddress.getByName("2001:67c:2354:2::53"), + ).build(), + ) /** * Source: https://shecan.ir/ */ -fun OkHttpClient.Builder.dohShecan() = dns( - DnsOverHttps.Builder().client(build()) - .url("https://free.shecan.ir/dns-query".toHttpUrl()) - .bootstrapDnsHosts( - InetAddress.getByName("178.22.122.100"), - InetAddress.getByName("185.51.200.2"), - ) - .build(), -) +fun OkHttpClient.Builder.dohShecan() = + dns( + DnsOverHttps + .Builder() + .client(build()) + .url("https://free.shecan.ir/dns-query".toHttpUrl()) + .bootstrapDnsHosts( + InetAddress.getByName("178.22.122.100"), + InetAddress.getByName("185.51.200.2"), + ).build(), + ) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt index 5c78946060..a0cf575a51 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt @@ -8,8 +8,9 @@ import tachiyomi.core.common.util.lang.withIOContext * Util for evaluating JavaScript in sources. */ @Suppress("UNUSED", "UNCHECKED_CAST") -class JavaScriptEngine(context: Context) { - +class JavaScriptEngine( + context: Context, +) { /** * Evaluate arbitrary JavaScript code and get the result as a primtive type * (e.g., String, Int). @@ -18,9 +19,10 @@ class JavaScriptEngine(context: Context) { * @param script JavaScript to execute. * @return Result of JavaScript code as a primitive type. */ - suspend fun evaluate(script: String): T = withIOContext { - QuickJs.create().use { - it.evaluate(script) as T + suspend fun evaluate(script: String): T = + withIOContext { + QuickJs.create().use { + it.evaluate(script) as T + } } - } } 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 5516ed748a..2191909cd2 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 @@ -16,54 +16,56 @@ class NetworkHelper( private val context: Context, private val preferences: NetworkPreferences, ) { - val cookieJar = AndroidCookieJar() - val client: OkHttpClient = run { - val builder = OkHttpClient.Builder() - .cookieJar(cookieJar) - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .callTimeout(2, TimeUnit.MINUTES) - .cache( - Cache( - directory = File(context.cacheDir, "network_cache"), - maxSize = 5L * 1024 * 1024, // 5 MiB - ), - ) - .addInterceptor(UncaughtExceptionInterceptor()) - .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider)) - .addNetworkInterceptor(IgnoreGzipInterceptor()) - .addNetworkInterceptor(BrotliInterceptor) + val client: OkHttpClient = + run { + val builder = + OkHttpClient + .Builder() + .cookieJar(cookieJar) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .callTimeout(2, TimeUnit.MINUTES) + .cache( + Cache( + directory = File(context.cacheDir, "network_cache"), + maxSize = 5L * 1024 * 1024, // 5 MiB + ), + ).addInterceptor(UncaughtExceptionInterceptor()) + .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider)) + .addNetworkInterceptor(IgnoreGzipInterceptor()) + .addNetworkInterceptor(BrotliInterceptor) - if (preferences.verboseLogging().get()) { - val httpLoggingInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.HEADERS + if (preferences.verboseLogging().get()) { + val httpLoggingInterceptor = + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.HEADERS + } + builder.addNetworkInterceptor(httpLoggingInterceptor) } - builder.addNetworkInterceptor(httpLoggingInterceptor) - } - builder.addInterceptor( - CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider), - ) + builder.addInterceptor( + CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider), + ) - when (preferences.dohProvider().get()) { - PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() - PREF_DOH_GOOGLE -> builder.dohGoogle() - PREF_DOH_ADGUARD -> builder.dohAdGuard() - PREF_DOH_QUAD9 -> builder.dohQuad9() - PREF_DOH_ALIDNS -> builder.dohAliDNS() - PREF_DOH_DNSPOD -> builder.dohDNSPod() - PREF_DOH_360 -> builder.doh360() - PREF_DOH_QUAD101 -> builder.dohQuad101() - PREF_DOH_MULLVAD -> builder.dohMullvad() - PREF_DOH_CONTROLD -> builder.dohControlD() - PREF_DOH_NJALLA -> builder.dohNajalla() - PREF_DOH_SHECAN -> builder.dohShecan() - } + when (preferences.dohProvider().get()) { + PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() + PREF_DOH_GOOGLE -> builder.dohGoogle() + PREF_DOH_ADGUARD -> builder.dohAdGuard() + PREF_DOH_QUAD9 -> builder.dohQuad9() + PREF_DOH_ALIDNS -> builder.dohAliDNS() + PREF_DOH_DNSPOD -> builder.dohDNSPod() + PREF_DOH_360 -> builder.doh360() + PREF_DOH_QUAD101 -> builder.dohQuad101() + PREF_DOH_MULLVAD -> builder.dohMullvad() + PREF_DOH_CONTROLD -> builder.dohControlD() + PREF_DOH_NJALLA -> builder.dohNajalla() + PREF_DOH_SHECAN -> builder.dohShecan() + } - builder.build() - } + builder.build() + } /** * @deprecated Since extension-lib 1.5 diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt index c32864aec6..9c63a6ebe0 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -7,19 +7,13 @@ class NetworkPreferences( private val preferenceStore: PreferenceStore, private val verboseLogging: Boolean = false, ) { + fun verboseLogging(): Preference = preferenceStore.getBoolean("verbose_logging", verboseLogging) - fun verboseLogging(): Preference { - return preferenceStore.getBoolean("verbose_logging", verboseLogging) - } + fun dohProvider(): Preference = preferenceStore.getInt("doh_provider", -1) - fun dohProvider(): Preference { - return preferenceStore.getInt("doh_provider", -1) - } - - fun defaultUserAgent(): Preference { - return preferenceStore.getString( + fun defaultUserAgent(): Preference = + preferenceStore.getString( "default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", ) - } } 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 e6eec02f4a..8be6577e5e 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 @@ -27,45 +27,43 @@ fun Call.asObservable(): Observable { val call = clone() // Wrap the call in a helper which handles both unsubscription and backpressure. - val requestArbiter = object : AtomicBoolean(), Producer, Subscription { - override fun request(n: Long) { - if (n == 0L || !compareAndSet(false, true)) return - - try { - val response = call.execute() - if (!subscriber.isUnsubscribed) { - subscriber.onNext(response) - subscriber.onCompleted() - } - } catch (e: Exception) { - if (!subscriber.isUnsubscribed) { - subscriber.onError(e) + val requestArbiter = + object : AtomicBoolean(), Producer, Subscription { + override fun request(n: Long) { + if (n == 0L || !compareAndSet(false, true)) return + + try { + val response = call.execute() + if (!subscriber.isUnsubscribed) { + subscriber.onNext(response) + subscriber.onCompleted() + } + } catch (e: Exception) { + if (!subscriber.isUnsubscribed) { + subscriber.onError(e) + } } } - } - override fun unsubscribe() { - call.cancel() - } + override fun unsubscribe() { + call.cancel() + } - override fun isUnsubscribed(): Boolean { - return call.isCanceled() + override fun isUnsubscribed(): Boolean = call.isCanceled() } - } subscriber.add(requestArbiter) subscriber.setProducer(requestArbiter) } } -fun Call.asObservableSuccess(): Observable { - return asObservable().doOnNext { response -> +fun Call.asObservableSuccess(): Observable = + asObservable().doOnNext { response -> if (!response.isSuccessful) { response.close() throw HttpException(response.code) } } -} // Based on https://github.com/gildor/kotlin-coroutines-okhttp @OptIn(ExperimentalCoroutinesApi::class) @@ -73,13 +71,19 @@ private suspend fun Call.await(callStack: Array): Response { return suspendCancellableCoroutine { continuation -> val callback = object : Callback { - override fun onResponse(call: Call, response: Response) { + override fun onResponse( + call: Call, + response: Response, + ) { continuation.resume(response) { response.body.close() } } - override fun onFailure(call: Call, e: IOException) { + 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 } @@ -117,34 +121,35 @@ suspend fun Call.awaitSuccess(): Response { return response } -fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call { - val progressClient = newBuilder() - .cache(null) - .addNetworkInterceptor { chain -> - val originalResponse = chain.proceed(chain.request()) - originalResponse.newBuilder() - .body(ProgressResponseBody(originalResponse.body, listener)) - .build() - } - .build() +fun OkHttpClient.newCachelessCallWithProgress( + request: Request, + listener: ProgressListener, +): Call { + val progressClient = + newBuilder() + .cache(null) + .addNetworkInterceptor { chain -> + val originalResponse = chain.proceed(chain.request()) + originalResponse + .newBuilder() + .body(ProgressResponseBody(originalResponse.body, listener)) + .build() + }.build() return progressClient.newCall(request) } context(Json) -inline fun Response.parseAs(): T { - return decodeFromJsonResponse(serializer(), this) -} +inline fun Response.parseAs(): T = decodeFromJsonResponse(serializer(), this) context(Json) fun decodeFromJsonResponse( deserializer: DeserializationStrategy, response: Response, -): T { - return response.body.source().use { +): T = + response.body.source().use { decodeFromBufferedSource(deserializer, it) } -} /** * Exception that handles HTTP codes considered not successful by OkHttp. @@ -153,4 +158,6 @@ fun decodeFromJsonResponse( * @since extensions-lib 1.5 * @param code [Int] the HTTP status code */ -class HttpException(val code: Int) : IllegalStateException("HTTP error $code") +class HttpException( + val code: Int, +) : IllegalStateException("HTTP error $code") diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt index 2e219895fe..6d8da08903 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt @@ -1,5 +1,9 @@ package eu.kanade.tachiyomi.network interface ProgressListener { - fun update(bytesRead: Long, contentLength: Long, done: Boolean) + fun update( + bytesRead: Long, + contentLength: Long, + done: Boolean, + ) } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt index 6ba53b1977..abc75ced1a 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt @@ -13,29 +13,25 @@ class ProgressResponseBody( private val responseBody: ResponseBody, private val progressListener: ProgressListener, ) : ResponseBody() { - private val bufferedSource: BufferedSource by lazy { source(responseBody.source()).buffer() } - override fun contentType(): MediaType? { - return responseBody.contentType() - } + override fun contentType(): MediaType? = responseBody.contentType() - override fun contentLength(): Long { - return responseBody.contentLength() - } + override fun contentLength(): Long = responseBody.contentLength() - override fun source(): BufferedSource { - return bufferedSource - } + override fun source(): BufferedSource = bufferedSource private fun source(source: Source): Source { return object : ForwardingSource(source) { var totalBytesRead = 0L @Throws(IOException::class) - override fun read(sink: Buffer, byteCount: Long): Long { + override fun read( + sink: Buffer, + byteCount: Long, + ): Long { val bytesRead = super.read(sink, byteCount) // read() returns the number of bytes read, or -1 if this source is exhausted. totalBytesRead += if (bytesRead != -1L) bytesRead else 0 diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt index 6adb0de8ef..eefd9dea08 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt @@ -17,9 +17,7 @@ fun GET( url: String, headers: Headers = DEFAULT_HEADERS, cache: CacheControl = DEFAULT_CACHE_CONTROL, -): Request { - return GET(url.toHttpUrl(), headers, cache) -} +): Request = GET(url.toHttpUrl(), headers, cache) /** * @since extensions-lib 1.4 @@ -28,52 +26,52 @@ fun GET( url: HttpUrl, headers: Headers = DEFAULT_HEADERS, cache: CacheControl = DEFAULT_CACHE_CONTROL, -): Request { - return Request.Builder() +): Request = + Request + .Builder() .url(url) .headers(headers) .cacheControl(cache) .build() -} fun POST( url: String, headers: Headers = DEFAULT_HEADERS, body: RequestBody = DEFAULT_BODY, cache: CacheControl = DEFAULT_CACHE_CONTROL, -): Request { - return Request.Builder() +): Request = + Request + .Builder() .url(url) .post(body) .headers(headers) .cacheControl(cache) .build() -} fun PUT( url: String, headers: Headers = DEFAULT_HEADERS, body: RequestBody = DEFAULT_BODY, cache: CacheControl = DEFAULT_CACHE_CONTROL, -): Request { - return Request.Builder() +): Request = + Request + .Builder() .url(url) .put(body) .headers(headers) .cacheControl(cache) .build() -} fun DELETE( url: String, headers: Headers = DEFAULT_HEADERS, body: RequestBody = DEFAULT_BODY, cache: CacheControl = DEFAULT_CACHE_CONTROL, -): Request { - return Request.Builder() +): Request = + Request + .Builder() .url(url) .delete(body) .headers(headers) .cacheControl(cache) .build() -} diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 6a765c680e..f054f13b05 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -24,7 +24,6 @@ class CloudflareInterceptor( private val cookieManager: AndroidCookieJar, defaultUserAgentProvider: () -> String, ) : WebViewInterceptor(context, defaultUserAgentProvider) { - private val executor = ContextCompat.getMainExecutor(context) override fun shouldIntercept(response: Response): Boolean { @@ -40,15 +39,16 @@ class CloudflareInterceptor( try { response.close() cookieManager.remove(request.url, COOKIE_NAMES, 0) - val oldCookie = cookieManager.get(request.url) - .firstOrNull { it.name == "cf_clearance" } + val oldCookie = + cookieManager + .get(request.url) + .firstOrNull { it.name == "cf_clearance" } resolveWithWebView(request, oldCookie) return chain.proceed(request) } // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that - // we don't crash the entire app - catch (e: CloudflareBypassException) { + // we don't crash the entire app catch (e: CloudflareBypassException) { throw IOException(context.stringResource(MR.strings.information_cloudflare_bypass_failure), e) } catch (e: Exception) { throw IOException(e) @@ -56,7 +56,10 @@ class CloudflareInterceptor( } @SuppressLint("SetJavaScriptEnabled") - private fun resolveWithWebView(originalRequest: Request, oldCookie: Cookie?) { + private fun resolveWithWebView( + originalRequest: Request, + oldCookie: Cookie?, + ) { // We need to lock this thread until the WebView finds the challenge solution url, because // OkHttp doesn't support asynchronous interceptors. val latch = CountDownLatch(1) @@ -73,43 +76,47 @@ class CloudflareInterceptor( executor.execute { webview = createWebView(originalRequest) - webview?.webViewClient = object : WebViewClientCompat() { - override fun onPageFinished(view: WebView, url: String) { - fun isCloudFlareBypassed(): Boolean { - return cookieManager.get(origRequestUrl.toHttpUrl()) - .firstOrNull { it.name == "cf_clearance" } - .let { it != null && it != oldCookie } - } - - if (isCloudFlareBypassed()) { - cloudflareBypassed = true - latch.countDown() - } + webview?.webViewClient = + object : WebViewClientCompat() { + override fun onPageFinished( + view: WebView, + url: String, + ) { + fun isCloudFlareBypassed(): Boolean = + cookieManager + .get(origRequestUrl.toHttpUrl()) + .firstOrNull { it.name == "cf_clearance" } + .let { it != null && it != oldCookie } + + if (isCloudFlareBypassed()) { + cloudflareBypassed = true + latch.countDown() + } - if (url == origRequestUrl && !challengeFound) { - // The first request didn't return the challenge, abort. - latch.countDown() + if (url == origRequestUrl && !challengeFound) { + // The first request didn't return the challenge, abort. + latch.countDown() + } } - } - override fun onReceivedErrorCompat( - view: WebView, - errorCode: Int, - description: String?, - failingUrl: String, - isMainFrame: Boolean, - ) { - if (isMainFrame) { - if (errorCode in ERROR_CODES) { - // Found the Cloudflare challenge page. - challengeFound = true - } else { - // Unlock thread, the challenge wasn't found. - latch.countDown() + override fun onReceivedErrorCompat( + view: WebView, + errorCode: Int, + description: String?, + failingUrl: String, + isMainFrame: Boolean, + ) { + if (isMainFrame) { + if (errorCode in ERROR_CODES) { + // Found the Cloudflare challenge page. + challengeFound = true + } else { + // Unlock thread, the challenge wasn't found. + latch.countDown() + } } } } - } webview?.loadUrl(origRequestUrl, headers) } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt index eab5ec302a..9df3874091 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt @@ -50,8 +50,10 @@ fun OkHttpClient.Builder.rateLimit( * @param permits [Int] Number of requests allowed within a period of units. * @param period [Duration] The limiting duration. Defaults to 1.seconds. */ -fun OkHttpClient.Builder.rateLimit(permits: Int, period: Duration = 1.seconds) = - addInterceptor(RateLimitInterceptor(null, permits, period)) +fun OkHttpClient.Builder.rateLimit( + permits: Int, + period: Duration = 1.seconds, +) = addInterceptor(RateLimitInterceptor(null, permits, period)) /** We can probably accept domains or wildcards by comparing with [endsWith], etc. */ @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") @@ -60,7 +62,6 @@ internal class RateLimitInterceptor( private val permits: Int, period: Duration, ) : Interceptor { - private val requestQueue = ArrayDeque(permits) private val rateLimitMillis = period.inWholeMilliseconds private val fairLock = Semaphore(1, true) @@ -98,7 +99,8 @@ internal class RateLimitInterceptor( } else if (hasRemovedExpired) { break } else { - try { // wait for the first entry to expire, or notified by cached response + try { + // wait for the first entry to expire, or notified by cached response (requestQueue as Object).wait(requestQueue.first - periodStart) } catch (_: InterruptedException) { continue diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt index 9f860faab8..e959cd54da 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt @@ -73,5 +73,8 @@ fun OkHttpClient.Builder.rateLimitHost( * @param period [Duration] The limiting duration. Defaults to 1.seconds. */ @Suppress("UNUSED") -fun OkHttpClient.Builder.rateLimitHost(url: String, permits: Int, period: Duration = 1.seconds) = - addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period)) +fun OkHttpClient.Builder.rateLimitHost( + url: String, + permits: Int, + period: Duration = 1.seconds, +) = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period)) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt index 1de824381b..bca1090123 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt @@ -13,9 +13,8 @@ import java.io.IOException * See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/ */ class UncaughtExceptionInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - return try { + override fun intercept(chain: Interceptor.Chain): Response = + try { chain.proceed(chain.request()) } catch (e: Exception) { if (e is IOException) { @@ -24,5 +23,4 @@ class UncaughtExceptionInterceptor : Interceptor { throw IOException(e) } } - } } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt index b085ece3c1..78675d6d31 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt @@ -6,16 +6,16 @@ import okhttp3.Response class UserAgentInterceptor( private val defaultUserAgentProvider: () -> String, ) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() return if (originalRequest.header("User-Agent").isNullOrEmpty()) { - val newRequest = originalRequest - .newBuilder() - .removeHeader("User-Agent") - .addHeader("User-Agent", defaultUserAgentProvider()) - .build() + val newRequest = + originalRequest + .newBuilder() + .removeHeader("User-Agent") + .addHeader("User-Agent", defaultUserAgentProvider()) + .build() chain.proceed(newRequest) } else { chain.proceed(originalRequest) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt index e18fa42997..e49c2fe999 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt @@ -23,7 +23,6 @@ abstract class WebViewInterceptor( private val context: Context, private val defaultUserAgentProvider: () -> String, ) : Interceptor { - /** * When this is called, it initializes the WebView if it wasn't already. We use this to avoid * blocking the main thread too much. If used too often we could consider moving it to the @@ -46,7 +45,11 @@ abstract class WebViewInterceptor( abstract fun shouldIntercept(response: Response): Boolean - abstract fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response + abstract fun intercept( + chain: Interceptor.Chain, + request: Request, + response: Response, + ): Response override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() @@ -66,38 +69,48 @@ abstract class WebViewInterceptor( return intercept(chain, request, response) } - fun parseHeaders(headers: Headers): Map { - return headers + fun parseHeaders(headers: Headers): Map = + headers // Keeping unsafe header makes webview throw [net::ERR_INVALID_ARGUMENT] .filter { (name, value) -> isRequestHeaderSafe(name, value) - } - .groupBy(keySelector = { (name, _) -> name }) { (_, value) -> value } + }.groupBy(keySelector = { (name, _) -> name }) { (_, value) -> value } .mapValues { it.value.getOrNull(0).orEmpty() } - } fun CountDownLatch.awaitFor30Seconds() { await(30, TimeUnit.SECONDS) } - fun createWebView(request: Request): WebView { - return WebView(context).apply { + fun createWebView(request: Request): WebView = + WebView(context).apply { setDefaultSettings() // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty settings.userAgentString = request.header("User-Agent") ?: defaultUserAgentProvider() } - } } // Based on [IsRequestHeaderSafe] in // https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc -private fun isRequestHeaderSafe(_name: String, _value: String): Boolean { +private fun isRequestHeaderSafe( + _name: String, + _value: String, +): Boolean { val name = _name.lowercase(Locale.ENGLISH) val value = _value.lowercase(Locale.ENGLISH) if (name in unsafeHeaderNames || name.startsWith("proxy-")) return false if (name == "connection" && value == "upgrade") return false return true } -private val unsafeHeaderNames = listOf( - "content-length", "host", "trailer", "te", "upgrade", "cookie2", "keep-alive", "transfer-encoding", "set-cookie", -) + +private val unsafeHeaderNames = + listOf( + "content-length", + "host", + "trailer", + "te", + "upgrade", + "cookie2", + "keep-alive", + "transfer-encoding", + "set-cookie", + ) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/lang/Hash.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/lang/Hash.kt index 32d2a2d2e6..cdb3f17ab4 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/lang/Hash.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/lang/Hash.kt @@ -3,31 +3,37 @@ package eu.kanade.tachiyomi.util.lang import java.security.MessageDigest object Hash { - - private val chars = charArrayOf( - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f', - ) + private val chars = + charArrayOf( + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + ) private val MD5 get() = MessageDigest.getInstance("MD5") private val SHA256 get() = MessageDigest.getInstance("SHA-256") - fun sha256(bytes: ByteArray): String { - return encodeHex(SHA256.digest(bytes)) - } + fun sha256(bytes: ByteArray): String = encodeHex(SHA256.digest(bytes)) - fun sha256(string: String): String { - return sha256(string.toByteArray()) - } + fun sha256(string: String): String = sha256(string.toByteArray()) - fun md5(bytes: ByteArray): String { - return encodeHex(MD5.digest(bytes)) - } + fun md5(bytes: ByteArray): String = encodeHex(MD5.digest(bytes)) - fun md5(string: String): String { - return md5(string.toByteArray()) - } + fun md5(string: String): String = md5(string.toByteArray()) private fun encodeHex(data: ByteArray): String { val l = data.size diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/lang/StringExtensions.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/lang/StringExtensions.kt index 97bb916802..695dca3385 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/lang/StringExtensions.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/lang/StringExtensions.kt @@ -9,19 +9,24 @@ import kotlin.math.floor * Replaces the given string to have at most [count] characters using [replacement] at its end. * If [replacement] is longer than [count] an exception will be thrown when `length > count`. */ -fun String.chop(count: Int, replacement: String = "…"): String { - return if (length > count) { +fun String.chop( + count: Int, + replacement: String = "…", +): String = + if (length > count) { take(count - replacement.length) + replacement } else { this } -} /** * Replaces the given string to have at most [count] characters using [replacement] near the center. * If [replacement] is longer than [count] an exception will be thrown when `length > count`. */ -fun String.truncateCenter(count: Int, replacement: String = "..."): String { +fun String.truncateCenter( + count: Int, + replacement: String = "...", +): String { if (length <= count) { return this } @@ -42,9 +47,7 @@ fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int { /** * Returns the size of the string as the number of bytes. */ -fun String.byteSize(): Int { - return toByteArray(StandardCharsets.UTF_8).size -} +fun String.byteSize(): Int = toByteArray(StandardCharsets.UTF_8).size /** * Returns a string containing the first [n] bytes from this string, or the entire string if this @@ -62,6 +65,4 @@ fun String.takeBytes(n: Int): String { /** * HTML-decode the string */ -fun String.htmlDecode(): String { - return this.parseAsHtml().toString() -} +fun String.htmlDecode(): String = this.parseAsHtml().toString() diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/DiskUtil.kt index 6d5d2ffb68..96ef383d48 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/DiskUtil.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/DiskUtil.kt @@ -11,12 +11,12 @@ import eu.kanade.tachiyomi.util.lang.Hash import java.io.File object DiskUtil { - /** * Returns the root folders of all the available external storages. */ - fun getExternalStorages(context: Context): List { - return ContextCompat.getExternalFilesDirs(context, null) + fun getExternalStorages(context: Context): List = + ContextCompat + .getExternalFilesDirs(context, null) .filterNotNull() .mapNotNull { val file = File(it.absolutePath.substringBefore("/Android/")) @@ -27,11 +27,8 @@ object DiskUtil { null } } - } - fun hashKeyForDisk(key: String): String { - return Hash.md5(key) - } + fun hashKeyForDisk(key: String): String = Hash.md5(key) fun getDirectorySize(f: File): Long { var size: Long = 0 @@ -48,43 +45,43 @@ object DiskUtil { /** * Gets the total space for the disk that a file path points to, in bytes. */ - fun getTotalStorageSpace(file: File): Long { - return try { + fun getTotalStorageSpace(file: File): Long = + try { val stat = StatFs(file.absolutePath) stat.blockCountLong * stat.blockSizeLong } catch (_: Exception) { -1L } - } /** * Gets the available space for the disk that a file path points to, in bytes. */ - fun getAvailableStorageSpace(file: File): Long { - return try { + fun getAvailableStorageSpace(file: File): Long = + try { val stat = StatFs(file.absolutePath) stat.availableBlocksLong * stat.blockSizeLong } catch (_: Exception) { -1L } - } /** * Gets the available space for the disk that a file path points to, in bytes. */ - fun getAvailableStorageSpace(f: UniFile): Long { - return try { + fun getAvailableStorageSpace(f: UniFile): Long = + try { val stat = StatFs(f.uri.path) stat.availableBlocksLong * stat.blockSizeLong } catch (_: Exception) { -1L } - } /** * Don't display downloaded chapters in gallery apps creating `.nomedia`. */ - fun createNoMediaFile(dir: UniFile?, context: Context?) { + fun createNoMediaFile( + dir: UniFile?, + context: Context?, + ) { if (dir != null && dir.exists()) { val nomedia = dir.findFile(NOMEDIA_FILE) if (nomedia == null) { @@ -97,7 +94,10 @@ object DiskUtil { /** * Scans the given file so that it can be shown in gallery apps, for example. */ - fun scanMedia(context: Context, uri: Uri) { + fun scanMedia( + context: Context, + uri: Uri, + ) { MediaScannerConnection.scanFile(context, arrayOf(uri.path), null, null) } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt index b194c5ee3a..3a8206f088 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt @@ -10,8 +10,9 @@ import java.io.InputStream /** * Wrapper over ZipFile to load files in epub format. */ -class EpubFile(private val reader: ArchiveReader) : Closeable by reader { - +class EpubFile( + private val reader: ArchiveReader, +) : Closeable by reader { /** * Path separator used by this epub. */ @@ -20,9 +21,7 @@ class EpubFile(private val reader: ArchiveReader) : Closeable by reader { /** * Returns an input stream for reading the contents of the specified zip file entry. */ - fun getInputStream(entryName: String): InputStream? { - return reader.getInputStream(entryName) - } + fun getInputStream(entryName: String): InputStream? = reader.getInputStream(entryName) /** * Returns the path of all the images found in the epub file. @@ -52,17 +51,17 @@ class EpubFile(private val reader: ArchiveReader) : Closeable by reader { /** * Returns the package document where all the files are listed. */ - fun getPackageDocument(ref: String): Document { - return getInputStream(ref)!!.use { Jsoup.parse(it, null, "") } - } + fun getPackageDocument(ref: String): Document = getInputStream(ref)!!.use { Jsoup.parse(it, null, "") } /** * Returns all the pages from the epub. */ private fun getPagesFromDocument(document: Document): List { - val pages = document.select("manifest > item") - .filter { node -> "application/xhtml+xml" == node.attr("media-type") } - .associateBy { it.attr("id") } + val pages = + document + .select("manifest > item") + .filter { node -> "application/xhtml+xml" == node.attr("media-type") } + .associateBy { it.attr("id") } val spine = document.select("spine > itemref").map { it.attr("idref") } return spine.mapNotNull { pages[it] }.map { it.attr("href") } @@ -71,7 +70,10 @@ class EpubFile(private val reader: ArchiveReader) : Closeable by reader { /** * Returns all the images contained in every page from the epub. */ - private fun getImagesFromPages(pages: List, packageHref: String): List { + private fun getImagesFromPages( + pages: List, + packageHref: String, + ): List { val result = mutableListOf() val basePath = getParentDirectory(packageHref) pages.forEach { page -> @@ -106,7 +108,10 @@ class EpubFile(private val reader: ArchiveReader) : Closeable by reader { /** * Resolves a zip path from base and relative components and a path separator. */ - private fun resolveZipPath(basePath: String, relativePath: String): String { + private fun resolveZipPath( + basePath: String, + relativePath: String, + ): String { if (relativePath.startsWith(pathSeparator)) { // Path is absolute, so return as-is. return relativePath diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt index c9f20326d0..33068b2f51 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt @@ -9,7 +9,6 @@ import logcat.LogPriority import tachiyomi.core.common.util.system.logcat object DeviceUtil { - val isMiui: Boolean by lazy { getSystemProperty("ro.miui.ui.version.name")?.isNotEmpty() ?: false } @@ -36,7 +35,8 @@ object DeviceUtil { } return try { - Class.forName("android.miui.AppOpsUtils") + Class + .forName("android.miui.AppOpsUtils") .getDeclaredMethod("isXOptMode") .invoke(null) as Boolean } catch (e: Exception) { @@ -62,11 +62,12 @@ object DeviceUtil { } } - val invalidDefaultBrowsers = listOf( - "android", - "com.huawei.android.internal.app", - "com.zui.resolver", - ) + val invalidDefaultBrowsers = + listOf( + "android", + "com.huawei.android.internal.app", + "com.zui.resolver", + ) /** * ActivityManager#isLowRamDevice is based on a system property, which isn't @@ -83,14 +84,14 @@ object DeviceUtil { } @SuppressLint("PrivateApi") - private fun getSystemProperty(key: String?): String? { - return try { - Class.forName("android.os.SystemProperties") + private fun getSystemProperty(key: String?): String? = + try { + Class + .forName("android.os.SystemProperties") .getDeclaredMethod("get", String::class.java) .invoke(null, key) as String } catch (e: Exception) { logcat(LogPriority.WARN, e) { "Unable to use SystemProperties.get()" } null } - } } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt index 453f9289e5..f350f41632 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt @@ -15,9 +15,7 @@ fun Context.toast( resource: StringResource, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}, -): Toast { - return toast(stringResource(resource), duration, block) -} +): Toast = toast(stringResource(resource), duration, block) /** * Display a toast in this context. @@ -29,9 +27,8 @@ fun Context.toast( text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}, -): Toast { - return Toast.makeText(applicationContext, text.orEmpty(), duration).also { +): Toast = + Toast.makeText(applicationContext, text.orEmpty(), duration).also { block(it) it.show() } -} diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt index 6547a46e01..90200c5ac3 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt @@ -8,14 +8,15 @@ import android.webkit.WebViewClient @Suppress("OverridingDeprecatedMember") abstract class WebViewClientCompat : WebViewClient() { + open fun shouldOverrideUrlCompat( + view: WebView, + url: String, + ): Boolean = false - open fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { - return false - } - - open fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? { - return null - } + open fun shouldInterceptRequestCompat( + view: WebView, + url: String, + ): WebResourceResponse? = null open fun onReceivedErrorCompat( view: WebView, @@ -29,24 +30,22 @@ abstract class WebViewClientCompat : WebViewClient() { final override fun shouldOverrideUrlLoading( view: WebView, request: WebResourceRequest, - ): Boolean { - return shouldOverrideUrlCompat(view, request.url.toString()) - } + ): Boolean = shouldOverrideUrlCompat(view, request.url.toString()) - final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { - return shouldOverrideUrlCompat(view, url) - } + final override fun shouldOverrideUrlLoading( + view: WebView, + url: String, + ): Boolean = shouldOverrideUrlCompat(view, url) final override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest, - ): WebResourceResponse? { - return shouldInterceptRequestCompat(view, request.url.toString()) - } + ): WebResourceResponse? = shouldInterceptRequestCompat(view, request.url.toString()) - final override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { - return shouldInterceptRequestCompat(view, url) - } + final override fun shouldInterceptRequest( + view: WebView, + url: String, + ): WebResourceResponse? = shouldInterceptRequestCompat(view, url) final override fun onReceivedError( view: WebView, diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt index b059e15d42..8ff2a0b899 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt @@ -26,12 +26,11 @@ object WebViewUtil { * Example of Chrome on Android: * Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.3 */ - fun getInferredUserAgent(context: Context): String { - return WebView(context) + fun getInferredUserAgent(context: Context): String = + WebView(context) .getDefaultUserAgentString() .replace("; Android .*?\\)".toRegex(), "; Android 10; K)") .replace("Version/.* Chrome/".toRegex(), "Chrome/") - } fun getVersion(context: Context): String { val webView = WebView.getCurrentWebViewPackage() ?: return "how did you get here?" @@ -55,13 +54,12 @@ object WebViewUtil { } } -fun WebView.isOutdated(): Boolean { - return getWebViewMajorVersion() < WebViewUtil.MINIMUM_WEBVIEW_VERSION -} +fun WebView.isOutdated(): Boolean = getWebViewMajorVersion() < WebViewUtil.MINIMUM_WEBVIEW_VERSION -suspend fun WebView.getHtml(): String = suspendCancellableCoroutine { - evaluateJavascript("document.documentElement.outerHTML") { html -> it.resume(html) } -} +suspend fun WebView.getHtml(): String = + suspendCancellableCoroutine { + evaluateJavascript("document.documentElement.outerHTML") { html -> it.resume(html) } + } @SuppressLint("SetJavaScriptEnabled") fun WebView.setDefaultSettings() { diff --git a/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveInputStream.kt b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveInputStream.kt index 1499867c83..0b0a7a29b4 100644 --- a/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveInputStream.kt +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveInputStream.kt @@ -7,7 +7,10 @@ import java.io.InputStream import java.nio.ByteBuffer import kotlin.concurrent.Volatile -class ArchiveInputStream(buffer: Long, size: Long) : InputStream() { +class ArchiveInputStream( + buffer: Long, + size: Long, +) : InputStream() { private val lock = Any() @Volatile @@ -34,7 +37,11 @@ class ArchiveInputStream(buffer: Long, size: Long) : InputStream() { return if (oneByteBuffer.hasRemaining()) oneByteBuffer.get().toUByte().toInt() else -1 } - override fun read(b: ByteArray, off: Int, len: Int): Int { + override fun read( + b: ByteArray, + off: Int, + len: Int, + ): Int { val buffer = ByteBuffer.wrap(b, off, len) read(buffer) return if (buffer.hasRemaining()) buffer.remaining() else -1 @@ -55,9 +62,10 @@ class ArchiveInputStream(buffer: Long, size: Long) : InputStream() { Archive.readFree(archive) } - fun getNextEntry() = Archive.readNextHeader(archive).takeUnless { it == 0L }?.let { entry -> - val name = ArchiveEntry.pathnameUtf8(entry) ?: ArchiveEntry.pathname(entry)?.decodeToString() ?: return null - val isFile = ArchiveEntry.filetype(entry) == ArchiveEntry.AE_IFREG - ArchiveEntry(name, isFile) - } + fun getNextEntry() = + Archive.readNextHeader(archive).takeUnless { it == 0L }?.let { entry -> + val name = ArchiveEntry.pathnameUtf8(entry) ?: ArchiveEntry.pathname(entry)?.decodeToString() ?: return null + val isFile = ArchiveEntry.filetype(entry) == ArchiveEntry.AE_IFREG + ArchiveEntry(name, isFile) + } } diff --git a/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveReader.kt b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveReader.kt index 28467d0fea..5bb49a0434 100644 --- a/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveReader.kt +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveReader.kt @@ -10,7 +10,9 @@ import tachiyomi.core.common.storage.openFileDescriptor import java.io.Closeable import java.io.InputStream -class ArchiveReader(pfd: ParcelFileDescriptor) : Closeable { +class ArchiveReader( + pfd: ParcelFileDescriptor, +) : Closeable { val size = pfd.statSize val address = Os.mmap(0, size, OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, pfd.fileDescriptor, 0) diff --git a/core/common/src/main/kotlin/mihon/core/common/archive/ZipWriter.kt b/core/common/src/main/kotlin/mihon/core/common/archive/ZipWriter.kt index b5d2015168..abcbf10c9f 100644 --- a/core/common/src/main/kotlin/mihon/core/common/archive/ZipWriter.kt +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ZipWriter.kt @@ -11,7 +11,10 @@ import tachiyomi.core.common.storage.openFileDescriptor import java.io.Closeable import java.nio.ByteBuffer -class ZipWriter(val context: Context, file: UniFile) : Closeable { +class ZipWriter( + val context: Context, + file: UniFile, +) : Closeable { private val pfd = file.openFileDescriptor(context, "wt") private val archive = Archive.writeNew() private val entry = ArchiveEntry.new2(archive) @@ -55,20 +58,21 @@ class ZipWriter(val context: Context, file: UniFile) : Closeable { } } -private fun StructStat.toArchiveStat() = ArchiveEntry.StructStat().apply { - stDev = st_dev - stMode = st_mode - stNlink = st_nlink.toInt() - stUid = st_uid - stGid = st_gid - stRdev = st_rdev - stSize = st_size - stBlksize = st_blksize - stBlocks = st_blocks - stAtim = timespec(st_atime) - stMtim = timespec(st_mtime) - stCtim = timespec(st_ctime) - stIno = st_ino -} +private fun StructStat.toArchiveStat() = + ArchiveEntry.StructStat().apply { + stDev = st_dev + stMode = st_mode + stNlink = st_nlink.toInt() + stUid = st_uid + stGid = st_gid + stRdev = st_rdev + stSize = st_size + stBlksize = st_blksize + stBlocks = st_blocks + stAtim = timespec(st_atime) + stMtim = timespec(st_mtime) + stCtim = timespec(st_ctime) + stIno = st_ino + } private fun timespec(tvSec: Long) = ArchiveEntry.StructTimespec().also { it.tvSec = tvSec } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/i18n/Localize.kt b/core/common/src/main/kotlin/tachiyomi/core/common/i18n/Localize.kt index c698e856e5..a0c971419a 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/i18n/Localize.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/i18n/Localize.kt @@ -9,22 +9,23 @@ import dev.icerock.moko.resources.desc.Resource import dev.icerock.moko.resources.desc.ResourceFormatted import dev.icerock.moko.resources.desc.StringDesc -fun Context.stringResource(resource: StringResource): String { - return StringDesc.Resource(resource).toString(this).fixed() -} +fun Context.stringResource(resource: StringResource): String = StringDesc.Resource(resource).toString(this).fixed() -fun Context.stringResource(resource: StringResource, vararg args: Any): String { - return StringDesc.ResourceFormatted(resource, *args).toString(this).fixed() -} +fun Context.stringResource( + resource: StringResource, + vararg args: Any, +): String = StringDesc.ResourceFormatted(resource, *args).toString(this).fixed() -fun Context.pluralStringResource(resource: PluralsResource, count: Int): String { - return StringDesc.Plural(resource, count).toString(this).fixed() -} +fun Context.pluralStringResource( + resource: PluralsResource, + count: Int, +): String = StringDesc.Plural(resource, count).toString(this).fixed() -fun Context.pluralStringResource(resource: PluralsResource, count: Int, vararg args: Any): String { - return StringDesc.PluralFormatted(resource, count, *args).toString(this).fixed() -} +fun Context.pluralStringResource( + resource: PluralsResource, + count: Int, + vararg args: Any, +): String = StringDesc.PluralFormatted(resource, count, *args).toString(this).fixed() // TODO: janky workaround for https://github.com/icerockdev/moko-resources/issues/337 -private fun String.fixed() = - this.replace("""\""", """"""") +private fun String.fixed() = this.replace("""\""", """"""") diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreference.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreference.kt index 577d83687a..9efca189c8 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreference.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreference.kt @@ -20,32 +20,33 @@ sealed class AndroidPreference( private val key: String, private val defaultValue: T, ) : Preference { + abstract fun read( + preferences: SharedPreferences, + key: String, + defaultValue: T, + ): T - abstract fun read(preferences: SharedPreferences, key: String, defaultValue: T): T - - abstract fun write(key: String, value: T): Editor.() -> Unit + abstract fun write( + key: String, + value: T, + ): Editor.() -> Unit - override fun key(): String { - return key - } + override fun key(): String = key - override fun get(): T { - return try { + override fun get(): T = + try { read(preferences, key, defaultValue) } catch (e: ClassCastException) { logcat { "Invalid value for $key; deleting" } delete() defaultValue } - } override fun set(value: T) { preferences.edit(action = write(key, value)) } - override fun isSet(): Boolean { - return preferences.contains(key) - } + override fun isSet(): Boolean = preferences.contains(key) override fun delete() { preferences.edit { @@ -53,21 +54,16 @@ sealed class AndroidPreference( } } - override fun defaultValue(): T { - return defaultValue - } + override fun defaultValue(): T = defaultValue - override fun changes(): Flow { - return keyFlow + override fun changes(): Flow = + keyFlow .filter { it == key || it == null } .onStart { emit("ignition") } .map { get() } .conflate() - } - override fun stateIn(scope: CoroutineScope): StateFlow { - return changes().stateIn(scope, SharingStarted.Eagerly, get()) - } + override fun stateIn(scope: CoroutineScope): StateFlow = changes().stateIn(scope, SharingStarted.Eagerly, get()) class StringPrimitive( preferences: SharedPreferences, @@ -79,13 +75,15 @@ sealed class AndroidPreference( preferences: SharedPreferences, key: String, defaultValue: String, - ): String { - return preferences.getString(key, defaultValue) ?: defaultValue - } + ): String = preferences.getString(key, defaultValue) ?: defaultValue - override fun write(key: String, value: String): Editor.() -> Unit = { - putString(key, value) - } + override fun write( + key: String, + value: String, + ): Editor.() -> Unit = + { + putString(key, value) + } } class LongPrimitive( @@ -94,13 +92,19 @@ sealed class AndroidPreference( key: String, defaultValue: Long, ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { - override fun read(preferences: SharedPreferences, key: String, defaultValue: Long): Long { - return preferences.getLong(key, defaultValue) - } + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: Long, + ): Long = preferences.getLong(key, defaultValue) - override fun write(key: String, value: Long): Editor.() -> Unit = { - putLong(key, value) - } + override fun write( + key: String, + value: Long, + ): Editor.() -> Unit = + { + putLong(key, value) + } } class IntPrimitive( @@ -109,13 +113,19 @@ sealed class AndroidPreference( key: String, defaultValue: Int, ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { - override fun read(preferences: SharedPreferences, key: String, defaultValue: Int): Int { - return preferences.getInt(key, defaultValue) - } + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: Int, + ): Int = preferences.getInt(key, defaultValue) - override fun write(key: String, value: Int): Editor.() -> Unit = { - putInt(key, value) - } + override fun write( + key: String, + value: Int, + ): Editor.() -> Unit = + { + putInt(key, value) + } } class FloatPrimitive( @@ -124,13 +134,19 @@ sealed class AndroidPreference( key: String, defaultValue: Float, ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { - override fun read(preferences: SharedPreferences, key: String, defaultValue: Float): Float { - return preferences.getFloat(key, defaultValue) - } + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: Float, + ): Float = preferences.getFloat(key, defaultValue) - override fun write(key: String, value: Float): Editor.() -> Unit = { - putFloat(key, value) - } + override fun write( + key: String, + value: Float, + ): Editor.() -> Unit = + { + putFloat(key, value) + } } class BooleanPrimitive( @@ -143,13 +159,15 @@ sealed class AndroidPreference( preferences: SharedPreferences, key: String, defaultValue: Boolean, - ): Boolean { - return preferences.getBoolean(key, defaultValue) - } + ): Boolean = preferences.getBoolean(key, defaultValue) - override fun write(key: String, value: Boolean): Editor.() -> Unit = { - putBoolean(key, value) - } + override fun write( + key: String, + value: Boolean, + ): Editor.() -> Unit = + { + putBoolean(key, value) + } } class StringSetPrimitive( @@ -162,13 +180,15 @@ sealed class AndroidPreference( preferences: SharedPreferences, key: String, defaultValue: Set, - ): Set { - return preferences.getStringSet(key, defaultValue) ?: defaultValue - } + ): Set = preferences.getStringSet(key, defaultValue) ?: defaultValue - override fun write(key: String, value: Set): Editor.() -> Unit = { - putStringSet(key, value) - } + override fun write( + key: String, + value: Set, + ): Editor.() -> Unit = + { + putStringSet(key, value) + } } class Object( @@ -179,16 +199,23 @@ sealed class AndroidPreference( val serializer: (T) -> String, val deserializer: (String) -> T, ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { - override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T { - return try { + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: T, + ): T = + try { preferences.getString(key, null)?.let(deserializer) ?: defaultValue } catch (e: Exception) { defaultValue } - } - override fun write(key: String, value: T): Editor.() -> Unit = { - putString(key, serializer(value)) - } + override fun write( + key: String, + value: T, + ): Editor.() -> Unit = + { + putString(key, serializer(value)) + } } } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreferenceStore.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreferenceStore.kt index 6bdb120cdc..f9872a00fe 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreferenceStore.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreferenceStore.kt @@ -17,40 +17,45 @@ class AndroidPreferenceStore( context: Context, private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context), ) : PreferenceStore { - private val keyFlow = sharedPreferences.keyFlow - override fun getString(key: String, defaultValue: String): Preference { - return StringPrimitive(sharedPreferences, keyFlow, key, defaultValue) - } + override fun getString( + key: String, + defaultValue: String, + ): Preference = StringPrimitive(sharedPreferences, keyFlow, key, defaultValue) - override fun getLong(key: String, defaultValue: Long): Preference { - return LongPrimitive(sharedPreferences, keyFlow, key, defaultValue) - } + override fun getLong( + key: String, + defaultValue: Long, + ): Preference = LongPrimitive(sharedPreferences, keyFlow, key, defaultValue) - override fun getInt(key: String, defaultValue: Int): Preference { - return IntPrimitive(sharedPreferences, keyFlow, key, defaultValue) - } + override fun getInt( + key: String, + defaultValue: Int, + ): Preference = IntPrimitive(sharedPreferences, keyFlow, key, defaultValue) - override fun getFloat(key: String, defaultValue: Float): Preference { - return FloatPrimitive(sharedPreferences, keyFlow, key, defaultValue) - } + override fun getFloat( + key: String, + defaultValue: Float, + ): Preference = FloatPrimitive(sharedPreferences, keyFlow, key, defaultValue) - override fun getBoolean(key: String, defaultValue: Boolean): Preference { - return BooleanPrimitive(sharedPreferences, keyFlow, key, defaultValue) - } + override fun getBoolean( + key: String, + defaultValue: Boolean, + ): Preference = BooleanPrimitive(sharedPreferences, keyFlow, key, defaultValue) - override fun getStringSet(key: String, defaultValue: Set): Preference> { - return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue) - } + override fun getStringSet( + key: String, + defaultValue: Set, + ): Preference> = StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue) override fun getObject( key: String, defaultValue: T, serializer: (T) -> String, deserializer: (String) -> T, - ): Preference { - return Object( + ): Preference = + Object( preferences = sharedPreferences, keyFlow = keyFlow, key = key, @@ -58,22 +63,21 @@ class AndroidPreferenceStore( serializer = serializer, deserializer = deserializer, ) - } - override fun getAll(): Map { - return sharedPreferences.all ?: emptyMap() - } + override fun getAll(): Map = sharedPreferences.all ?: emptyMap() } private val SharedPreferences.keyFlow - get() = callbackFlow { - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> - trySend( - key, - ) - } - registerOnSharedPreferenceChangeListener(listener) - awaitClose { - unregisterOnSharedPreferenceChangeListener(listener) + get() = + callbackFlow { + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> + trySend( + key, + ) + } + registerOnSharedPreferenceChangeListener(listener) + awaitClose { + unregisterOnSharedPreferenceChangeListener(listener) + } } - } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/CheckboxState.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/CheckboxState.kt index cf7e471c03..92fdc667ad 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/CheckboxState.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/CheckboxState.kt @@ -1,47 +1,63 @@ package tachiyomi.core.common.preference -sealed class CheckboxState(open val value: T) { - +sealed class CheckboxState( + open val value: T, +) { abstract fun next(): CheckboxState - sealed class State(override val value: T) : CheckboxState(value) { - data class Checked(override val value: T) : State(value) - data class None(override val value: T) : State(value) + sealed class State( + override val value: T, + ) : CheckboxState(value) { + data class Checked( + override val value: T, + ) : State(value) + + data class None( + override val value: T, + ) : State(value) val isChecked: Boolean get() = this is Checked - override fun next(): CheckboxState { - return when (this) { + override fun next(): CheckboxState = + when (this) { is Checked -> None(value) is None -> Checked(value) } - } } - sealed class TriState(override val value: T) : CheckboxState(value) { - data class Include(override val value: T) : TriState(value) - data class Exclude(override val value: T) : TriState(value) - data class None(override val value: T) : TriState(value) + sealed class TriState( + override val value: T, + ) : CheckboxState(value) { + data class Include( + override val value: T, + ) : TriState(value) + + data class Exclude( + override val value: T, + ) : TriState(value) + + data class None( + override val value: T, + ) : TriState(value) - override fun next(): CheckboxState { - return when (this) { + override fun next(): CheckboxState = + when (this) { is Exclude -> None(value) is Include -> Exclude(value) is None -> Include(value) } - } } } -inline fun T.asCheckboxState(condition: (T) -> Boolean): CheckboxState.State { - return if (condition(this)) { +inline fun T.asCheckboxState(condition: (T) -> Boolean): CheckboxState.State = + if (condition(this)) { CheckboxState.State.Checked(this) } else { CheckboxState.State.None(this) } -} -inline fun List.mapAsCheckboxState(condition: (T) -> Boolean): List> { - return this.map { it.asCheckboxState(condition) } -} +inline fun List.mapAsCheckboxState(condition: (T) -> Boolean): List> = + this.map { + it.asCheckboxState(condition) + } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/InMemoryPreferenceStore.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/InMemoryPreferenceStore.kt index 96e8644ad3..b990ea7b04 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/InMemoryPreferenceStore.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/InMemoryPreferenceStore.kt @@ -13,41 +13,58 @@ import kotlinx.coroutines.flow.stateIn class InMemoryPreferenceStore( initialPreferences: Sequence> = sequenceOf(), ) : PreferenceStore { - private val preferences: Map> = initialPreferences.toList().associateBy { it.key() } - override fun getString(key: String, defaultValue: String): Preference { + override fun getString( + key: String, + defaultValue: String, + ): Preference { val default = InMemoryPreference(key, null, defaultValue) val data: String? = preferences[key]?.get() as? String return if (data == null) default else InMemoryPreference(key, data, defaultValue) } - override fun getLong(key: String, defaultValue: Long): Preference { + override fun getLong( + key: String, + defaultValue: Long, + ): Preference { val default = InMemoryPreference(key, null, defaultValue) val data: Long? = preferences[key]?.get() as? Long return if (data == null) default else InMemoryPreference(key, data, defaultValue) } - override fun getInt(key: String, defaultValue: Int): Preference { + override fun getInt( + key: String, + defaultValue: Int, + ): Preference { val default = InMemoryPreference(key, null, defaultValue) val data: Int? = preferences[key]?.get() as? Int return if (data == null) default else InMemoryPreference(key, data, defaultValue) } - override fun getFloat(key: String, defaultValue: Float): Preference { + override fun getFloat( + key: String, + defaultValue: Float, + ): Preference { val default = InMemoryPreference(key, null, defaultValue) val data: Float? = preferences[key]?.get() as? Float return if (data == null) default else InMemoryPreference(key, data, defaultValue) } - override fun getBoolean(key: String, defaultValue: Boolean): Preference { + override fun getBoolean( + key: String, + defaultValue: Boolean, + ): Preference { val default = InMemoryPreference(key, null, defaultValue) val data: Boolean? = preferences[key]?.get() as? Boolean return if (data == null) default else InMemoryPreference(key, data, defaultValue) } - override fun getStringSet(key: String, defaultValue: Set): Preference> { + override fun getStringSet( + key: String, + defaultValue: Set, + ): Preference> { TODO("Not yet implemented") } @@ -63,9 +80,7 @@ class InMemoryPreferenceStore( return if (data == null) default else InMemoryPreference(key, data, defaultValue) } - override fun getAll(): Map { - return preferences - } + override fun getAll(): Map = preferences class InMemoryPreference( private val key: String, @@ -86,9 +101,8 @@ class InMemoryPreferenceStore( override fun changes(): Flow = flow { data } - override fun stateIn(scope: CoroutineScope): StateFlow { - return changes().stateIn(scope, SharingStarted.Eagerly, get()) - } + override fun stateIn(scope: CoroutineScope): StateFlow = + changes().stateIn(scope, SharingStarted.Eagerly, get()) override fun set(value: T) { data = value diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/Preference.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/Preference.kt index f75384491a..9c00289a64 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/Preference.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/Preference.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface Preference { - fun key(): String fun get(): T @@ -26,32 +25,27 @@ interface Preference { /** * A preference that should not be exposed in places like backups without user consent. */ - fun isPrivate(key: String): Boolean { - return key.startsWith(PRIVATE_PREFIX) - } - fun privateKey(key: String): String { - return "$PRIVATE_PREFIX$key" - } + fun isPrivate(key: String): Boolean = key.startsWith(PRIVATE_PREFIX) + + fun privateKey(key: String): String = "$PRIVATE_PREFIX$key" /** * A preference used for internal app state that isn't really a user preference * and therefore should not be in places like backups. */ - fun isAppState(key: String): Boolean { - return key.startsWith(APP_STATE_PREFIX) - } - fun appStateKey(key: String): String { - return "$APP_STATE_PREFIX$key" - } + fun isAppState(key: String): Boolean = key.startsWith(APP_STATE_PREFIX) + + fun appStateKey(key: String): String = "$APP_STATE_PREFIX$key" private const val APP_STATE_PREFIX = "__APP_STATE_" private const val PRIVATE_PREFIX = "__PRIVATE_" } } -inline fun Preference.getAndSet(crossinline block: (T) -> R) = set( - block(get()), -) +inline fun Preference.getAndSet(crossinline block: (T) -> R) = + set( + block(get()), + ) operator fun Preference>.plusAssign(item: T) { set(get() + item) diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt index 0cd3a21bf2..53332cfba1 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt @@ -1,18 +1,35 @@ package tachiyomi.core.common.preference interface PreferenceStore { + fun getString( + key: String, + defaultValue: String = "", + ): Preference - fun getString(key: String, defaultValue: String = ""): Preference - - fun getLong(key: String, defaultValue: Long = 0): Preference + fun getLong( + key: String, + defaultValue: Long = 0, + ): Preference - fun getInt(key: String, defaultValue: Int = 0): Preference + fun getInt( + key: String, + defaultValue: Int = 0, + ): Preference - fun getFloat(key: String, defaultValue: Float = 0f): Preference + fun getFloat( + key: String, + defaultValue: Float = 0f, + ): Preference - fun getBoolean(key: String, defaultValue: Boolean = false): Preference + fun getBoolean( + key: String, + defaultValue: Boolean = false, + ): Preference - fun getStringSet(key: String, defaultValue: Set = emptySet()): Preference> + fun getStringSet( + key: String, + defaultValue: Set = emptySet(), + ): Preference> fun getObject( key: String, @@ -27,8 +44,8 @@ interface PreferenceStore { inline fun > PreferenceStore.getEnum( key: String, defaultValue: T, -): Preference { - return getObject( +): Preference = + getObject( key = key, defaultValue = defaultValue, serializer = { it.name }, @@ -40,4 +57,3 @@ inline fun > PreferenceStore.getEnum( } }, ) -} diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/TriState.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/TriState.kt index 703f069c3f..5e8cd8ec77 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/TriState.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/TriState.kt @@ -6,11 +6,10 @@ enum class TriState { ENABLED_NOT, // Enabled with "not" filter ; - fun next(): TriState { - return when (this) { + fun next(): TriState = + when (this) { DISABLED -> ENABLED_IS ENABLED_IS -> ENABLED_NOT ENABLED_NOT -> DISABLED } - } } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/storage/AndroidStorageFolderProvider.kt b/core/common/src/main/kotlin/tachiyomi/core/common/storage/AndroidStorageFolderProvider.kt index 33335f3458..eb5b9b861e 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/storage/AndroidStorageFolderProvider.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/storage/AndroidStorageFolderProvider.kt @@ -10,15 +10,11 @@ import java.io.File class AndroidStorageFolderProvider( private val context: Context, ) : FolderProvider { - - override fun directory(): File { - return File( + override fun directory(): File = + File( Environment.getExternalStorageDirectory().absolutePath + File.separator + context.stringResource(MR.strings.app_name), ) - } - override fun path(): String { - return directory().toUri().toString() - } + override fun path(): String = directory().toUri().toString() } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/storage/FolderProvider.kt b/core/common/src/main/kotlin/tachiyomi/core/common/storage/FolderProvider.kt index 06d3e364fb..1563c3d9a4 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/storage/FolderProvider.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/storage/FolderProvider.kt @@ -3,7 +3,6 @@ package tachiyomi.core.common.storage import java.io.File interface FolderProvider { - fun directory(): File fun path(): String diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt b/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt index 4b04ff4056..2ca5edc788 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt @@ -13,5 +13,8 @@ val UniFile.nameWithoutExtension: String? val UniFile.displayablePath: String get() = filePath ?: uri.toString() -fun UniFile.openFileDescriptor(context: Context, mode: String): ParcelFileDescriptor = +fun UniFile.openFileDescriptor( + context: Context, + mode: String, +): ParcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, mode) ?: error("Failed to open file descriptor: $displayablePath") diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/CoroutinesExtensions.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/CoroutinesExtensions.kt index a76bff2cc4..c0f38e7e13 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/CoroutinesExtensions.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/CoroutinesExtensions.kt @@ -43,24 +43,23 @@ fun launchIO(block: suspend CoroutineScope.() -> Unit): Job = fun launchNow(block: suspend CoroutineScope.() -> Unit): Job = GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block) -fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job = - launch(Dispatchers.Main, block = block) +fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job = launch(Dispatchers.Main, block = block) -fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job = - launch(Dispatchers.IO, block = block) +fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job = launch(Dispatchers.IO, block = block) fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job = launchIO { withContext(NonCancellable, block) } -suspend fun withUIContext(block: suspend CoroutineScope.() -> T) = withContext( - Dispatchers.Main, - block, -) +suspend fun withUIContext(block: suspend CoroutineScope.() -> T) = + withContext( + Dispatchers.Main, + block, + ) -suspend fun withIOContext(block: suspend CoroutineScope.() -> T) = withContext( - Dispatchers.IO, - block, -) +suspend fun withIOContext(block: suspend CoroutineScope.() -> T) = + withContext( + Dispatchers.IO, + block, + ) -suspend fun withNonCancellableContext(block: suspend CoroutineScope.() -> T) = - withContext(NonCancellable, block) +suspend fun withNonCancellableContext(block: suspend CoroutineScope.() -> T) = withContext(NonCancellable, block) diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/RxCoroutineBridge.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/RxCoroutineBridge.kt index d6f521cc01..0c00f0e6be 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/RxCoroutineBridge.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/RxCoroutineBridge.kt @@ -16,43 +16,44 @@ import kotlin.coroutines.resumeWithException suspend fun Observable.awaitSingle(): T = single().awaitOne() @OptIn(InternalCoroutinesApi::class) -private suspend fun Observable.awaitOne(): T = suspendCancellableCoroutine { cont -> - cont.unsubscribeOnCancellation( - subscribe( - object : Subscriber() { - override fun onStart() { - request(1) - } - - override fun onNext(t: T) { - cont.resume(t) - } - - override fun onCompleted() { - if (cont.isActive) { - cont.resumeWithException( - IllegalStateException( - "Should have invoked onNext", - ), - ) +private suspend fun Observable.awaitOne(): T = + suspendCancellableCoroutine { cont -> + cont.unsubscribeOnCancellation( + subscribe( + object : Subscriber() { + override fun onStart() { + request(1) } - } - override fun onError(e: Throwable) { + override fun onNext(t: T) { + cont.resume(t) + } + + override fun onCompleted() { + if (cont.isActive) { + cont.resumeWithException( + IllegalStateException( + "Should have invoked onNext", + ), + ) + } + } + + override fun onError(e: Throwable) { /* * Rx1 observable throws NoSuchElementException if cancellation happened before * element emission. To mitigate this we try to atomically resume continuation with exception: * if resume failed, then we know that continuation successfully cancelled itself */ - val token = cont.tryResumeWithException(e) - if (token != null) { - cont.completeResume(token) + val token = cont.tryResumeWithException(e) + if (token != null) { + cont.completeResume(token) + } } - } - }, - ), - ) -} + }, + ), + ) + } private fun CancellableContinuation.unsubscribeOnCancellation(sub: Subscription) = invokeOnCancellation { sub.unsubscribe() } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/SortUtil.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/SortUtil.kt index 9efe5f79f9..a35e9119b5 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/SortUtil.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/SortUtil.kt @@ -10,6 +10,4 @@ private val collator by lazy { } } -fun String.compareToWithCollator(other: String): Int { - return collator.compare(this, other) -} +fun String.compareToWithCollator(other: String): Int = collator.compare(this, other) diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt index f5e9a80983..4e576cbb43 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt @@ -36,24 +36,25 @@ import kotlin.math.max import kotlin.math.min object ImageUtil { - - fun isImage(name: String?, openStream: (() -> InputStream)? = null): Boolean { + fun isImage( + name: String?, + openStream: (() -> InputStream)? = null, + ): Boolean { if (name == null) return false - val contentType = try { - URLConnection.guessContentTypeFromName(name) - } catch (e: Exception) { - null - } ?: openStream?.let { findImageType(it)?.mime } + val contentType = + try { + URLConnection.guessContentTypeFromName(name) + } catch (e: Exception) { + null + } ?: openStream?.let { findImageType(it)?.mime } return contentType?.startsWith("image/") ?: false } - fun findImageType(openStream: () -> InputStream): ImageType? { - return openStream().use { findImageType(it) } - } + fun findImageType(openStream: () -> InputStream): ImageType? = openStream().use { findImageType(it) } - fun findImageType(stream: InputStream): ImageType? { - return try { + fun findImageType(stream: InputStream): ImageType? = + try { when (getImageType(stream)?.format) { Format.Avif -> ImageType.AVIF Format.Gif -> ImageType.GIF @@ -67,13 +68,11 @@ object ImageUtil { } catch (e: Exception) { null } - } - fun getExtensionFromMimeType(mime: String?): String { - return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) + fun getExtensionFromMimeType(mime: String?): String = + MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime] ?: "jpg" - } fun isAnimatedAndSupported(source: BufferedSource): Boolean { return try { @@ -95,12 +94,13 @@ object ImageUtil { private fun getImageType(stream: InputStream): tachiyomi.decoder.ImageType? { val bytes = ByteArray(32) - val length = if (stream.markSupported()) { - stream.mark(bytes.size) - stream.read(bytes, 0, bytes.size).also { stream.reset() } - } else { - stream.read(bytes, 0, bytes.size) - } + val length = + if (stream.markSupported()) { + stream.mark(bytes.size) + stream.read(bytes, 0, bytes.size).also { stream.reset() } + } else { + stream.read(bytes, 0, bytes.size) + } if (length == -1) { return null @@ -109,7 +109,10 @@ object ImageUtil { return ImageDecoder.findType(bytes) } - enum class ImageType(val mime: String, val extension: String) { + enum class ImageType( + val mime: String, + val extension: String, + ) { AVIF("image/avif", "avif"), GIF("image/gif", "gif"), HEIF("image/heif", "heif"), @@ -132,7 +135,10 @@ object ImageUtil { /** * Extract the 'side' part from [BufferedSource] and return it as [BufferedSource]. */ - fun splitInHalf(imageSource: BufferedSource, side: Side): BufferedSource { + fun splitInHalf( + imageSource: BufferedSource, + side: Side, + ): BufferedSource { val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream()) val height = imageBitmap.height val width = imageBitmap.width @@ -140,10 +146,11 @@ object ImageUtil { val singlePage = Rect(0, 0, width / 2, height) val half = createBitmap(width / 2, height) - val part = when (side) { - Side.RIGHT -> Rect(width - width / 2, 0, width, height) - Side.LEFT -> Rect(0, 0, width / 2, height) - } + val part = + when (side) { + Side.RIGHT -> Rect(width - width / 2, 0, width, height) + Side.LEFT -> Rect(0, 0, width / 2, height) + } half.applyCanvas { drawBitmap(imageBitmap, part, singlePage, null) } @@ -153,7 +160,10 @@ object ImageUtil { return output } - fun rotateImage(imageSource: BufferedSource, degrees: Float): BufferedSource { + fun rotateImage( + imageSource: BufferedSource, + degrees: Float, + ): BufferedSource { val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream()) val rotated = rotateBitMap(imageBitmap, degrees) @@ -163,7 +173,10 @@ object ImageUtil { return output } - private fun rotateBitMap(bitmap: Bitmap, degrees: Float): Bitmap { + private fun rotateBitMap( + bitmap: Bitmap, + degrees: Float, + ): Bitmap { val matrix = Matrix().apply { postRotate(degrees) } return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) } @@ -171,7 +184,10 @@ object ImageUtil { /** * Split the image into left and right parts, then merge them into a new image. */ - fun splitAndMerge(imageSource: BufferedSource, upperSide: Side): BufferedSource { + fun splitAndMerge( + imageSource: BufferedSource, + upperSide: Side, + ): BufferedSource { val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream()) val height = imageBitmap.height val width = imageBitmap.width @@ -179,17 +195,19 @@ object ImageUtil { val result = createBitmap(width / 2, height * 2) result.applyCanvas { // right -> upper - val rightPart = when (upperSide) { - Side.RIGHT -> Rect(width - width / 2, 0, width, height) - Side.LEFT -> Rect(0, 0, width / 2, height) - } + val rightPart = + when (upperSide) { + Side.RIGHT -> Rect(width - width / 2, 0, width, height) + Side.LEFT -> Rect(0, 0, width / 2, height) + } val upperPart = Rect(0, 0, width / 2, height) drawBitmap(imageBitmap, rightPart, upperPart, null) // left -> bottom - val leftPart = when (upperSide) { - Side.LEFT -> Rect(width - width / 2, 0, width, height) - Side.RIGHT -> Rect(0, 0, width / 2, height) - } + val leftPart = + when (upperSide) { + Side.LEFT -> Rect(width - width / 2, 0, width, height) + Side.RIGHT -> Rect(0, 0, width / 2, height) + } val bottomPart = Rect(0, height, width / 2, height * 2) drawBitmap(imageBitmap, leftPart, bottomPart, null) } @@ -217,7 +235,11 @@ object ImageUtil { /** * Splits tall images to improve performance of reader */ - fun splitTallImage(tmpDir: UniFile, imageFile: UniFile, filenamePrefix: String): Boolean { + fun splitTallImage( + tmpDir: UniFile, + imageFile: UniFile, + filenamePrefix: String, + ): Boolean { val imageSource = imageFile.openInputStream().use { Buffer().readFrom(it) } if (isAnimatedAndSupported(imageSource) || !isTallImage(imageSource)) { return true @@ -229,9 +251,10 @@ object ImageUtil { return false } - val options = extractImageOptions(imageSource).apply { - inJustDecodeBounds = false - } + val options = + extractImageOptions(imageSource).apply { + inJustDecodeBounds = false + } val splitDataList = options.splitData @@ -269,7 +292,10 @@ object ImageUtil { } } - private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format( + private fun splitImageName( + filenamePrefix: String, + index: Int, + ) = "${filenamePrefix}__${"%03d".format( Locale.ENGLISH, index + 1, )}.jpg" @@ -319,7 +345,10 @@ object ImageUtil { /** * Algorithm for determining what background to accompany a comic/manga page */ - fun chooseBackground(context: Context, imageStream: InputStream): Drawable { + fun chooseBackground( + context: Context, + imageStream: InputStream, + ): Drawable { val decoder = ImageDecoder.newInstance(imageStream) val image = decoder?.decode() decoder?.recycle() @@ -363,29 +392,33 @@ object ImageUtil { val topAndBotPixels = listOf(topLeftPixel, topCenterPixel, topRightPixel, botRightPixel, bottomCenterPixel, botLeftPixel) - val isNotWhiteAndCloseTo = topAndBotPixels.mapIndexed { index, color -> - val other = topAndBotPixels[(index + 1) % topAndBotPixels.size] - !color.isWhite() && color.isCloseTo(other) - } + val isNotWhiteAndCloseTo = + topAndBotPixels.mapIndexed { index, color -> + val other = topAndBotPixels[(index + 1) % topAndBotPixels.size] + !color.isWhite() && color.isCloseTo(other) + } if (isNotWhiteAndCloseTo.all { it }) { return ColorDrawable(topLeftPixel) } val cornerPixels = listOf(topLeftPixel, topRightPixel, botLeftPixel, botRightPixel) - val numberOfWhiteCorners = cornerPixels.map { cornerPixel -> cornerPixel.isWhite() } - .filter { it } - .size + val numberOfWhiteCorners = + cornerPixels + .map { cornerPixel -> cornerPixel.isWhite() } + .filter { it } + .size if (numberOfWhiteCorners > 2) { darkBG = false } - var blackColor = when { - topLeftIsDark -> topLeftPixel - topRightIsDark -> topRightPixel - botLeftIsDark -> botLeftPixel - botRightIsDark -> botRightPixel - else -> whiteColor - } + var blackColor = + when { + topLeftIsDark -> topLeftPixel + topRightIsDark -> topRightPixel + botLeftIsDark -> botLeftPixel + botRightIsDark -> botRightPixel + else -> whiteColor + } var overallWhitePixels = 0 var overallBlackPixels = 0 @@ -443,11 +476,12 @@ object ImageUtil { when { blackPixels > 22 -> { if (x == right || x == rightOffsetX) { - blackColor = when { - topRightIsDark -> topRightPixel - botRightIsDark -> botRightPixel - else -> blackColor - } + blackColor = + when { + topRightIsDark -> topRightPixel + botRightIsDark -> botRightPixel + else -> blackColor + } } darkBG = true overallWhitePixels = 0 @@ -456,11 +490,12 @@ object ImageUtil { blackStreak -> { darkBG = true if (x == right || x == rightOffsetX) { - blackColor = when { - topRightIsDark -> topRightPixel - botRightIsDark -> botRightPixel - else -> blackColor - } + blackColor = + when { + topRightIsDark -> topRightPixel + botRightIsDark -> botRightPixel + else -> blackColor + } } if (blackPixels > 18) { overallWhitePixels = 0 @@ -497,32 +532,37 @@ object ImageUtil { val topOffsetCornersIsDark = image[leftOffsetX, top].isDark() && image[rightOffsetX, top].isDark() val botOffsetCornersIsDark = image[leftOffsetX, bot].isDark() && image[rightOffsetX, bot].isDark() - val gradient = when { - darkBG && botCornersIsWhite -> { - intArrayOf(blackColor, blackColor, whiteColor, whiteColor) - } - darkBG && topCornersIsWhite -> { - intArrayOf(whiteColor, whiteColor, blackColor, blackColor) - } - darkBG -> { - return ColorDrawable(blackColor) - } - topIsBlackStreak || ( - topCornersIsDark && topOffsetCornersIsDark && - (topMidIsDark || overallBlackPixels > 9) - ) -> { - intArrayOf(blackColor, blackColor, whiteColor, whiteColor) - } - bottomIsBlackStreak || ( - botCornersIsDark && botOffsetCornersIsDark && - (bottomCenterPixel.isDark() || overallBlackPixels > 9) - ) -> { - intArrayOf(whiteColor, whiteColor, blackColor, blackColor) - } - else -> { - return ColorDrawable(whiteColor) + val gradient = + when { + darkBG && botCornersIsWhite -> { + intArrayOf(blackColor, blackColor, whiteColor, whiteColor) + } + darkBG && topCornersIsWhite -> { + intArrayOf(whiteColor, whiteColor, blackColor, blackColor) + } + darkBG -> { + return ColorDrawable(blackColor) + } + topIsBlackStreak || + ( + topCornersIsDark && + topOffsetCornersIsDark && + (topMidIsDark || overallBlackPixels > 9) + ) -> { + intArrayOf(blackColor, blackColor, whiteColor, whiteColor) + } + bottomIsBlackStreak || + ( + botCornersIsDark && + botOffsetCornersIsDark && + (bottomCenterPixel.isDark() || overallBlackPixels > 9) + ) -> { + intArrayOf(whiteColor, whiteColor, blackColor, blackColor) + } + else -> { + return ColorDrawable(whiteColor) + } } - } return GradientDrawable( GradientDrawable.Orientation.TOP_BOTTOM, @@ -530,14 +570,12 @@ object ImageUtil { ) } - private fun @receiver:ColorInt Int.isDark(): Boolean = - red < 40 && blue < 40 && green < 40 && alpha > 200 + private fun @receiver:ColorInt Int.isDark(): Boolean = red < 40 && blue < 40 && green < 40 && alpha > 200 private fun @receiver:ColorInt Int.isCloseTo(other: Int): Boolean = abs(red - other.red) < 30 && abs(green - other.green) < 30 && abs(blue - other.blue) < 30 - private fun @receiver:ColorInt Int.isWhite(): Boolean = - red + blue + green > 740 + private fun @receiver:ColorInt Int.isWhite(): Boolean = red + blue + green > 740 /** * Used to check an image's dimensions without loading it in the memory. @@ -548,22 +586,22 @@ object ImageUtil { return options } - private fun getBitmapRegionDecoder(imageStream: InputStream): BitmapRegionDecoder? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + private fun getBitmapRegionDecoder(imageStream: InputStream): BitmapRegionDecoder? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { BitmapRegionDecoder.newInstance(imageStream) } else { @Suppress("DEPRECATION") BitmapRegionDecoder.newInstance(imageStream, false) } - } private val optimalImageHeight = getDisplayMaxHeightInPx * 2 // Android doesn't include some mappings - private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf( - // https://issuetracker.google.com/issues/182703810 - "image/jxl" to "jxl", - ) + private val SUPPLEMENTARY_MIMETYPE_MAPPING = + mapOf( + // https://issuetracker.google.com/issues/182703810 + "image/jxl" to "jxl", + ) } val getDisplayMaxHeightInPx: Int diff --git a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt b/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt index 24910d9683..8b3c55e6ff 100644 --- a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt +++ b/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt @@ -11,27 +11,27 @@ import tachiyomi.data.DatabaseHandler class ExtensionRepoRepositoryImpl( private val handler: DatabaseHandler, ) : ExtensionRepoRepository { - override fun subscribeAll(): Flow> { - return handler.subscribeToList { extension_reposQueries.findAll(::mapExtensionRepo) } - } + override fun subscribeAll(): Flow> = + handler.subscribeToList { + extension_reposQueries.findAll(::mapExtensionRepo) + } - override suspend fun getAll(): List { - return handler.awaitList { extension_reposQueries.findAll(::mapExtensionRepo) } - } + override suspend fun getAll(): List = + handler.awaitList { + extension_reposQueries.findAll(::mapExtensionRepo) + } - override suspend fun getRepo(baseUrl: String): ExtensionRepo? { - return handler.awaitOneOrNull { extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) } - } + override suspend fun getRepo(baseUrl: String): ExtensionRepo? = + handler.awaitOneOrNull { + extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) + } - override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? { - return handler.awaitOneOrNull { + override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? = + handler.awaitOneOrNull { extension_reposQueries.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo) } - } - override fun getCount(): Flow { - return handler.subscribeToOne { extension_reposQueries.count() }.map { it.toInt() } - } + override fun getCount(): Flow = handler.subscribeToOne { extension_reposQueries.count() }.map { it.toInt() } override suspend fun insertRepo( baseUrl: String, @@ -73,9 +73,7 @@ class ExtensionRepoRepositoryImpl( } } - override suspend fun deleteRepo(baseUrl: String) { - return handler.await { extension_reposQueries.delete(baseUrl) } - } + override suspend fun deleteRepo(baseUrl: String) = handler.await { extension_reposQueries.delete(baseUrl) } private fun mapExtensionRepo( baseUrl: String, @@ -83,11 +81,12 @@ class ExtensionRepoRepositoryImpl( shortName: String?, website: String, signingKeyFingerprint: String, - ): ExtensionRepo = ExtensionRepo( - baseUrl = baseUrl, - name = name, - shortName = shortName, - website = website, - signingKeyFingerprint = signingKeyFingerprint, - ) + ): ExtensionRepo = + ExtensionRepo( + baseUrl = baseUrl, + name = name, + shortName = shortName, + website = website, + signingKeyFingerprint = signingKeyFingerprint, + ) } diff --git a/data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt b/data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt index be74a1102a..a80742d598 100644 --- a/data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt +++ b/data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt @@ -19,74 +19,63 @@ class AndroidDatabaseHandler( val queryDispatcher: CoroutineDispatcher = Dispatchers.IO, val transactionDispatcher: CoroutineDispatcher = queryDispatcher, ) : DatabaseHandler { - val suspendingTransactionId = ThreadLocal() - override suspend fun await(inTransaction: Boolean, block: suspend Database.() -> T): T { - return dispatch(inTransaction, block) - } + override suspend fun await( + inTransaction: Boolean, + block: suspend Database.() -> T, + ): T = dispatch(inTransaction, block) override suspend fun awaitList( inTransaction: Boolean, block: suspend Database.() -> Query, - ): List { - return dispatch(inTransaction) { block(db).executeAsList() } - } + ): List = dispatch(inTransaction) { block(db).executeAsList() } override suspend fun awaitOne( inTransaction: Boolean, block: suspend Database.() -> Query, - ): T { - return dispatch(inTransaction) { block(db).executeAsOne() } - } + ): T = dispatch(inTransaction) { block(db).executeAsOne() } override suspend fun awaitOneExecutable( inTransaction: Boolean, block: suspend Database.() -> ExecutableQuery, - ): T { - return dispatch(inTransaction) { block(db).executeAsOne() } - } + ): T = dispatch(inTransaction) { block(db).executeAsOne() } override suspend fun awaitOneOrNull( inTransaction: Boolean, block: suspend Database.() -> Query, - ): T? { - return dispatch(inTransaction) { block(db).executeAsOneOrNull() } - } + ): T? = dispatch(inTransaction) { block(db).executeAsOneOrNull() } override suspend fun awaitOneOrNullExecutable( inTransaction: Boolean, block: suspend Database.() -> ExecutableQuery, - ): T? { - return dispatch(inTransaction) { block(db).executeAsOneOrNull() } - } + ): T? = dispatch(inTransaction) { block(db).executeAsOneOrNull() } - override fun subscribeToList(block: Database.() -> Query): Flow> { - return block(db).asFlow().mapToList(queryDispatcher) - } + override fun subscribeToList(block: Database.() -> Query): Flow> = + block(db).asFlow().mapToList(queryDispatcher) - override fun subscribeToOne(block: Database.() -> Query): Flow { - return block(db).asFlow().mapToOne(queryDispatcher) - } + override fun subscribeToOne(block: Database.() -> Query): Flow = + block(db).asFlow().mapToOne(queryDispatcher) - override fun subscribeToOneOrNull(block: Database.() -> Query): Flow { - return block(db).asFlow().mapToOneOrNull(queryDispatcher) - } + override fun subscribeToOneOrNull(block: Database.() -> Query): Flow = + block(db).asFlow().mapToOneOrNull(queryDispatcher) override fun subscribeToPagingSource( countQuery: Database.() -> Query, queryProvider: Database.(Long, Long) -> Query, - ): PagingSource { - return QueryPagingSource( + ): PagingSource = + QueryPagingSource( handler = this, countQuery = countQuery, queryProvider = { limit, offset -> queryProvider.invoke(db, limit, offset) }, ) - } - private suspend fun dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T { + private suspend fun dispatch( + inTransaction: Boolean, + block: suspend Database.() -> T, + ): T { // Create a transaction if needed and run the calling block inside it. if (inTransaction) { return withTransaction { block(db) } diff --git a/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt b/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt index 596a31286d..ddde7e513d 100644 --- a/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt +++ b/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt @@ -6,19 +6,24 @@ import java.util.Date object DateColumnAdapter : ColumnAdapter { override fun decode(databaseValue: Long): Date = Date(databaseValue) + override fun encode(value: Date): Long = value.time } private const val LIST_OF_STRINGS_SEPARATOR = ", " + object StringListColumnAdapter : ColumnAdapter, String> { - override fun decode(databaseValue: String) = if (databaseValue.isEmpty()) { - emptyList() - } else { - databaseValue.split(LIST_OF_STRINGS_SEPARATOR) - } - override fun encode(value: List) = value.joinToString( - separator = LIST_OF_STRINGS_SEPARATOR, - ) + override fun decode(databaseValue: String) = + if (databaseValue.isEmpty()) { + emptyList() + } else { + databaseValue.split(LIST_OF_STRINGS_SEPARATOR) + } + + override fun encode(value: List) = + value.joinToString( + separator = LIST_OF_STRINGS_SEPARATOR, + ) } object UpdateStrategyColumnAdapter : ColumnAdapter { diff --git a/data/src/main/java/tachiyomi/data/DatabaseHandler.kt b/data/src/main/java/tachiyomi/data/DatabaseHandler.kt index 57f58f7909..d0eef2bf2d 100644 --- a/data/src/main/java/tachiyomi/data/DatabaseHandler.kt +++ b/data/src/main/java/tachiyomi/data/DatabaseHandler.kt @@ -6,8 +6,10 @@ import app.cash.sqldelight.Query import kotlinx.coroutines.flow.Flow interface DatabaseHandler { - - suspend fun await(inTransaction: Boolean = false, block: suspend Database.() -> T): T + suspend fun await( + inTransaction: Boolean = false, + block: suspend Database.() -> T, + ): T suspend fun awaitList( inTransaction: Boolean = false, diff --git a/data/src/main/java/tachiyomi/data/QueryPagingSource.kt b/data/src/main/java/tachiyomi/data/QueryPagingSource.kt index 321f637bc2..7a184290e7 100644 --- a/data/src/main/java/tachiyomi/data/QueryPagingSource.kt +++ b/data/src/main/java/tachiyomi/data/QueryPagingSource.kt @@ -9,8 +9,8 @@ class QueryPagingSource( val handler: DatabaseHandler, val countQuery: Database.() -> Query, val queryProvider: Database.(Long, Long) -> Query, -) : PagingSource(), Query.Listener { - +) : PagingSource(), + Query.Listener { override val jumpingSupported: Boolean = true private var currentQuery: Query? by Delegates.observable(null) { _, old, new -> @@ -31,20 +31,27 @@ class QueryPagingSource( val loadSize = params.loadSize val count = handler.awaitOne { countQuery() } - val (offset, limit) = when (params) { - is LoadParams.Prepend -> key - loadSize to loadSize.toLong() - else -> key to loadSize.toLong() - } + val (offset, limit) = + when (params) { + is LoadParams.Prepend -> key - loadSize to loadSize.toLong() + else -> key to loadSize.toLong() + } - val data = handler.awaitList { - queryProvider(limit, offset) - .also { currentQuery = it } - } + val data = + handler.awaitList { + queryProvider(limit, offset) + .also { currentQuery = it } + } - val (prevKey, nextKey) = when (params) { - is LoadParams.Append -> { offset - loadSize to offset + loadSize } - else -> { offset to offset + loadSize } - } + val (prevKey, nextKey) = + when (params) { + is LoadParams.Append -> { + offset - loadSize to offset + loadSize + } + else -> { + offset to offset + loadSize + } + } return LoadResult.Page( data = data, @@ -58,12 +65,11 @@ class QueryPagingSource( } } - override fun getRefreshKey(state: PagingState): Long? { - return state.anchorPosition?.let { anchorPosition -> + override fun getRefreshKey(state: PagingState): Long? = + state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey ?: anchorPage?.nextKey } - } override fun queryResultsChanged() { invalidate() diff --git a/data/src/main/java/tachiyomi/data/TransactionContext.kt b/data/src/main/java/tachiyomi/data/TransactionContext.kt index 63a2b7519a..7c80ed0afd 100644 --- a/data/src/main/java/tachiyomi/data/TransactionContext.kt +++ b/data/src/main/java/tachiyomi/data/TransactionContext.kt @@ -17,9 +17,8 @@ import kotlin.coroutines.resume /** * Returns the transaction dispatcher if we are on a transaction, or the database dispatchers. */ -internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext { - return coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher -} +internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext = + coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher /** * Calls the specified suspending [block] in a database transaction. The transaction will be @@ -94,10 +93,8 @@ private suspend fun AndroidDatabaseHandler.createTransactionContext(): Coroutine * coroutines to the acquired thread. The [controlJob] is used to control the release of the * thread by cancelling the job. */ -private suspend fun CoroutineDispatcher.acquireTransactionThread( - controlJob: Job, -): ContinuationInterceptor { - return suspendCancellableCoroutine { continuation -> +private suspend fun CoroutineDispatcher.acquireTransactionThread(controlJob: Job): ContinuationInterceptor = + suspendCancellableCoroutine { continuation -> continuation.invokeOnCancellation { // We got cancelled while waiting to acquire a thread, we can't stop our attempt to // acquire a thread, but we can cancel the controlling job so once it gets acquired it @@ -122,7 +119,6 @@ private suspend fun CoroutineDispatcher.acquireTransactionThread( ) } } -} /** * A [CoroutineContext.Element] that indicates there is an on-going database transaction. @@ -131,7 +127,6 @@ private class TransactionElement( private val transactionThreadControlJob: Job, val transactionDispatcher: ContinuationInterceptor, ) : CoroutineContext.Element { - companion object Key : CoroutineContext.Key override val key: CoroutineContext.Key diff --git a/data/src/main/java/tachiyomi/data/category/CategoryRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/category/CategoryRepositoryImpl.kt index bf379978e4..74a7c00e19 100644 --- a/data/src/main/java/tachiyomi/data/category/CategoryRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/category/CategoryRepositoryImpl.kt @@ -10,30 +10,25 @@ import tachiyomi.domain.category.repository.CategoryRepository class CategoryRepositoryImpl( private val handler: DatabaseHandler, ) : CategoryRepository { + override suspend fun get(id: Long): Category? = + handler.awaitOneOrNull { categoriesQueries.getCategory(id, ::mapCategory) } - override suspend fun get(id: Long): Category? { - return handler.awaitOneOrNull { categoriesQueries.getCategory(id, ::mapCategory) } - } - - override suspend fun getAll(): List { - return handler.awaitList { categoriesQueries.getCategories(::mapCategory) } - } + override suspend fun getAll(): List = handler.awaitList { categoriesQueries.getCategories(::mapCategory) } - override fun getAllAsFlow(): Flow> { - return handler.subscribeToList { categoriesQueries.getCategories(::mapCategory) } - } + override fun getAllAsFlow(): Flow> = + handler.subscribeToList { + categoriesQueries.getCategories(::mapCategory) + } - override suspend fun getCategoriesByMangaId(mangaId: Long): List { - return handler.awaitList { + override suspend fun getCategoriesByMangaId(mangaId: Long): List = + handler.awaitList { categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory) } - } - override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow> { - return handler.subscribeToList { + override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow> = + handler.subscribeToList { categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory) } - } override suspend fun insert(category: Category) { handler.await { @@ -87,12 +82,11 @@ class CategoryRepositoryImpl( name: String, order: Long, flags: Long, - ): Category { - return Category( + ): Category = + Category( id = id, name = name, order = order, flags = flags, ) - } } diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt index 099ccb0da9..fae21097ee 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt @@ -12,9 +12,8 @@ import tachiyomi.domain.chapter.repository.ChapterRepository class ChapterRepositoryImpl( private val handler: DatabaseHandler, ) : ChapterRepository { - - override suspend fun addAll(chapters: List): List { - return try { + override suspend fun addAll(chapters: List): List = + try { handler.await(inTransaction = true) { chapters.map { chapter -> chaptersQueries.insert( @@ -39,7 +38,6 @@ class ChapterRepositoryImpl( logcat(LogPriority.ERROR, e) emptyList() } - } override suspend fun update(chapterUpdate: ChapterUpdate) { partialUpdate(chapterUpdate) @@ -80,52 +78,56 @@ class ChapterRepositoryImpl( } } - override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List { - return handler.awaitList { + override suspend fun getChapterByMangaId( + mangaId: Long, + applyScanlatorFilter: Boolean, + ): List = + handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter) } - } - override suspend fun getScanlatorsByMangaId(mangaId: Long): List { - return handler.awaitList { + override suspend fun getScanlatorsByMangaId(mangaId: Long): List = + handler.awaitList { chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } } - } - override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow> { - return handler.subscribeToList { + override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow> = + handler.subscribeToList { chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } } - } - override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List { - return handler.awaitList { + override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List = + handler.awaitList { chaptersQueries.getBookmarkedChaptersByMangaId( mangaId, ::mapChapter, ) } - } - override suspend fun getChapterById(id: Long): Chapter? { - return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, ::mapChapter) } - } + override suspend fun getChapterById(id: Long): Chapter? = + handler.awaitOneOrNull { + chaptersQueries.getChapterById(id, ::mapChapter) + } - override suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean): Flow> { - return handler.subscribeToList { + override suspend fun getChapterByMangaIdAsFlow( + mangaId: Long, + applyScanlatorFilter: Boolean, + ): Flow> = + handler.subscribeToList { chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter) } - } - override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? { - return handler.awaitOneOrNull { + override suspend fun getChapterByUrlAndMangaId( + url: String, + mangaId: Long, + ): Chapter? = + handler.awaitOneOrNull { chaptersQueries.getChapterByUrlAndMangaId( url, mangaId, ::mapChapter, ) } - } @Suppress("LongParameterList") private fun mapChapter( @@ -145,20 +147,21 @@ class ChapterRepositoryImpl( version: Long, @Suppress("UNUSED_PARAMETER") isSyncing: Long, - ): Chapter = Chapter( - id = id, - mangaId = mangaId, - read = read, - bookmark = bookmark, - lastPageRead = lastPageRead, - dateFetch = dateFetch, - sourceOrder = sourceOrder, - url = url, - name = name, - dateUpload = dateUpload, - chapterNumber = chapterNumber, - scanlator = scanlator, - lastModifiedAt = lastModifiedAt, - version = version, - ) + ): Chapter = + Chapter( + id = id, + mangaId = mangaId, + read = read, + bookmark = bookmark, + lastPageRead = lastPageRead, + dateFetch = dateFetch, + sourceOrder = sourceOrder, + url = url, + name = name, + dateUpload = dateUpload, + chapterNumber = chapterNumber, + scanlator = scanlator, + lastModifiedAt = lastModifiedAt, + version = version, + ) } diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterSanitizer.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterSanitizer.kt index ee1b3d698e..11bc3940f2 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterSanitizer.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterSanitizer.kt @@ -1,46 +1,44 @@ package tachiyomi.data.chapter object ChapterSanitizer { - - fun String.sanitize(title: String): String { - return trim() + fun String.sanitize(title: String): String = + trim() .removePrefix(title) .trim(*CHAPTER_TRIM_CHARS) - } - - private val CHAPTER_TRIM_CHARS = arrayOf( - // Whitespace - ' ', - '\u0009', - '\u000A', - '\u000B', - '\u000C', - '\u000D', - '\u0020', - '\u0085', - '\u00A0', - '\u1680', - '\u2000', - '\u2001', - '\u2002', - '\u2003', - '\u2004', - '\u2005', - '\u2006', - '\u2007', - '\u2008', - '\u2009', - '\u200A', - '\u2028', - '\u2029', - '\u202F', - '\u205F', - '\u3000', - // Separators - '-', - '_', - ',', - ':', - ).toCharArray() + private val CHAPTER_TRIM_CHARS = + arrayOf( + // Whitespace + ' ', + '\u0009', + '\u000A', + '\u000B', + '\u000C', + '\u000D', + '\u0020', + '\u0085', + '\u00A0', + '\u1680', + '\u2000', + '\u2001', + '\u2002', + '\u2003', + '\u2004', + '\u2005', + '\u2006', + '\u2007', + '\u2008', + '\u2009', + '\u200A', + '\u2028', + '\u2029', + '\u202F', + '\u205F', + '\u3000', + // Separators + '-', + '_', + ',', + ':', + ).toCharArray() } diff --git a/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt b/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt index 709c5800d8..dcab128bbb 100644 --- a/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt +++ b/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt @@ -11,12 +11,13 @@ object HistoryMapper { chapterId: Long, readAt: Date?, readDuration: Long, - ): History = History( - id = id, - chapterId = chapterId, - readAt = readAt, - readDuration = readDuration, - ) + ): History = + History( + id = id, + chapterId = chapterId, + readAt = readAt, + readDuration = readDuration, + ) fun mapHistoryWithRelations( historyId: Long, @@ -30,20 +31,22 @@ object HistoryMapper { chapterNumber: Double, readAt: Date?, readDuration: Long, - ): HistoryWithRelations = HistoryWithRelations( - id = historyId, - chapterId = chapterId, - mangaId = mangaId, - title = title, - chapterNumber = chapterNumber, - readAt = readAt, - readDuration = readDuration, - coverData = MangaCover( + ): HistoryWithRelations = + HistoryWithRelations( + id = historyId, + chapterId = chapterId, mangaId = mangaId, - sourceId = sourceId, - isMangaFavorite = isFavorite, - url = thumbnailUrl, - lastModified = coverLastModified, - ), - ) + title = title, + chapterNumber = chapterNumber, + readAt = readAt, + readDuration = readDuration, + coverData = + MangaCover( + mangaId = mangaId, + sourceId = sourceId, + isMangaFavorite = isFavorite, + url = thumbnailUrl, + lastModified = coverLastModified, + ), + ) } diff --git a/data/src/main/java/tachiyomi/data/history/HistoryRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/history/HistoryRepositoryImpl.kt index e9157025c0..b95598f9c6 100644 --- a/data/src/main/java/tachiyomi/data/history/HistoryRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/history/HistoryRepositoryImpl.kt @@ -12,26 +12,22 @@ import tachiyomi.domain.history.repository.HistoryRepository class HistoryRepositoryImpl( private val handler: DatabaseHandler, ) : HistoryRepository { - - override fun getHistory(query: String): Flow> { - return handler.subscribeToList { + override fun getHistory(query: String): Flow> = + handler.subscribeToList { historyViewQueries.history(query, HistoryMapper::mapHistoryWithRelations) } - } - override suspend fun getLastHistory(): HistoryWithRelations? { - return handler.awaitOneOrNull { + override suspend fun getLastHistory(): HistoryWithRelations? = + handler.awaitOneOrNull { historyViewQueries.getLatestHistory(HistoryMapper::mapHistoryWithRelations) } - } - override suspend fun getTotalReadDuration(): Long { - return handler.awaitOne { historyQueries.getReadDuration() } - } + override suspend fun getTotalReadDuration(): Long = handler.awaitOne { historyQueries.getReadDuration() } - override suspend fun getHistoryByMangaId(mangaId: Long): List { - return handler.awaitList { historyQueries.getHistoryByMangaId(mangaId, HistoryMapper::mapHistory) } - } + override suspend fun getHistoryByMangaId(mangaId: Long): List = + handler.awaitList { + historyQueries.getHistoryByMangaId(mangaId, HistoryMapper::mapHistory) + } override suspend fun resetHistory(historyId: Long) { try { @@ -49,15 +45,14 @@ class HistoryRepositoryImpl( } } - override suspend fun deleteAllHistory(): Boolean { - return try { + override suspend fun deleteAllHistory(): Boolean = + try { handler.await { historyQueries.removeAllHistory() } true } catch (e: Exception) { logcat(LogPriority.ERROR, throwable = e) false } - } override suspend fun upsertHistory(historyUpdate: HistoryUpdate) { try { diff --git a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt index 8f0a68d43e..e94c46aaf4 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt @@ -32,31 +32,32 @@ object MangaMapper { version: Long, @Suppress("UNUSED_PARAMETER") isSyncing: Long, - ): Manga = Manga( - id = id, - source = source, - favorite = favorite, - lastUpdate = lastUpdate ?: 0, - nextUpdate = nextUpdate ?: 0, - fetchInterval = calculateInterval.toInt(), - dateAdded = dateAdded, - viewerFlags = viewerFlags, - chapterFlags = chapterFlags, - coverLastModified = coverLastModified, - url = url, - title = title, - artist = artist, - author = author, - description = description, - genre = genre, - status = status, - thumbnailUrl = thumbnailUrl, - updateStrategy = updateStrategy, - initialized = initialized, - lastModifiedAt = lastModifiedAt, - favoriteModifiedAt = favoriteModifiedAt, - version = version, - ) + ): Manga = + Manga( + id = id, + source = source, + favorite = favorite, + lastUpdate = lastUpdate ?: 0, + nextUpdate = nextUpdate ?: 0, + fetchInterval = calculateInterval.toInt(), + dateAdded = dateAdded, + viewerFlags = viewerFlags, + chapterFlags = chapterFlags, + coverLastModified = coverLastModified, + url = url, + title = title, + artist = artist, + author = author, + description = description, + genre = genre, + status = status, + thumbnailUrl = thumbnailUrl, + updateStrategy = updateStrategy, + initialized = initialized, + lastModifiedAt = lastModifiedAt, + favoriteModifiedAt = favoriteModifiedAt, + version = version, + ) @Suppress("LongParameterList") fun mapLibraryManga( @@ -91,39 +92,41 @@ object MangaMapper { lastRead: Long, bookmarkCount: Double, category: Long, - ): LibraryManga = LibraryManga( - manga = mapManga( - id, - source, - url, - artist, - author, - description, - genre, - title, - status, - thumbnailUrl, - favorite, - lastUpdate, - nextUpdate, - initialized, - viewerFlags, - chapterFlags, - coverLastModified, - dateAdded, - updateStrategy, - calculateInterval, - lastModifiedAt, - favoriteModifiedAt, - version, - isSyncing, - ), - category = category, - totalChapters = totalCount, - readCount = readCount.toLong(), - bookmarkCount = bookmarkCount.toLong(), - latestUpload = latestUpload, - chapterFetchedAt = chapterFetchedAt, - lastRead = lastRead, - ) + ): LibraryManga = + LibraryManga( + manga = + mapManga( + id, + source, + url, + artist, + author, + description, + genre, + title, + status, + thumbnailUrl, + favorite, + lastUpdate, + nextUpdate, + initialized, + viewerFlags, + chapterFlags, + coverLastModified, + dateAdded, + updateStrategy, + calculateInterval, + lastModifiedAt, + favoriteModifiedAt, + version, + isSyncing, + ), + category = category, + totalChapters = totalCount, + readCount = readCount.toLong(), + bookmarkCount = bookmarkCount.toLong(), + latestUpload = latestUpload, + chapterFetchedAt = chapterFetchedAt, + lastRead = lastRead, + ) } diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index 06b7bde583..173229752e 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -16,56 +16,65 @@ import java.time.ZoneId class MangaRepositoryImpl( private val handler: DatabaseHandler, ) : MangaRepository { + override suspend fun getMangaById(id: Long): Manga = + handler.awaitOne { mangasQueries.getMangaById(id, MangaMapper::mapManga) } - override suspend fun getMangaById(id: Long): Manga { - return handler.awaitOne { mangasQueries.getMangaById(id, MangaMapper::mapManga) } - } - - override suspend fun getMangaByIdAsFlow(id: Long): Flow { - return handler.subscribeToOne { mangasQueries.getMangaById(id, MangaMapper::mapManga) } - } + override suspend fun getMangaByIdAsFlow(id: Long): Flow = + handler.subscribeToOne { + mangasQueries.getMangaById(id, MangaMapper::mapManga) + } - override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? { - return handler.awaitOneOrNull { + override suspend fun getMangaByUrlAndSourceId( + url: String, + sourceId: Long, + ): Manga? = + handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource( url, sourceId, MangaMapper::mapManga, ) } - } - override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow { - return handler.subscribeToOneOrNull { + override fun getMangaByUrlAndSourceIdAsFlow( + url: String, + sourceId: Long, + ): Flow = + handler.subscribeToOneOrNull { mangasQueries.getMangaByUrlAndSource( url, sourceId, MangaMapper::mapManga, ) } - } - override suspend fun getFavorites(): List { - return handler.awaitList { mangasQueries.getFavorites(MangaMapper::mapManga) } - } + override suspend fun getFavorites(): List = + handler.awaitList { + mangasQueries.getFavorites(MangaMapper::mapManga) + } - override suspend fun getLibraryManga(): List { - return handler.awaitList { libraryViewQueries.library(MangaMapper::mapLibraryManga) } - } + override suspend fun getLibraryManga(): List = + handler.awaitList { + libraryViewQueries.library(MangaMapper::mapLibraryManga) + } - override fun getLibraryMangaAsFlow(): Flow> { - return handler.subscribeToList { libraryViewQueries.library(MangaMapper::mapLibraryManga) } - } + override fun getLibraryMangaAsFlow(): Flow> = + handler.subscribeToList { + libraryViewQueries.library(MangaMapper::mapLibraryManga) + } - override fun getFavoritesBySourceId(sourceId: Long): Flow> { - return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) } - } + override fun getFavoritesBySourceId(sourceId: Long): Flow> = + handler.subscribeToList { + mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) + } - override suspend fun getDuplicateLibraryManga(id: Long, title: String): List { - return handler.awaitList { + override suspend fun getDuplicateLibraryManga( + id: Long, + title: String, + ): List = + handler.awaitList { mangasQueries.getDuplicateLibraryManga(title, id, MangaMapper::mapManga) } - } @Suppress("MagicNumber") override suspend fun getUpcomingManga(statuses: Set): Flow> { @@ -75,17 +84,19 @@ class MangaRepositoryImpl( } } - override suspend fun resetViewerFlags(): Boolean { - return try { + override suspend fun resetViewerFlags(): Boolean = + try { handler.await { mangasQueries.resetViewerFlags() } true } catch (e: Exception) { logcat(LogPriority.ERROR, e) false } - } - override suspend fun setMangaCategories(mangaId: Long, categoryIds: List) { + override suspend fun setMangaCategories( + mangaId: Long, + categoryIds: List, + ) { handler.await(inTransaction = true) { mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId) categoryIds.map { categoryId -> @@ -94,8 +105,8 @@ class MangaRepositoryImpl( } } - override suspend fun insert(manga: Manga): Long? { - return handler.awaitOneOrNullExecutable(inTransaction = true) { + override suspend fun insert(manga: Manga): Long? = + handler.awaitOneOrNullExecutable(inTransaction = true) { mangasQueries.insert( source = manga.source, url = manga.url, @@ -120,27 +131,24 @@ class MangaRepositoryImpl( ) mangasQueries.selectLastInsertedRowId() } - } - override suspend fun update(update: MangaUpdate): Boolean { - return try { + override suspend fun update(update: MangaUpdate): Boolean = + try { partialUpdate(update) true } catch (e: Exception) { logcat(LogPriority.ERROR, e) false } - } - override suspend fun updateAll(mangaUpdates: List): Boolean { - return try { + override suspend fun updateAll(mangaUpdates: List): Boolean = + try { partialUpdate(*mangaUpdates.toTypedArray()) true } catch (e: Exception) { logcat(LogPriority.ERROR, e) false } - } private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) { handler.await(inTransaction = true) { diff --git a/data/src/main/java/tachiyomi/data/release/GithubRelease.kt b/data/src/main/java/tachiyomi/data/release/GithubRelease.kt index f7fa169236..4689a6c1a0 100644 --- a/data/src/main/java/tachiyomi/data/release/GithubRelease.kt +++ b/data/src/main/java/tachiyomi/data/release/GithubRelease.kt @@ -19,7 +19,9 @@ data class GithubRelease( * Assets class containing download url. */ @Serializable -data class GitHubAssets(@SerialName("browser_download_url") val downloadLink: String) +data class GitHubAssets( + @SerialName("browser_download_url") val downloadLink: String, +) /** * Regular expression that matches a mention to a valid GitHub username, like it's diff --git a/data/src/main/java/tachiyomi/data/release/ReleaseServiceImpl.kt b/data/src/main/java/tachiyomi/data/release/ReleaseServiceImpl.kt index ea2363d53b..1495180147 100644 --- a/data/src/main/java/tachiyomi/data/release/ReleaseServiceImpl.kt +++ b/data/src/main/java/tachiyomi/data/release/ReleaseServiceImpl.kt @@ -12,14 +12,12 @@ class ReleaseServiceImpl( private val networkService: NetworkHelper, private val json: Json, ) : ReleaseService { - - override suspend fun latest(repository: String): Release { - return with(json) { + override suspend fun latest(repository: String): Release = + with(json) { networkService.client .newCall(GET("https://api.github.com/repos/$repository/releases/latest")) .awaitSuccess() .parseAs() .let(releaseMapper) } - } } diff --git a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt index fc091c4917..bd77d6f97f 100644 --- a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt @@ -8,43 +8,45 @@ import eu.kanade.tachiyomi.source.model.SManga import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.source.repository.SourcePagingSourceType -class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : - SourcePagingSource(source) { - override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.getSearchManga(currentPage, query, filters) - } +class SourceSearchPagingSource( + source: CatalogueSource, + val query: String, + val filters: FilterList, +) : SourcePagingSource(source) { + override suspend fun requestNextPage(currentPage: Int): MangasPage = + source.getSearchManga(currentPage, query, filters) } -class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) { - override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.getPopularManga(currentPage) - } +class SourcePopularPagingSource( + source: CatalogueSource, +) : SourcePagingSource(source) { + override suspend fun requestNextPage(currentPage: Int): MangasPage = source.getPopularManga(currentPage) } -class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) { - override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.getLatestUpdates(currentPage) - } +class SourceLatestPagingSource( + source: CatalogueSource, +) : SourcePagingSource(source) { + override suspend fun requestNextPage(currentPage: Int): MangasPage = source.getLatestUpdates(currentPage) } abstract class SourcePagingSource( protected val source: CatalogueSource, ) : SourcePagingSourceType() { - abstract suspend fun requestNextPage(currentPage: Int): MangasPage override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: 1 - val mangasPage = try { - withIOContext { - requestNextPage(page.toInt()) - .takeIf { it.mangas.isNotEmpty() } - ?: throw NoResultsException() + val mangasPage = + try { + withIOContext { + requestNextPage(page.toInt()) + .takeIf { it.mangas.isNotEmpty() } + ?: throw NoResultsException() + } + } catch (e: Exception) { + return LoadResult.Error(e) } - } catch (e: Exception) { - return LoadResult.Error(e) - } return LoadResult.Page( data = mangasPage.mangas, @@ -53,12 +55,11 @@ abstract class SourcePagingSource( ) } - override fun getRefreshKey(state: PagingState): Long? { - return state.anchorPosition?.let { anchorPosition -> + override fun getRefreshKey(state: PagingState): Long? = + state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey ?: anchorPage?.nextKey } - } } class NoResultsException : Exception() diff --git a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt index f2b2e0e057..4d76596fd0 100644 --- a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt @@ -19,40 +19,37 @@ class SourceRepositoryImpl( private val sourceManager: SourceManager, private val handler: DatabaseHandler, ) : SourceRepository { - - override fun getSources(): Flow> { - return sourceManager.catalogueSources.map { sources -> + override fun getSources(): Flow> = + sourceManager.catalogueSources.map { sources -> sources.map { mapSourceToDomainSource(it).copy( supportsLatest = it.supportsLatest, ) } } - } - override fun getOnlineSources(): Flow> { - return sourceManager.catalogueSources.map { sources -> + override fun getOnlineSources(): Flow> = + sourceManager.catalogueSources.map { sources -> sources .filterIsInstance() .map(::mapSourceToDomainSource) } - } - override fun getSourcesWithFavoriteCount(): Flow>> { - return combine( + override fun getSourcesWithFavoriteCount(): Flow>> = + combine( handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }, sourceManager.catalogueSources, ) { sourceIdWithFavoriteCount, _ -> sourceIdWithFavoriteCount } .map { it.map { (sourceId, count) -> val source = sourceManager.getOrStub(sourceId) - val domainSource = mapSourceToDomainSource(source).copy( - isStub = source is StubSource, - ) + val domainSource = + mapSourceToDomainSource(source).copy( + isStub = source is StubSource, + ) domainSource to count } } - } override fun getSourcesWithNonLibraryManga(): Flow> { val sourceIdWithNonLibraryManga = @@ -60,9 +57,10 @@ class SourceRepositoryImpl( return sourceIdWithNonLibraryManga.map { sourceId -> sourceId.map { (sourceId, count) -> val source = sourceManager.getOrStub(sourceId) - val domainSource = mapSourceToDomainSource(source).copy( - isStub = source is StubSource, - ) + val domainSource = + mapSourceToDomainSource(source).copy( + isStub = source is StubSource, + ) SourceWithCount(domainSource, count) } } @@ -87,11 +85,12 @@ class SourceRepositoryImpl( return SourceLatestPagingSource(source) } - private fun mapSourceToDomainSource(source: Source): DomainSource = DomainSource( - id = source.id, - lang = source.lang, - name = source.name, - supportsLatest = false, - isStub = false, - ) + private fun mapSourceToDomainSource(source: Source): DomainSource = + DomainSource( + id = source.id, + lang = source.lang, + name = source.name, + supportsLatest = false, + isStub = false, + ) } diff --git a/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt index 157e1eb2f1..8489f69caa 100644 --- a/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt @@ -8,16 +8,21 @@ import tachiyomi.domain.source.repository.StubSourceRepository class StubSourceRepositoryImpl( private val handler: DatabaseHandler, ) : StubSourceRepository { + override fun subscribeAll(): Flow> = + handler.subscribeToList { + sourcesQueries.findAll(::mapStubSource) + } - override fun subscribeAll(): Flow> { - return handler.subscribeToList { sourcesQueries.findAll(::mapStubSource) } - } - - override suspend fun getStubSource(id: Long): StubSource? { - return handler.awaitOneOrNull { sourcesQueries.findOne(id, ::mapStubSource) } - } + override suspend fun getStubSource(id: Long): StubSource? = + handler.awaitOneOrNull { + sourcesQueries.findOne(id, ::mapStubSource) + } - override suspend fun upsertStubSource(id: Long, lang: String, name: String) { + override suspend fun upsertStubSource( + id: Long, + lang: String, + name: String, + ) { handler.await { sourcesQueries.upsert(id, lang, name) } } diff --git a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt index ee941d49c5..d5fa691446 100644 --- a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt +++ b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt @@ -17,19 +17,20 @@ object TrackMapper { remoteUrl: String, startDate: Long, finishDate: Long, - ): Track = Track( - id = id, - mangaId = mangaId, - trackerId = syncId, - remoteId = remoteId, - libraryId = libraryId, - title = title, - lastChapterRead = lastChapterRead, - totalChapters = totalChapters, - status = status, - score = score, - remoteUrl = remoteUrl, - startDate = startDate, - finishDate = finishDate, - ) + ): Track = + Track( + id = id, + mangaId = mangaId, + trackerId = syncId, + remoteId = remoteId, + libraryId = libraryId, + title = title, + lastChapterRead = lastChapterRead, + totalChapters = totalChapters, + status = status, + score = score, + remoteUrl = remoteUrl, + startDate = startDate, + finishDate = finishDate, + ) } diff --git a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt index 6cd8396b11..e3dfd8e7c0 100644 --- a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt @@ -8,30 +8,28 @@ import tachiyomi.domain.track.repository.TrackRepository class TrackRepositoryImpl( private val handler: DatabaseHandler, ) : TrackRepository { + override suspend fun getTrackById(id: Long): Track? = + handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, TrackMapper::mapTrack) } - override suspend fun getTrackById(id: Long): Track? { - return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, TrackMapper::mapTrack) } - } - - override suspend fun getTracksByMangaId(mangaId: Long): List { - return handler.awaitList { + override suspend fun getTracksByMangaId(mangaId: Long): List = + handler.awaitList { manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack) } - } - override fun getTracksAsFlow(): Flow> { - return handler.subscribeToList { + override fun getTracksAsFlow(): Flow> = + handler.subscribeToList { manga_syncQueries.getTracks(TrackMapper::mapTrack) } - } - override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow> { - return handler.subscribeToList { + override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow> = + handler.subscribeToList { manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack) } - } - override suspend fun delete(mangaId: Long, trackerId: Long) { + override suspend fun delete( + mangaId: Long, + trackerId: Long, + ) { handler.await { manga_syncQueries.delete( mangaId = mangaId, diff --git a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt index 0af6bcd50c..727ee8e4d0 100644 --- a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt @@ -9,13 +9,12 @@ import tachiyomi.domain.updates.repository.UpdatesRepository class UpdatesRepositoryImpl( private val databaseHandler: DatabaseHandler, ) : UpdatesRepository { - override suspend fun awaitWithRead( read: Boolean, after: Long, limit: Long, - ): List { - return databaseHandler.awaitList { + ): List = + databaseHandler.awaitList { updatesViewQueries.getUpdatesByReadStatus( read = read, after = after, @@ -23,20 +22,21 @@ class UpdatesRepositoryImpl( mapper = ::mapUpdatesWithRelations, ) } - } - override fun subscribeAll(after: Long, limit: Long): Flow> { - return databaseHandler.subscribeToList { + override fun subscribeAll( + after: Long, + limit: Long, + ): Flow> = + databaseHandler.subscribeToList { updatesViewQueries.getRecentUpdates(after, limit, ::mapUpdatesWithRelations) } - } override fun subscribeWithRead( read: Boolean, after: Long, limit: Long, - ): Flow> { - return databaseHandler.subscribeToList { + ): Flow> = + databaseHandler.subscribeToList { updatesViewQueries.getUpdatesByReadStatus( read = read, after = after, @@ -44,7 +44,6 @@ class UpdatesRepositoryImpl( mapper = ::mapUpdatesWithRelations, ) } - } private fun mapUpdatesWithRelations( mangaId: Long, @@ -61,23 +60,25 @@ class UpdatesRepositoryImpl( coverLastModified: Long, dateUpload: Long, dateFetch: Long, - ): UpdatesWithRelations = UpdatesWithRelations( - mangaId = mangaId, - mangaTitle = mangaTitle, - chapterId = chapterId, - chapterName = chapterName, - scanlator = scanlator, - read = read, - bookmark = bookmark, - lastPageRead = lastPageRead, - sourceId = sourceId, - dateFetch = dateFetch, - coverData = MangaCover( + ): UpdatesWithRelations = + UpdatesWithRelations( mangaId = mangaId, + mangaTitle = mangaTitle, + chapterId = chapterId, + chapterName = chapterName, + scanlator = scanlator, + read = read, + bookmark = bookmark, + lastPageRead = lastPageRead, sourceId = sourceId, - isMangaFavorite = favorite, - url = thumbnailUrl, - lastModified = coverLastModified, - ), - ) + dateFetch = dateFetch, + coverData = + MangaCover( + mangaId = mangaId, + sourceId = sourceId, + isMangaFavorite = favorite, + url = thumbnailUrl, + lastModified = coverLastModified, + ), + ) } diff --git a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt b/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt index 4c6990be09..93ae15c4b1 100644 --- a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt +++ b/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt @@ -7,4 +7,6 @@ import java.io.IOException * * @param throwable the source throwable to include for tracing. */ -class SaveExtensionRepoException(throwable: Throwable) : IOException("Error Saving Repository to Database", throwable) +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 index ec9ca6e725..4ab9c6208a 100644 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt +++ b/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt @@ -62,10 +62,17 @@ class CreateExtensionRepo( } sealed interface Result { - data class DuplicateFingerprint(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : 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 } } diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt index a393e69d5a..b64b1d3fc0 100644 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt +++ b/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt @@ -11,12 +11,13 @@ class UpdateExtensionRepo( private val repository: ExtensionRepoRepository, private val service: ExtensionRepoService, ) { - - suspend fun awaitAll() = coroutineScope { - repository.getAll() - .map { async { await(it) } } - .awaitAll() - } + suspend fun awaitAll() = + coroutineScope { + repository + .getAll() + .map { async { await(it) } } + .awaitAll() + } suspend fun await(repo: ExtensionRepo) { val newRepo = service.fetchRepoDetails(repo.baseUrl) ?: return diff --git a/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt b/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt index 47be56dcf3..32f8602a96 100644 --- a/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt +++ b/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.flow.Flow import mihon.domain.extensionrepo.model.ExtensionRepo interface ExtensionRepoRepository { - fun subscribeAll(): Flow> suspend fun getAll(): List diff --git a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt index 6a0a492dee..ee59006292 100644 --- a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt +++ b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt @@ -16,12 +16,11 @@ data class ExtensionRepoDto( val signingKeyFingerprint: String, ) -fun ExtensionRepoMetaDto.toExtensionRepo(baseUrl: String): ExtensionRepo { - return ExtensionRepo( +fun ExtensionRepoMetaDto.toExtensionRepo(baseUrl: String): ExtensionRepo = + 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 index 8262961a7b..dc6d9d992f 100644 --- a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt +++ b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt @@ -18,15 +18,14 @@ class ExtensionRepoService( val client = networkHelper.client @Suppress("TooGenericExceptionCaught") - suspend fun fetchRepoDetails( - repo: String, - ): ExtensionRepo? { - return withIOContext { + suspend fun fetchRepoDetails(repo: String): ExtensionRepo? = + withIOContext { val url = "$repo/repo.json".toUri() try { with(json) { - client.newCall(GET(url.toString())) + client + .newCall(GET(url.toString())) .awaitSuccess() .parseAs() .toExtensionRepo(baseUrl = repo) @@ -36,5 +35,4 @@ class ExtensionRepoService( null } } - } } diff --git a/domain/src/main/java/mihon/domain/upcoming/interactor/GetUpcomingManga.kt b/domain/src/main/java/mihon/domain/upcoming/interactor/GetUpcomingManga.kt index dd618b9550..57bc98cbb0 100644 --- a/domain/src/main/java/mihon/domain/upcoming/interactor/GetUpcomingManga.kt +++ b/domain/src/main/java/mihon/domain/upcoming/interactor/GetUpcomingManga.kt @@ -8,13 +8,11 @@ import tachiyomi.domain.manga.repository.MangaRepository class GetUpcomingManga( private val mangaRepository: MangaRepository, ) { + private val includedStatuses = + setOf( + SManga.ONGOING.toLong(), + SManga.PUBLISHING_FINISHED.toLong(), + ) - private val includedStatuses = setOf( - SManga.ONGOING.toLong(), - SManga.PUBLISHING_FINISHED.toLong(), - ) - - suspend fun subscribe(): Flow> { - return mangaRepository.getUpcomingManga(includedStatuses) - } + suspend fun subscribe(): Flow> = mangaRepository.getUpcomingManga(includedStatuses) } diff --git a/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt b/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt index 41f29392d1..b550013998 100644 --- a/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt @@ -6,7 +6,6 @@ import tachiyomi.core.common.preference.PreferenceStore class BackupPreferences( private val preferenceStore: PreferenceStore, ) { - fun backupInterval() = preferenceStore.getInt("backup_interval", 12) fun lastAutoBackupTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_auto_backup_timestamp"), 0L) diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/CreateCategoryWithName.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/CreateCategoryWithName.kt index b6b15b83ed..aa2db5f3f6 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/CreateCategoryWithName.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/CreateCategoryWithName.kt @@ -11,34 +11,38 @@ class CreateCategoryWithName( private val categoryRepository: CategoryRepository, private val preferences: LibraryPreferences, ) { - private val initialFlags: Long get() { val sort = preferences.sortingMode().get() return sort.type.flag or sort.direction.flag } - suspend fun await(name: String): Result = withNonCancellableContext { - val categories = categoryRepository.getAll() - val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0 - val newCategory = Category( - id = 0, - name = name, - order = nextOrder, - flags = initialFlags, - ) + suspend fun await(name: String): Result = + withNonCancellableContext { + val categories = categoryRepository.getAll() + val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0 + val newCategory = + Category( + id = 0, + name = name, + order = nextOrder, + flags = initialFlags, + ) - try { - categoryRepository.insert(newCategory) - Result.Success - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - Result.InternalError(e) + try { + categoryRepository.insert(newCategory) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + Result.InternalError(e) + } } - } sealed interface Result { data object Success : Result - data class InternalError(val error: Throwable) : Result + + data class InternalError( + val error: Throwable, + ) : Result } } diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/DeleteCategory.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/DeleteCategory.kt index bf26d959fe..88f00b998c 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/DeleteCategory.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/DeleteCategory.kt @@ -9,34 +9,38 @@ import tachiyomi.domain.category.repository.CategoryRepository class DeleteCategory( private val categoryRepository: CategoryRepository, ) { + suspend fun await(categoryId: Long) = + withNonCancellableContext { + try { + categoryRepository.delete(categoryId) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + return@withNonCancellableContext Result.InternalError(e) + } - suspend fun await(categoryId: Long) = withNonCancellableContext { - try { - categoryRepository.delete(categoryId) - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - return@withNonCancellableContext Result.InternalError(e) - } + val categories = categoryRepository.getAll() + val updates = + categories.mapIndexed { index, category -> + CategoryUpdate( + id = category.id, + order = index.toLong(), + ) + } - val categories = categoryRepository.getAll() - val updates = categories.mapIndexed { index, category -> - CategoryUpdate( - id = category.id, - order = index.toLong(), - ) + try { + categoryRepository.updatePartial(updates) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + Result.InternalError(e) + } } - try { - categoryRepository.updatePartial(updates) - Result.Success - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - Result.InternalError(e) - } - } - sealed interface Result { data object Success : Result - data class InternalError(val error: Throwable) : Result + + data class InternalError( + val error: Throwable, + ) : Result } } diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/GetCategories.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/GetCategories.kt index 1d05ce611f..1afe503232 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/GetCategories.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/GetCategories.kt @@ -7,20 +7,11 @@ import tachiyomi.domain.category.repository.CategoryRepository class GetCategories( private val categoryRepository: CategoryRepository, ) { + fun subscribe(): Flow> = categoryRepository.getAllAsFlow() - fun subscribe(): Flow> { - return categoryRepository.getAllAsFlow() - } + fun subscribe(mangaId: Long): Flow> = categoryRepository.getCategoriesByMangaIdAsFlow(mangaId) - fun subscribe(mangaId: Long): Flow> { - return categoryRepository.getCategoriesByMangaIdAsFlow(mangaId) - } + suspend fun await(): List = categoryRepository.getAll() - suspend fun await(): List { - return categoryRepository.getAll() - } - - suspend fun await(mangaId: Long): List { - return categoryRepository.getCategoriesByMangaId(mangaId) - } + suspend fun await(mangaId: Long): List = categoryRepository.getCategoriesByMangaId(mangaId) } diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/RenameCategory.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/RenameCategory.kt index 59c4c193e5..f5cc5bf82c 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/RenameCategory.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/RenameCategory.kt @@ -10,12 +10,15 @@ import tachiyomi.domain.category.repository.CategoryRepository class RenameCategory( private val categoryRepository: CategoryRepository, ) { - - suspend fun await(categoryId: Long, name: String) = withNonCancellableContext { - val update = CategoryUpdate( - id = categoryId, - name = name, - ) + suspend fun await( + categoryId: Long, + name: String, + ) = withNonCancellableContext { + val update = + CategoryUpdate( + id = categoryId, + name = name, + ) try { categoryRepository.updatePartial(update) @@ -26,10 +29,16 @@ class RenameCategory( } } - suspend fun await(category: Category, name: String) = await(category.id, name) + suspend fun await( + category: Category, + name: String, + ) = await(category.id, name) sealed interface Result { data object Success : Result - data class InternalError(val error: Throwable) : Result + + data class InternalError( + val error: Throwable, + ) : Result } } diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt index a648248017..d1d38bbb77 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt @@ -13,38 +13,44 @@ import java.util.Collections class ReorderCategory( private val categoryRepository: CategoryRepository, ) { - private val mutex = Mutex() suspend fun moveUp(category: Category): Result = await(category, MoveTo.UP) suspend fun moveDown(category: Category): Result = await(category, MoveTo.DOWN) - private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext { + private suspend fun await( + category: Category, + moveTo: MoveTo, + ) = withNonCancellableContext { mutex.withLock { - val categories = categoryRepository.getAll() - .filterNot(Category::isSystemCategory) - .toMutableList() + val categories = + categoryRepository + .getAll() + .filterNot(Category::isSystemCategory) + .toMutableList() val currentIndex = categories.indexOfFirst { it.id == category.id } if (currentIndex == -1) { return@withNonCancellableContext Result.Unchanged } - val newPosition = when (moveTo) { - MoveTo.UP -> currentIndex - 1 - MoveTo.DOWN -> currentIndex + 1 - }.toInt() + val newPosition = + when (moveTo) { + MoveTo.UP -> currentIndex - 1 + MoveTo.DOWN -> currentIndex + 1 + }.toInt() try { Collections.swap(categories, currentIndex, newPosition) - val updates = categories.mapIndexed { index, category -> - CategoryUpdate( - id = category.id, - order = index.toLong(), - ) - } + val updates = + categories.mapIndexed { index, category -> + CategoryUpdate( + id = category.id, + order = index.toLong(), + ) + } categoryRepository.updatePartial(updates) Result.Success @@ -55,31 +61,38 @@ class ReorderCategory( } } - suspend fun sortAlphabetically() = withNonCancellableContext { - mutex.withLock { - val updates = categoryRepository.getAll() - .sortedBy { category -> category.name } - .mapIndexed { index, category -> - CategoryUpdate( - id = category.id, - order = index.toLong(), - ) - } + suspend fun sortAlphabetically() = + withNonCancellableContext { + mutex.withLock { + val updates = + categoryRepository + .getAll() + .sortedBy { category -> category.name } + .mapIndexed { index, category -> + CategoryUpdate( + id = category.id, + order = index.toLong(), + ) + } - try { - categoryRepository.updatePartial(updates) - Result.Success - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - Result.InternalError(e) + try { + categoryRepository.updatePartial(updates) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + Result.InternalError(e) + } } } - } sealed interface Result { data object Success : Result + data object Unchanged : Result - data class InternalError(val error: Throwable) : Result + + data class InternalError( + val error: Throwable, + ) : Result } private enum class MoveTo { diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/ResetCategoryFlags.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/ResetCategoryFlags.kt index 4032206c3a..aa48f2601e 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/ResetCategoryFlags.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/ResetCategoryFlags.kt @@ -8,7 +8,6 @@ class ResetCategoryFlags( private val preferences: LibraryPreferences, private val categoryRepository: CategoryRepository, ) { - suspend fun await() { val sort = preferences.sortingMode().get() categoryRepository.updateAllFlags(sort.type + sort.direction) diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/SetDisplayMode.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/SetDisplayMode.kt index 32e23148f0..77a3ea8569 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/SetDisplayMode.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/SetDisplayMode.kt @@ -6,7 +6,6 @@ import tachiyomi.domain.library.service.LibraryPreferences class SetDisplayMode( private val preferences: LibraryPreferences, ) { - fun await(display: LibraryDisplayMode) { preferences.displayMode().set(display) } diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/SetMangaCategories.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/SetMangaCategories.kt index 604e3f0d1e..6a5f9894f1 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/SetMangaCategories.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/SetMangaCategories.kt @@ -7,8 +7,10 @@ import tachiyomi.domain.manga.repository.MangaRepository class SetMangaCategories( private val mangaRepository: MangaRepository, ) { - - suspend fun await(mangaId: Long, categoryIds: List) { + suspend fun await( + mangaId: Long, + categoryIds: List, + ) { try { mangaRepository.setMangaCategories(mangaId, categoryIds) } catch (e: Exception) { diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/SetSortModeForCategory.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/SetSortModeForCategory.kt index e514e08988..a601979031 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/SetSortModeForCategory.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/SetSortModeForCategory.kt @@ -11,8 +11,11 @@ class SetSortModeForCategory( private val preferences: LibraryPreferences, private val categoryRepository: CategoryRepository, ) { - - suspend fun await(categoryId: Long?, type: LibrarySort.Type, direction: LibrarySort.Direction) { + suspend fun await( + categoryId: Long?, + type: LibrarySort.Type, + direction: LibrarySort.Direction, + ) { val category = categoryId?.let { categoryRepository.get(it) } val flags = (category?.flags ?: 0) + type + direction if (category != null && preferences.categorizedDisplaySettings().get()) { diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/UpdateCategory.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/UpdateCategory.kt index f3f601ceb0..13006554bb 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/UpdateCategory.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/UpdateCategory.kt @@ -7,18 +7,21 @@ import tachiyomi.domain.category.repository.CategoryRepository class UpdateCategory( private val categoryRepository: CategoryRepository, ) { - - suspend fun await(payload: CategoryUpdate): Result = withNonCancellableContext { - try { - categoryRepository.updatePartial(payload) - Result.Success - } catch (e: Exception) { - Result.Error(e) + suspend fun await(payload: CategoryUpdate): Result = + withNonCancellableContext { + try { + categoryRepository.updatePartial(payload) + Result.Success + } catch (e: Exception) { + Result.Error(e) + } } - } sealed interface Result { data object Success : Result - data class Error(val error: Exception) : Result + + data class Error( + val error: Exception, + ) : Result } } diff --git a/domain/src/main/java/tachiyomi/domain/category/model/Category.kt b/domain/src/main/java/tachiyomi/domain/category/model/Category.kt index ea901ce80f..f4f82c810b 100644 --- a/domain/src/main/java/tachiyomi/domain/category/model/Category.kt +++ b/domain/src/main/java/tachiyomi/domain/category/model/Category.kt @@ -8,7 +8,6 @@ data class Category( val order: Long, val flags: Long, ) : Serializable { - val isSystemCategory: Boolean = id == UNCATEGORIZED_ID companion object { diff --git a/domain/src/main/java/tachiyomi/domain/category/repository/CategoryRepository.kt b/domain/src/main/java/tachiyomi/domain/category/repository/CategoryRepository.kt index e2f8871df7..24d8f3ba42 100644 --- a/domain/src/main/java/tachiyomi/domain/category/repository/CategoryRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/category/repository/CategoryRepository.kt @@ -5,7 +5,6 @@ import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.CategoryUpdate interface CategoryRepository { - suspend fun get(id: Long): Category? suspend fun getAll(): List diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapter.kt index 4b4b9d94d1..284aa9bada 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapter.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapter.kt @@ -8,22 +8,22 @@ import tachiyomi.domain.chapter.repository.ChapterRepository class GetChapter( private val chapterRepository: ChapterRepository, ) { - - suspend fun await(id: Long): Chapter? { - return try { + suspend fun await(id: Long): Chapter? = + try { chapterRepository.getChapterById(id) } catch (e: Exception) { logcat(LogPriority.ERROR, e) null } - } - suspend fun await(url: String, mangaId: Long): Chapter? { - return try { + suspend fun await( + url: String, + mangaId: Long, + ): Chapter? = + try { chapterRepository.getChapterByUrlAndMangaId(url, mangaId) } catch (e: Exception) { logcat(LogPriority.ERROR, e) null } - } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByUrlAndMangaId.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByUrlAndMangaId.kt index f0399d17f7..fac35e997f 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByUrlAndMangaId.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByUrlAndMangaId.kt @@ -6,12 +6,13 @@ import tachiyomi.domain.chapter.repository.ChapterRepository class GetChapterByUrlAndMangaId( private val chapterRepository: ChapterRepository, ) { - - suspend fun await(url: String, sourceId: Long): Chapter? { - return try { + suspend fun await( + url: String, + sourceId: Long, + ): Chapter? = + try { chapterRepository.getChapterByUrlAndMangaId(url, sourceId) } catch (e: Exception) { null } - } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChaptersByMangaId.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChaptersByMangaId.kt index 1dee7770e1..9737e009ad 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChaptersByMangaId.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChaptersByMangaId.kt @@ -8,13 +8,14 @@ import tachiyomi.domain.chapter.repository.ChapterRepository class GetChaptersByMangaId( private val chapterRepository: ChapterRepository, ) { - - suspend fun await(mangaId: Long, applyScanlatorFilter: Boolean = false): List { - return try { + suspend fun await( + mangaId: Long, + applyScanlatorFilter: Boolean = false, + ): List = + try { chapterRepository.getChapterByMangaId(mangaId, applyScanlatorFilter) } catch (e: Exception) { logcat(LogPriority.ERROR, e) emptyList() } - } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/SetMangaDefaultChapterFlags.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/SetMangaDefaultChapterFlags.kt index 4c968b39aa..3c00eaff0f 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/interactor/SetMangaDefaultChapterFlags.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/SetMangaDefaultChapterFlags.kt @@ -11,7 +11,6 @@ class SetMangaDefaultChapterFlags( private val setMangaChapterFlags: SetMangaChapterFlags, private val getFavorites: GetFavorites, ) { - suspend fun await(manga: Manga) { withNonCancellableContext { with(libraryPreferences) { diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/ShouldUpdateDbChapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/ShouldUpdateDbChapter.kt index 5e5ac0fe68..0e01459d9c 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/interactor/ShouldUpdateDbChapter.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/ShouldUpdateDbChapter.kt @@ -3,11 +3,13 @@ package tachiyomi.domain.chapter.interactor import tachiyomi.domain.chapter.model.Chapter class ShouldUpdateDbChapter { - - fun await(dbChapter: Chapter, sourceChapter: Chapter): Boolean { - return dbChapter.scanlator != sourceChapter.scanlator || dbChapter.name != sourceChapter.name || + fun await( + dbChapter: Chapter, + sourceChapter: Chapter, + ): Boolean = + dbChapter.scanlator != sourceChapter.scanlator || + dbChapter.name != sourceChapter.name || dbChapter.dateUpload != sourceChapter.dateUpload || dbChapter.chapterNumber != sourceChapter.chapterNumber || dbChapter.sourceOrder != sourceChapter.sourceOrder - } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/UpdateChapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/UpdateChapter.kt index 3daaf90102..a82befa507 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/interactor/UpdateChapter.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/UpdateChapter.kt @@ -8,7 +8,6 @@ import tachiyomi.domain.chapter.repository.ChapterRepository class UpdateChapter( private val chapterRepository: ChapterRepository, ) { - suspend fun await(chapterUpdate: ChapterUpdate) { try { chapterRepository.update(chapterUpdate) diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt index f993e0256e..397174e47b 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt @@ -19,32 +19,32 @@ data class Chapter( val isRecognizedNumber: Boolean get() = chapterNumber >= 0f - fun copyFrom(other: Chapter): Chapter { - return copy( + fun copyFrom(other: Chapter): Chapter = + copy( name = other.name, url = other.url, dateUpload = other.dateUpload, chapterNumber = other.chapterNumber, scanlator = other.scanlator?.ifBlank { null }, ) - } companion object { - fun create() = Chapter( - id = -1, - mangaId = -1, - read = false, - bookmark = false, - lastPageRead = 0, - dateFetch = 0, - sourceOrder = 0, - url = "", - name = "", - dateUpload = -1, - chapterNumber = -1.0, - scanlator = null, - lastModifiedAt = 0, - version = 1, - ) + fun create() = + Chapter( + id = -1, + mangaId = -1, + read = false, + bookmark = false, + lastPageRead = 0, + dateFetch = 0, + sourceOrder = 0, + url = "", + name = "", + dateUpload = -1, + chapterNumber = -1.0, + scanlator = null, + lastModifiedAt = 0, + version = 1, + ) } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt index 5a9193dc68..7a95ed85c5 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt @@ -16,8 +16,8 @@ data class ChapterUpdate( val version: Long? = null, ) -fun Chapter.toChapterUpdate(): ChapterUpdate { - return ChapterUpdate( +fun Chapter.toChapterUpdate(): ChapterUpdate = + ChapterUpdate( id, mangaId, read, @@ -32,4 +32,3 @@ fun Chapter.toChapterUpdate(): ChapterUpdate { scanlator, version, ) -} diff --git a/domain/src/main/java/tachiyomi/domain/chapter/repository/ChapterRepository.kt b/domain/src/main/java/tachiyomi/domain/chapter/repository/ChapterRepository.kt index ae4af5106f..0a77489e88 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/repository/ChapterRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/repository/ChapterRepository.kt @@ -5,7 +5,6 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.ChapterUpdate interface ChapterRepository { - suspend fun addAll(chapters: List): List suspend fun update(chapterUpdate: ChapterUpdate) @@ -14,7 +13,10 @@ interface ChapterRepository { suspend fun removeChaptersWithIds(chapterIds: List) - suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean = false): List + suspend fun getChapterByMangaId( + mangaId: Long, + applyScanlatorFilter: Boolean = false, + ): List suspend fun getScanlatorsByMangaId(mangaId: Long): List @@ -24,7 +26,13 @@ interface ChapterRepository { suspend fun getChapterById(id: Long): Chapter? - suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean = false): Flow> + suspend fun getChapterByMangaIdAsFlow( + mangaId: Long, + applyScanlatorFilter: Boolean = false, + ): Flow> - suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? + suspend fun getChapterByUrlAndMangaId( + url: String, + mangaId: Long, + ): Chapter? } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt index 3190e0456a..6f30e5f7f3 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt @@ -4,7 +4,6 @@ package tachiyomi.domain.chapter.service * -R> = regex conversion. */ object ChapterRecognition { - private const val NUMBER_PATTERN = """([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""" /** @@ -41,14 +40,17 @@ object ChapterRecognition { } // Get chapter title with lower case - val cleanChapterName = chapterName.lowercase() - // Remove manga title from chapter title. - .replace(mangaTitle.lowercase(), "").trim() - // Remove comma's or hyphens. - .replace(',', '.') - .replace('-', '.') - // Remove unwanted white spaces. - .replace(unwantedWhiteSpace, "") + val cleanChapterName = + chapterName + .lowercase() + // Remove manga title from chapter title. + .replace(mangaTitle.lowercase(), "") + .trim() + // Remove comma's or hyphens. + .replace(',', '.') + .replace('-', '.') + // Remove unwanted white spaces. + .replace(unwantedWhiteSpace, "") val numberMatch = number.findAll(cleanChapterName) @@ -77,15 +79,14 @@ object ChapterRecognition { * @param match result of regex * @return chapter number if found else null */ - private fun getChapterNumberFromMatch(match: MatchResult): Double { - return match.let { + private fun getChapterNumberFromMatch(match: MatchResult): Double = + match.let { val initial = it.groups[1]?.value?.toDouble()!! val subChapterDecimal = it.groups[2]?.value val subChapterAlpha = it.groups[3]?.value val addition = checkForDecimal(subChapterDecimal, subChapterAlpha) initial.plus(addition) } - } /** * Check for decimal in received strings @@ -93,7 +94,10 @@ object ChapterRecognition { * @param alpha alpha value of regex * @return decimal/alpha float value */ - private fun checkForDecimal(decimal: String?, alpha: String?): Double { + private fun checkForDecimal( + decimal: String?, + alpha: String?, + ): Double { if (!decimal.isNullOrEmpty()) { return decimal.toDouble() } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt index 795805555e..6d940d135e 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt @@ -10,24 +10,27 @@ fun getChapterSort( ): ( Chapter, Chapter, -) -> Int { - return when (manga.sorting) { - Manga.CHAPTER_SORTING_SOURCE -> when (sortDescending) { - true -> { c1, c2 -> c1.sourceOrder.compareTo(c2.sourceOrder) } - false -> { c1, c2 -> c2.sourceOrder.compareTo(c1.sourceOrder) } - } - Manga.CHAPTER_SORTING_NUMBER -> when (sortDescending) { - true -> { c1, c2 -> c2.chapterNumber.compareTo(c1.chapterNumber) } - false -> { c1, c2 -> c1.chapterNumber.compareTo(c2.chapterNumber) } - } - Manga.CHAPTER_SORTING_UPLOAD_DATE -> when (sortDescending) { - true -> { c1, c2 -> c2.dateUpload.compareTo(c1.dateUpload) } - false -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) } - } - Manga.CHAPTER_SORTING_ALPHABET -> when (sortDescending) { - true -> { c1, c2 -> c2.name.compareToWithCollator(c1.name) } - false -> { c1, c2 -> c1.name.compareToWithCollator(c2.name) } - } +) -> Int = + when (manga.sorting) { + Manga.CHAPTER_SORTING_SOURCE -> + when (sortDescending) { + true -> { c1, c2 -> c1.sourceOrder.compareTo(c2.sourceOrder) } + false -> { c1, c2 -> c2.sourceOrder.compareTo(c1.sourceOrder) } + } + Manga.CHAPTER_SORTING_NUMBER -> + when (sortDescending) { + true -> { c1, c2 -> c2.chapterNumber.compareTo(c1.chapterNumber) } + false -> { c1, c2 -> c1.chapterNumber.compareTo(c2.chapterNumber) } + } + Manga.CHAPTER_SORTING_UPLOAD_DATE -> + when (sortDescending) { + true -> { c1, c2 -> c2.dateUpload.compareTo(c1.dateUpload) } + false -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) } + } + Manga.CHAPTER_SORTING_ALPHABET -> + when (sortDescending) { + true -> { c1, c2 -> c2.name.compareToWithCollator(c1.name) } + false -> { c1, c2 -> c1.name.compareToWithCollator(c2.name) } + } else -> throw NotImplementedError("Invalid chapter sorting method: ${manga.sorting}") } -} diff --git a/domain/src/main/java/tachiyomi/domain/chapter/service/MissingChapters.kt b/domain/src/main/java/tachiyomi/domain/chapter/service/MissingChapters.kt index 55cce541b7..ca887bbbcc 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/service/MissingChapters.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/service/MissingChapters.kt @@ -8,14 +8,15 @@ fun List.missingChaptersCount(): Int { return 0 } - val chapters = this - // Ignore unknown chapter numbers - .filterNot { it == -1.0 } - // Convert to integers, as we cannot check if 16.5 is missing - .map(Double::toInt) - // Only keep unique chapters so that -1 or 16 are not counted multiple times - .distinct() - .sorted() + val chapters = + this + // Ignore unknown chapter numbers + .filterNot { it == -1.0 } + // Convert to integers, as we cannot check if 16.5 is missing + .map(Double::toInt) + // Only keep unique chapters so that -1 or 16 are not counted multiple times + .distinct() + .sorted() if (chapters.isEmpty()) { return 0 @@ -37,13 +38,19 @@ fun List.missingChaptersCount(): Int { return missingChaptersCount } -fun calculateChapterGap(higherChapter: Chapter?, lowerChapter: Chapter?): Int { +fun calculateChapterGap( + higherChapter: Chapter?, + lowerChapter: Chapter?, +): Int { if (higherChapter == null || lowerChapter == null) return 0 if (!higherChapter.isRecognizedNumber || !lowerChapter.isRecognizedNumber) return 0 return calculateChapterGap(higherChapter.chapterNumber, lowerChapter.chapterNumber) } -fun calculateChapterGap(higherChapterNumber: Double, lowerChapterNumber: Double): Int { +fun calculateChapterGap( + higherChapterNumber: Double, + lowerChapterNumber: Double, +): Int { if (higherChapterNumber < 0.0 || lowerChapterNumber < 0.0) return 0 return floor(higherChapterNumber).toInt() - floor(lowerChapterNumber).toInt() - 1 } 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 a0625e5a7f..26441b9e68 100644 --- a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt @@ -5,11 +5,11 @@ import tachiyomi.core.common.preference.PreferenceStore class DownloadPreferences( private val preferenceStore: PreferenceStore, ) { - - fun downloadOnlyOverWifi() = preferenceStore.getBoolean( - "pref_download_only_over_wifi_key", - true, - ) + fun downloadOnlyOverWifi() = + preferenceStore.getBoolean( + "pref_download_only_over_wifi_key", + true, + ) fun saveChaptersAsCBZ() = preferenceStore.getBoolean("save_chapter_as_cbz", true) @@ -19,27 +19,31 @@ class DownloadPreferences( fun removeAfterReadSlots() = preferenceStore.getInt("remove_after_read_slots", -1) - fun removeAfterMarkedAsRead() = preferenceStore.getBoolean( - "pref_remove_after_marked_as_read_key", - false, - ) + fun removeAfterMarkedAsRead() = + preferenceStore.getBoolean( + "pref_remove_after_marked_as_read_key", + false, + ) fun removeBookmarkedChapters() = preferenceStore.getBoolean("pref_remove_bookmarked", false) - fun removeExcludeCategories() = preferenceStore.getStringSet( - "remove_exclude_categories", - emptySet(), - ) + fun removeExcludeCategories() = + preferenceStore.getStringSet( + "remove_exclude_categories", + emptySet(), + ) fun downloadNewChapters() = preferenceStore.getBoolean("download_new", false) - fun downloadNewChapterCategories() = preferenceStore.getStringSet( - "download_new_categories", - emptySet(), - ) - - fun downloadNewChapterCategoriesExclude() = preferenceStore.getStringSet( - "download_new_categories_exclude", - emptySet(), - ) + fun downloadNewChapterCategories() = + preferenceStore.getStringSet( + "download_new_categories", + emptySet(), + ) + + fun downloadNewChapterCategoriesExclude() = + preferenceStore.getStringSet( + "download_new_categories_exclude", + emptySet(), + ) } diff --git a/domain/src/main/java/tachiyomi/domain/history/interactor/GetHistory.kt b/domain/src/main/java/tachiyomi/domain/history/interactor/GetHistory.kt index 8a40096f86..a36d435dcb 100644 --- a/domain/src/main/java/tachiyomi/domain/history/interactor/GetHistory.kt +++ b/domain/src/main/java/tachiyomi/domain/history/interactor/GetHistory.kt @@ -8,12 +8,7 @@ import tachiyomi.domain.history.repository.HistoryRepository class GetHistory( private val repository: HistoryRepository, ) { + suspend fun await(mangaId: Long): List = repository.getHistoryByMangaId(mangaId) - suspend fun await(mangaId: Long): List { - return repository.getHistoryByMangaId(mangaId) - } - - fun subscribe(query: String): Flow> { - return repository.getHistory(query) - } + fun subscribe(query: String): Flow> = repository.getHistory(query) } diff --git a/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt b/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt index 2e7fefc96d..20d62c63e5 100644 --- a/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt +++ b/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt @@ -12,16 +12,20 @@ class GetNextChapters( private val getManga: GetManga, private val historyRepository: HistoryRepository, ) { - suspend fun await(onlyUnread: Boolean = true): List { val history = historyRepository.getLastHistory() ?: return emptyList() return await(history.mangaId, history.chapterId, onlyUnread) } - suspend fun await(mangaId: Long, onlyUnread: Boolean = true): List { + suspend fun await( + mangaId: Long, + onlyUnread: Boolean = true, + ): List { val manga = getManga.await(mangaId) ?: return emptyList() - val chapters = getChaptersByMangaId.await(mangaId, applyScanlatorFilter = true) - .sortedWith(getChapterSort(manga, sortDescending = false)) + val chapters = + getChaptersByMangaId + .await(mangaId, applyScanlatorFilter = true) + .sortedWith(getChapterSort(manga, sortDescending = false)) return if (onlyUnread) { chapters.filterNot { it.read } diff --git a/domain/src/main/java/tachiyomi/domain/history/interactor/GetTotalReadDuration.kt b/domain/src/main/java/tachiyomi/domain/history/interactor/GetTotalReadDuration.kt index 9bde36520e..f49586bfd0 100644 --- a/domain/src/main/java/tachiyomi/domain/history/interactor/GetTotalReadDuration.kt +++ b/domain/src/main/java/tachiyomi/domain/history/interactor/GetTotalReadDuration.kt @@ -5,8 +5,5 @@ import tachiyomi.domain.history.repository.HistoryRepository class GetTotalReadDuration( private val repository: HistoryRepository, ) { - - suspend fun await(): Long { - return repository.getTotalReadDuration() - } + suspend fun await(): Long = repository.getTotalReadDuration() } diff --git a/domain/src/main/java/tachiyomi/domain/history/interactor/RemoveHistory.kt b/domain/src/main/java/tachiyomi/domain/history/interactor/RemoveHistory.kt index c6c8398ede..6a84534ebc 100644 --- a/domain/src/main/java/tachiyomi/domain/history/interactor/RemoveHistory.kt +++ b/domain/src/main/java/tachiyomi/domain/history/interactor/RemoveHistory.kt @@ -6,10 +6,7 @@ import tachiyomi.domain.history.repository.HistoryRepository class RemoveHistory( private val repository: HistoryRepository, ) { - - suspend fun awaitAll(): Boolean { - return repository.deleteAllHistory() - } + suspend fun awaitAll(): Boolean = repository.deleteAllHistory() suspend fun await(history: HistoryWithRelations) { repository.resetHistory(history.id) diff --git a/domain/src/main/java/tachiyomi/domain/history/interactor/UpsertHistory.kt b/domain/src/main/java/tachiyomi/domain/history/interactor/UpsertHistory.kt index 86b041b77c..b3c640e2e5 100644 --- a/domain/src/main/java/tachiyomi/domain/history/interactor/UpsertHistory.kt +++ b/domain/src/main/java/tachiyomi/domain/history/interactor/UpsertHistory.kt @@ -6,7 +6,6 @@ import tachiyomi.domain.history.repository.HistoryRepository class UpsertHistory( private val historyRepository: HistoryRepository, ) { - suspend fun await(historyUpdate: HistoryUpdate) { historyRepository.upsertHistory(historyUpdate) } diff --git a/domain/src/main/java/tachiyomi/domain/history/model/History.kt b/domain/src/main/java/tachiyomi/domain/history/model/History.kt index 41b58b6378..7a198d5b4e 100644 --- a/domain/src/main/java/tachiyomi/domain/history/model/History.kt +++ b/domain/src/main/java/tachiyomi/domain/history/model/History.kt @@ -9,11 +9,12 @@ data class History( val readDuration: Long, ) { companion object { - fun create() = History( - id = -1L, - chapterId = -1L, - readAt = null, - readDuration = -1L, - ) + fun create() = + History( + id = -1L, + chapterId = -1L, + readAt = null, + readDuration = -1L, + ) } } diff --git a/domain/src/main/java/tachiyomi/domain/history/repository/HistoryRepository.kt b/domain/src/main/java/tachiyomi/domain/history/repository/HistoryRepository.kt index 410bfe951c..e61fc7dc9f 100644 --- a/domain/src/main/java/tachiyomi/domain/history/repository/HistoryRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/history/repository/HistoryRepository.kt @@ -6,7 +6,6 @@ import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.history.model.HistoryWithRelations interface HistoryRepository { - fun getHistory(query: String): Flow> suspend fun getLastHistory(): HistoryWithRelations? diff --git a/domain/src/main/java/tachiyomi/domain/library/model/Flag.kt b/domain/src/main/java/tachiyomi/domain/library/model/Flag.kt index 655d19bf9d..337a1a16c2 100644 --- a/domain/src/main/java/tachiyomi/domain/library/model/Flag.kt +++ b/domain/src/main/java/tachiyomi/domain/library/model/Flag.kt @@ -8,28 +8,27 @@ interface Mask { val mask: Long } -interface FlagWithMask : Flag, Mask +interface FlagWithMask : + Flag, + Mask -operator fun Long.contains(other: Flag): Boolean { - return if (other is Mask) { +operator fun Long.contains(other: Flag): Boolean = + if (other is Mask) { other.flag == this and other.mask } else { other.flag == this } -} -operator fun Long.plus(other: Flag): Long { - return if (other is Mask) { +operator fun Long.plus(other: Flag): Long = + if (other is Mask) { this and other.mask.inv() or (other.flag and other.mask) } else { this or other.flag } -} -operator fun Flag.plus(other: Flag): Long { - return if (other is Mask) { +operator fun Flag.plus(other: Flag): Long = + if (other is Mask) { this.flag and other.mask.inv() or (other.flag and other.mask) } else { this.flag or other.flag } -} diff --git a/domain/src/main/java/tachiyomi/domain/library/model/LibraryDisplayMode.kt b/domain/src/main/java/tachiyomi/domain/library/model/LibraryDisplayMode.kt index bc2fde0fbf..8b7e905d06 100644 --- a/domain/src/main/java/tachiyomi/domain/library/model/LibraryDisplayMode.kt +++ b/domain/src/main/java/tachiyomi/domain/library/model/LibraryDisplayMode.kt @@ -1,43 +1,39 @@ package tachiyomi.domain.library.model sealed interface LibraryDisplayMode { - data object CompactGrid : LibraryDisplayMode + data object ComfortableGrid : LibraryDisplayMode + data object List : LibraryDisplayMode + data object CoverOnlyGrid : LibraryDisplayMode object Serializer { - fun deserialize(serialized: String): LibraryDisplayMode { - return LibraryDisplayMode.deserialize(serialized) - } + fun deserialize(serialized: String): LibraryDisplayMode = LibraryDisplayMode.deserialize(serialized) - fun serialize(value: LibraryDisplayMode): String { - return value.serialize() - } + fun serialize(value: LibraryDisplayMode): String = value.serialize() } companion object { val values by lazy { setOf(CompactGrid, ComfortableGrid, List, CoverOnlyGrid) } val default = CompactGrid - fun deserialize(serialized: String): LibraryDisplayMode { - return when (serialized) { + fun deserialize(serialized: String): LibraryDisplayMode = + when (serialized) { "COMFORTABLE_GRID" -> ComfortableGrid "COMPACT_GRID" -> CompactGrid "COVER_ONLY_GRID" -> CoverOnlyGrid "LIST" -> List else -> default } - } } - fun serialize(): String { - return when (this) { + fun serialize(): String = + when (this) { ComfortableGrid -> "COMFORTABLE_GRID" CompactGrid -> "COMPACT_GRID" CoverOnlyGrid -> "COVER_ONLY_GRID" List -> "LIST" } - } } diff --git a/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt b/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt index 6a89d4e526..ca63e145d3 100644 --- a/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt +++ b/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt @@ -6,7 +6,6 @@ data class LibrarySort( val type: Type, val direction: Direction, ) : FlagWithMask { - override val flag: Long get() = type + direction @@ -19,50 +18,52 @@ data class LibrarySort( sealed class Type( override val flag: Long, ) : FlagWithMask { - override val mask: Long = 0b00111100L data object Alphabetical : Type(0b00000000) + data object LastRead : Type(0b00000100) + data object LastUpdate : Type(0b00001000) + data object UnreadCount : Type(0b00001100) + data object TotalChapters : Type(0b00010000) + data object LatestChapter : Type(0b00010100) + data object ChapterFetchDate : Type(0b00011000) + data object DateAdded : Type(0b00011100) + data object TrackerMean : Type(0b000100000) companion object { - fun valueOf(flag: Long): Type { - return types.find { type -> type.flag == flag and type.mask } ?: default.type - } + fun valueOf(flag: Long): Type = types.find { type -> type.flag == flag and type.mask } ?: default.type } } sealed class Direction( override val flag: Long, ) : FlagWithMask { - override val mask: Long = 0b01000000L data object Ascending : Direction(0b01000000) + data object Descending : Direction(0b00000000) companion object { - fun valueOf(flag: Long): Direction { - return directions.find { direction -> direction.flag == flag and direction.mask } ?: default.direction - } + fun valueOf(flag: Long): Direction = + directions.find { direction -> + direction.flag == flag and direction.mask + } ?: default.direction } } object Serializer { - fun deserialize(serialized: String): LibrarySort { - return LibrarySort.deserialize(serialized) - } + fun deserialize(serialized: String): LibrarySort = LibrarySort.deserialize(serialized) - fun serialize(value: LibrarySort): String { - return value.serialize() - } + fun serialize(value: LibrarySort): String = value.serialize() } companion object { @@ -94,18 +95,19 @@ data class LibrarySort( if (serialized.isEmpty()) return default return try { val values = serialized.split(",") - val type = when (values[0]) { - "ALPHABETICAL" -> Type.Alphabetical - "LAST_READ" -> Type.LastRead - "LAST_MANGA_UPDATE" -> Type.LastUpdate - "UNREAD_COUNT" -> Type.UnreadCount - "TOTAL_CHAPTERS" -> Type.TotalChapters - "LATEST_CHAPTER" -> Type.LatestChapter - "CHAPTER_FETCH_DATE" -> Type.ChapterFetchDate - "DATE_ADDED" -> Type.DateAdded - "TRACKER_MEAN" -> Type.TrackerMean - else -> Type.Alphabetical - } + val type = + when (values[0]) { + "ALPHABETICAL" -> Type.Alphabetical + "LAST_READ" -> Type.LastRead + "LAST_MANGA_UPDATE" -> Type.LastUpdate + "UNREAD_COUNT" -> Type.UnreadCount + "TOTAL_CHAPTERS" -> Type.TotalChapters + "LATEST_CHAPTER" -> Type.LatestChapter + "CHAPTER_FETCH_DATE" -> Type.ChapterFetchDate + "DATE_ADDED" -> Type.DateAdded + "TRACKER_MEAN" -> Type.TrackerMean + else -> Type.Alphabetical + } val ascending = if (values[1] == "ASCENDING") Direction.Ascending else Direction.Descending LibrarySort(type, ascending) } catch (e: Exception) { @@ -115,17 +117,18 @@ data class LibrarySort( } fun serialize(): String { - val type = when (type) { - Type.Alphabetical -> "ALPHABETICAL" - Type.LastRead -> "LAST_READ" - Type.LastUpdate -> "LAST_MANGA_UPDATE" - Type.UnreadCount -> "UNREAD_COUNT" - Type.TotalChapters -> "TOTAL_CHAPTERS" - Type.LatestChapter -> "LATEST_CHAPTER" - Type.ChapterFetchDate -> "CHAPTER_FETCH_DATE" - Type.DateAdded -> "DATE_ADDED" - Type.TrackerMean -> "TRACKER_MEAN" - } + val type = + when (type) { + Type.Alphabetical -> "ALPHABETICAL" + Type.LastRead -> "LAST_READ" + Type.LastUpdate -> "LAST_MANGA_UPDATE" + Type.UnreadCount -> "UNREAD_COUNT" + Type.TotalChapters -> "TOTAL_CHAPTERS" + Type.LatestChapter -> "LATEST_CHAPTER" + Type.ChapterFetchDate -> "CHAPTER_FETCH_DATE" + Type.DateAdded -> "DATE_ADDED" + Type.TrackerMean -> "TRACKER_MEAN" + } val direction = if (direction == Direction.Ascending) "ASCENDING" else "DESCENDING" return "$type,$direction" } 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 437dc54bce..87b50aad26 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -11,84 +11,96 @@ import tachiyomi.domain.manga.model.Manga class LibraryPreferences( private val preferenceStore: PreferenceStore, ) { + fun displayMode() = + preferenceStore.getObject( + "pref_display_mode_library", + LibraryDisplayMode.default, + LibraryDisplayMode.Serializer::serialize, + LibraryDisplayMode.Serializer::deserialize, + ) - fun displayMode() = preferenceStore.getObject( - "pref_display_mode_library", - LibraryDisplayMode.default, - LibraryDisplayMode.Serializer::serialize, - LibraryDisplayMode.Serializer::deserialize, - ) - - fun sortingMode() = preferenceStore.getObject( - "library_sorting_mode", - LibrarySort.default, - LibrarySort.Serializer::serialize, - LibrarySort.Serializer::deserialize, - ) + fun sortingMode() = + preferenceStore.getObject( + "library_sorting_mode", + LibrarySort.default, + LibrarySort.Serializer::serialize, + LibrarySort.Serializer::deserialize, + ) fun portraitColumns() = preferenceStore.getInt("pref_library_columns_portrait_key", 0) fun landscapeColumns() = preferenceStore.getInt("pref_library_columns_landscape_key", 0) fun lastUpdatedTimestamp() = preferenceStore.getLong(Preference.appStateKey("library_update_last_timestamp"), 0L) + fun autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0) - fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet( - "library_update_restriction", - setOf( - DEVICE_ONLY_ON_WIFI, - ), - ) - fun autoUpdateMangaRestrictions() = preferenceStore.getStringSet( - "library_update_manga_restriction", - setOf( - MANGA_HAS_UNREAD, - MANGA_NON_COMPLETED, - MANGA_NON_READ, - MANGA_OUTSIDE_RELEASE_PERIOD, - ), - ) + fun autoUpdateDeviceRestrictions() = + preferenceStore.getStringSet( + "library_update_restriction", + setOf( + DEVICE_ONLY_ON_WIFI, + ), + ) + + fun autoUpdateMangaRestrictions() = + preferenceStore.getStringSet( + "library_update_manga_restriction", + setOf( + MANGA_HAS_UNREAD, + MANGA_NON_COMPLETED, + MANGA_NON_READ, + MANGA_OUTSIDE_RELEASE_PERIOD, + ), + ) fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false) - fun showContinueReadingButton() = preferenceStore.getBoolean( - "display_continue_reading_button", - false, - ) + fun showContinueReadingButton() = + preferenceStore.getBoolean( + "display_continue_reading_button", + false, + ) // region Filter - fun filterDownloaded() = preferenceStore.getEnum( - "pref_filter_library_downloaded_v2", - TriState.DISABLED, - ) + fun filterDownloaded() = + preferenceStore.getEnum( + "pref_filter_library_downloaded_v2", + TriState.DISABLED, + ) fun filterUnread() = preferenceStore.getEnum("pref_filter_library_unread_v2", TriState.DISABLED) - fun filterStarted() = preferenceStore.getEnum( - "pref_filter_library_started_v2", - TriState.DISABLED, - ) + fun filterStarted() = + preferenceStore.getEnum( + "pref_filter_library_started_v2", + TriState.DISABLED, + ) - fun filterBookmarked() = preferenceStore.getEnum( - "pref_filter_library_bookmarked_v2", - TriState.DISABLED, - ) + fun filterBookmarked() = + preferenceStore.getEnum( + "pref_filter_library_bookmarked_v2", + TriState.DISABLED, + ) - fun filterCompleted() = preferenceStore.getEnum( - "pref_filter_library_completed_v2", - TriState.DISABLED, - ) + fun filterCompleted() = + preferenceStore.getEnum( + "pref_filter_library_completed_v2", + TriState.DISABLED, + ) - fun filterIntervalCustom() = preferenceStore.getEnum( - "pref_filter_library_interval_custom", - TriState.DISABLED, - ) + fun filterIntervalCustom() = + preferenceStore.getEnum( + "pref_filter_library_interval_custom", + TriState.DISABLED, + ) - fun filterTracking(id: Int) = preferenceStore.getEnum( - "pref_filter_library_tracked_${id}_v2", - TriState.DISABLED, - ) + fun filterTracking(id: Int) = + preferenceStore.getEnum( + "pref_filter_library_tracked_${id}_v2", + TriState.DISABLED, + ) // endregion @@ -101,6 +113,7 @@ class LibraryPreferences( fun languageBadge() = preferenceStore.getBoolean("display_language_badge", false) fun newShowUpdatesCount() = preferenceStore.getBoolean("library_show_updates_count", true) + fun newUpdatesCount() = preferenceStore.getInt(Preference.appStateKey("library_unseen_updates_count"), 0) // endregion @@ -119,45 +132,52 @@ class LibraryPreferences( fun updateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet()) - fun updateCategoriesExclude() = preferenceStore.getStringSet( - "library_update_categories_exclude", - emptySet(), - ) + fun updateCategoriesExclude() = + preferenceStore.getStringSet( + "library_update_categories_exclude", + emptySet(), + ) // endregion // region Chapter - fun filterChapterByRead() = preferenceStore.getLong( - "default_chapter_filter_by_read", - Manga.SHOW_ALL, - ) + fun filterChapterByRead() = + preferenceStore.getLong( + "default_chapter_filter_by_read", + Manga.SHOW_ALL, + ) - fun filterChapterByDownloaded() = preferenceStore.getLong( - "default_chapter_filter_by_downloaded", - Manga.SHOW_ALL, - ) + fun filterChapterByDownloaded() = + preferenceStore.getLong( + "default_chapter_filter_by_downloaded", + Manga.SHOW_ALL, + ) - fun filterChapterByBookmarked() = preferenceStore.getLong( - "default_chapter_filter_by_bookmarked", - Manga.SHOW_ALL, - ) + fun filterChapterByBookmarked() = + preferenceStore.getLong( + "default_chapter_filter_by_bookmarked", + Manga.SHOW_ALL, + ) // and upload date - fun sortChapterBySourceOrNumber() = preferenceStore.getLong( - "default_chapter_sort_by_source_or_number", - Manga.CHAPTER_SORTING_SOURCE, - ) + fun sortChapterBySourceOrNumber() = + preferenceStore.getLong( + "default_chapter_sort_by_source_or_number", + Manga.CHAPTER_SORTING_SOURCE, + ) - fun displayChapterByNameOrNumber() = preferenceStore.getLong( - "default_chapter_display_by_name_or_number", - Manga.CHAPTER_DISPLAY_NAME, - ) + fun displayChapterByNameOrNumber() = + preferenceStore.getLong( + "default_chapter_display_by_name_or_number", + Manga.CHAPTER_DISPLAY_NAME, + ) - fun sortChapterByAscendingOrDescending() = preferenceStore.getLong( - "default_chapter_sort_by_ascending_or_descending", - Manga.CHAPTER_SORT_DESC, - ) + fun sortChapterByAscendingOrDescending() = + preferenceStore.getLong( + "default_chapter_sort_by_ascending_or_descending", + Manga.CHAPTER_SORT_DESC, + ) fun setChapterSettingsDefault(manga: Manga) { filterChapterByRead().set(manga.unreadFilterRaw) @@ -176,15 +196,17 @@ class LibraryPreferences( // region Swipe Actions - fun swipeToStartAction() = preferenceStore.getEnum( - "pref_chapter_swipe_end_action", - ChapterSwipeAction.ToggleBookmark, - ) + fun swipeToStartAction() = + preferenceStore.getEnum( + "pref_chapter_swipe_end_action", + ChapterSwipeAction.ToggleBookmark, + ) - fun swipeToEndAction() = preferenceStore.getEnum( - "pref_chapter_swipe_start_action", - ChapterSwipeAction.ToggleRead, - ) + fun swipeToEndAction() = + preferenceStore.getEnum( + "pref_chapter_swipe_start_action", + ChapterSwipeAction.ToggleRead, + ) // endregion diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt index f77f34a7ec..ec9403bbbe 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt @@ -13,21 +13,22 @@ import kotlin.math.absoluteValue class FetchInterval( private val getChaptersByMangaId: GetChaptersByMangaId, ) { - suspend fun toMangaUpdate( manga: Manga, dateTime: ZonedDateTime, window: Pair, ): MangaUpdate { - val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( - chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true), - zone = dateTime.zone, - ) - val currentWindow = if (window.first == 0L && window.second == 0L) { - getWindow(ZonedDateTime.now()) - } else { - window - } + val interval = + manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( + chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true), + zone = dateTime.zone, + ) + val currentWindow = + if (window.first == 0L && window.second == 0L) { + getWindow(ZonedDateTime.now()) + } else { + window + } val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow) return MangaUpdate(id = manga.id, nextUpdate = nextUpdate, fetchInterval = interval) @@ -40,48 +41,56 @@ class FetchInterval( return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1) } - internal fun calculateInterval(chapters: List, zone: ZoneId): Int { + internal fun calculateInterval( + chapters: List, + zone: ZoneId, + ): Int { val chapterWindow = if (chapters.size <= 8) 3 else 10 - val uploadDates = chapters.asSequence() - .filter { it.dateUpload > 0L } - .sortedByDescending { it.dateUpload } - .map { - ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone) - .toLocalDate() - .atStartOfDay() - } - .distinct() - .take(chapterWindow) - .toList() - - val fetchDates = chapters.asSequence() - .sortedByDescending { it.dateFetch } - .map { - ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone) - .toLocalDate() - .atStartOfDay() - } - .distinct() - .take(chapterWindow) - .toList() - - val interval = when { - // Enough upload date from source - uploadDates.size >= 3 -> { - val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS) - val uploadPeriod = uploadDates.indexOf(uploadDates.last()) - uploadDelta.floorDiv(uploadPeriod).toInt() + val uploadDates = + chapters + .asSequence() + .filter { it.dateUpload > 0L } + .sortedByDescending { it.dateUpload } + .map { + ZonedDateTime + .ofInstant(Instant.ofEpochMilli(it.dateUpload), zone) + .toLocalDate() + .atStartOfDay() + }.distinct() + .take(chapterWindow) + .toList() + + val fetchDates = + chapters + .asSequence() + .sortedByDescending { it.dateFetch } + .map { + ZonedDateTime + .ofInstant(Instant.ofEpochMilli(it.dateFetch), zone) + .toLocalDate() + .atStartOfDay() + }.distinct() + .take(chapterWindow) + .toList() + + val interval = + when { + // Enough upload date from source + uploadDates.size >= 3 -> { + val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS) + val uploadPeriod = uploadDates.indexOf(uploadDates.last()) + uploadDelta.floorDiv(uploadPeriod).toInt() + } + // Enough fetch date from client + fetchDates.size >= 3 -> { + val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS) + val uploadPeriod = fetchDates.indexOf(fetchDates.last()) + fetchDelta.floorDiv(uploadPeriod).toInt() + } + // Default to 7 days + else -> 7 } - // Enough fetch date from client - fetchDates.size >= 3 -> { - val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS) - val uploadPeriod = fetchDates.indexOf(fetchDates.last()) - fetchDelta.floorDiv(uploadPeriod).toInt() - } - // Default to 7 days - else -> 7 - } return interval.coerceIn(1, MAX_INTERVAL) } @@ -96,21 +105,27 @@ class FetchInterval( return manga.nextUpdate } - val latestDate = ZonedDateTime.ofInstant( - if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(), - dateTime.zone, - ) - .toLocalDate() - .atStartOfDay() + val latestDate = + ZonedDateTime + .ofInstant( + if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(), + dateTime.zone, + ).toLocalDate() + .atStartOfDay() val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt() - val cycle = timeSinceLatest.floorDiv( - interval.absoluteValue.takeIf { interval < 0 } - ?: increaseInterval(interval, timeSinceLatest, increaseWhenOver = 10), - ) + val cycle = + timeSinceLatest.floorDiv( + interval.absoluteValue.takeIf { interval < 0 } + ?: increaseInterval(interval, timeSinceLatest, increaseWhenOver = 10), + ) return latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000 } - private fun increaseInterval(delta: Int, timeSinceLatest: Int, increaseWhenOver: Int): Int { + private fun increaseInterval( + delta: Int, + timeSinceLatest: Int, + increaseWhenOver: Int, + ): Int { if (delta >= MAX_INTERVAL) return MAX_INTERVAL // double delta again if missed more than 9 check in new delta diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetDuplicateLibraryManga.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetDuplicateLibraryManga.kt index df5cec44a7..e7a43f8941 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetDuplicateLibraryManga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetDuplicateLibraryManga.kt @@ -6,8 +6,6 @@ import tachiyomi.domain.manga.repository.MangaRepository class GetDuplicateLibraryManga( private val mangaRepository: MangaRepository, ) { - - suspend fun await(manga: Manga): List { - return mangaRepository.getDuplicateLibraryManga(manga.id, manga.title.lowercase()) - } + suspend fun await(manga: Manga): List = + mangaRepository.getDuplicateLibraryManga(manga.id, manga.title.lowercase()) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetFavorites.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetFavorites.kt index a158b0f38a..8fdf015b8d 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetFavorites.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetFavorites.kt @@ -7,12 +7,7 @@ import tachiyomi.domain.manga.repository.MangaRepository class GetFavorites( private val mangaRepository: MangaRepository, ) { + suspend fun await(): List = mangaRepository.getFavorites() - suspend fun await(): List { - return mangaRepository.getFavorites() - } - - fun subscribe(sourceId: Long): Flow> { - return mangaRepository.getFavoritesBySourceId(sourceId) - } + fun subscribe(sourceId: Long): Flow> = mangaRepository.getFavoritesBySourceId(sourceId) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetLibraryManga.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetLibraryManga.kt index bcafbafade..8dd34820c1 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetLibraryManga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetLibraryManga.kt @@ -7,12 +7,7 @@ import tachiyomi.domain.manga.repository.MangaRepository class GetLibraryManga( private val mangaRepository: MangaRepository, ) { + suspend fun await(): List = mangaRepository.getLibraryManga() - suspend fun await(): List { - return mangaRepository.getLibraryManga() - } - - fun subscribe(): Flow> { - return mangaRepository.getLibraryMangaAsFlow() - } + fun subscribe(): Flow> = mangaRepository.getLibraryMangaAsFlow() } diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetManga.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetManga.kt index d4cad69d67..81bac6c2ac 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetManga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetManga.kt @@ -9,21 +9,18 @@ import tachiyomi.domain.manga.repository.MangaRepository class GetManga( private val mangaRepository: MangaRepository, ) { - - suspend fun await(id: Long): Manga? { - return try { + suspend fun await(id: Long): Manga? = + try { mangaRepository.getMangaById(id) } catch (e: Exception) { logcat(LogPriority.ERROR, e) null } - } - suspend fun subscribe(id: Long): Flow { - return mangaRepository.getMangaByIdAsFlow(id) - } + suspend fun subscribe(id: Long): Flow = mangaRepository.getMangaByIdAsFlow(id) - fun subscribe(url: String, sourceId: Long): Flow { - return mangaRepository.getMangaByUrlAndSourceIdAsFlow(url, sourceId) - } + fun subscribe( + url: String, + sourceId: Long, + ): Flow = mangaRepository.getMangaByUrlAndSourceIdAsFlow(url, sourceId) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt index c245a7da0d..cbee6d5209 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt @@ -6,7 +6,8 @@ import tachiyomi.domain.manga.repository.MangaRepository class GetMangaByUrlAndSourceId( private val mangaRepository: MangaRepository, ) { - suspend fun await(url: String, sourceId: Long): Manga? { - return mangaRepository.getMangaByUrlAndSourceId(url, sourceId) - } + suspend fun await( + url: String, + sourceId: Long, + ): Manga? = mangaRepository.getMangaByUrlAndSourceId(url, sourceId) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaWithChapters.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaWithChapters.kt index 4fddd81401..c68213de50 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaWithChapters.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaWithChapters.kt @@ -11,21 +11,21 @@ class GetMangaWithChapters( private val mangaRepository: MangaRepository, private val chapterRepository: ChapterRepository, ) { - - suspend fun subscribe(id: Long, applyScanlatorFilter: Boolean = false): Flow>> { - return combine( + suspend fun subscribe( + id: Long, + applyScanlatorFilter: Boolean = false, + ): Flow>> = + combine( mangaRepository.getMangaByIdAsFlow(id), chapterRepository.getChapterByMangaIdAsFlow(id, applyScanlatorFilter), ) { manga, chapters -> Pair(manga, chapters) } - } - suspend fun awaitManga(id: Long): Manga { - return mangaRepository.getMangaById(id) - } + suspend fun awaitManga(id: Long): Manga = mangaRepository.getMangaById(id) - suspend fun awaitChapters(id: Long, applyScanlatorFilter: Boolean = false): List { - return chapterRepository.getChapterByMangaId(id, applyScanlatorFilter) - } + suspend fun awaitChapters( + id: Long, + applyScanlatorFilter: Boolean = false, + ): List = chapterRepository.getChapterByMangaId(id, applyScanlatorFilter) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/NetworkToLocalManga.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/NetworkToLocalManga.kt index 5ca3fb647b..45734991c1 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/NetworkToLocalManga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/NetworkToLocalManga.kt @@ -6,7 +6,6 @@ import tachiyomi.domain.manga.repository.MangaRepository class NetworkToLocalManga( private val mangaRepository: MangaRepository, ) { - suspend fun await(manga: Manga): Manga { val localManga = getManga(manga.url, manga.source) return when { @@ -25,11 +24,10 @@ class NetworkToLocalManga( } } - private suspend fun getManga(url: String, sourceId: Long): Manga? { - return mangaRepository.getMangaByUrlAndSourceId(url, sourceId) - } + private suspend fun getManga( + url: String, + sourceId: Long, + ): Manga? = mangaRepository.getMangaByUrlAndSourceId(url, sourceId) - private suspend fun insertManga(manga: Manga): Long? { - return mangaRepository.insert(manga) - } + private suspend fun insertManga(manga: Manga): Long? = mangaRepository.insert(manga) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/ResetViewerFlags.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/ResetViewerFlags.kt index 44b546e2d4..ed3c19a577 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/ResetViewerFlags.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/ResetViewerFlags.kt @@ -5,8 +5,5 @@ import tachiyomi.domain.manga.repository.MangaRepository class ResetViewerFlags( private val mangaRepository: MangaRepository, ) { - - suspend fun await(): Boolean { - return mangaRepository.resetViewerFlags() - } + suspend fun await(): Boolean = mangaRepository.resetViewerFlags() } diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/SetMangaChapterFlags.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/SetMangaChapterFlags.kt index e3cacc4bf6..9f9355bd8d 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/SetMangaChapterFlags.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/SetMangaChapterFlags.kt @@ -7,60 +7,72 @@ import tachiyomi.domain.manga.repository.MangaRepository class SetMangaChapterFlags( private val mangaRepository: MangaRepository, ) { - - suspend fun awaitSetDownloadedFilter(manga: Manga, flag: Long): Boolean { - return mangaRepository.update( + suspend fun awaitSetDownloadedFilter( + manga: Manga, + flag: Long, + ): Boolean = + mangaRepository.update( MangaUpdate( id = manga.id, chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DOWNLOADED_MASK), ), ) - } - suspend fun awaitSetUnreadFilter(manga: Manga, flag: Long): Boolean { - return mangaRepository.update( + suspend fun awaitSetUnreadFilter( + manga: Manga, + flag: Long, + ): Boolean = + mangaRepository.update( MangaUpdate( id = manga.id, chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_UNREAD_MASK), ), ) - } - suspend fun awaitSetBookmarkFilter(manga: Manga, flag: Long): Boolean { - return mangaRepository.update( + suspend fun awaitSetBookmarkFilter( + manga: Manga, + flag: Long, + ): Boolean = + mangaRepository.update( MangaUpdate( id = manga.id, chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_BOOKMARKED_MASK), ), ) - } - suspend fun awaitSetDisplayMode(manga: Manga, flag: Long): Boolean { - return mangaRepository.update( + suspend fun awaitSetDisplayMode( + manga: Manga, + flag: Long, + ): Boolean = + mangaRepository.update( MangaUpdate( id = manga.id, chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DISPLAY_MASK), ), ) - } - suspend fun awaitSetSortingModeOrFlipOrder(manga: Manga, flag: Long): Boolean { - val newFlags = manga.chapterFlags.let { - if (manga.sorting == flag) { - // Just flip the order - val orderFlag = if (manga.sortDescending()) { - Manga.CHAPTER_SORT_ASC + suspend fun awaitSetSortingModeOrFlipOrder( + manga: Manga, + flag: Long, + ): Boolean { + val newFlags = + manga.chapterFlags.let { + if (manga.sorting == flag) { + // Just flip the order + val orderFlag = + if (manga.sortDescending()) { + Manga.CHAPTER_SORT_ASC + } else { + Manga.CHAPTER_SORT_DESC + } + it.setFlag(orderFlag, Manga.CHAPTER_SORT_DIR_MASK) } else { - Manga.CHAPTER_SORT_DESC + // Set new flag with ascending order + it + .setFlag(flag, Manga.CHAPTER_SORTING_MASK) + .setFlag(Manga.CHAPTER_SORT_ASC, Manga.CHAPTER_SORT_DIR_MASK) } - it.setFlag(orderFlag, Manga.CHAPTER_SORT_DIR_MASK) - } else { - // Set new flag with ascending order - it - .setFlag(flag, Manga.CHAPTER_SORTING_MASK) - .setFlag(Manga.CHAPTER_SORT_ASC, Manga.CHAPTER_SORT_DIR_MASK) } - } return mangaRepository.update( MangaUpdate( id = manga.id, @@ -77,21 +89,23 @@ class SetMangaChapterFlags( sortingMode: Long, sortingDirection: Long, displayMode: Long, - ): Boolean { - return mangaRepository.update( + ): Boolean = + mangaRepository.update( MangaUpdate( id = mangaId, - chapterFlags = 0L.setFlag(unreadFilter, Manga.CHAPTER_UNREAD_MASK) - .setFlag(downloadedFilter, Manga.CHAPTER_DOWNLOADED_MASK) - .setFlag(bookmarkedFilter, Manga.CHAPTER_BOOKMARKED_MASK) - .setFlag(sortingMode, Manga.CHAPTER_SORTING_MASK) - .setFlag(sortingDirection, Manga.CHAPTER_SORT_DIR_MASK) - .setFlag(displayMode, Manga.CHAPTER_DISPLAY_MASK), + chapterFlags = + 0L + .setFlag(unreadFilter, Manga.CHAPTER_UNREAD_MASK) + .setFlag(downloadedFilter, Manga.CHAPTER_DOWNLOADED_MASK) + .setFlag(bookmarkedFilter, Manga.CHAPTER_BOOKMARKED_MASK) + .setFlag(sortingMode, Manga.CHAPTER_SORTING_MASK) + .setFlag(sortingDirection, Manga.CHAPTER_SORT_DIR_MASK) + .setFlag(displayMode, Manga.CHAPTER_DISPLAY_MASK), ), ) - } - private fun Long.setFlag(flag: Long, mask: Long): Long { - return this and mask.inv() or (flag and mask) - } + private fun Long.setFlag( + flag: Long, + mask: Long, + ): Long = this and mask.inv() or (flag and mask) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index 1cfc01f803..b74192da43 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -31,11 +31,11 @@ data class Manga( val favoriteModifiedAt: Long?, val version: Long, ) : Serializable { - val expectedNextUpdate: Instant? - get() = nextUpdate - .takeIf { status != SManga.COMPLETED.toLong() } - ?.let { Instant.ofEpochMilli(it) } + get() = + nextUpdate + .takeIf { status != SManga.COMPLETED.toLong() } + ?.let { Instant.ofEpochMilli(it) } val sorting: Long get() = chapterFlags and CHAPTER_SORTING_MASK @@ -53,22 +53,22 @@ data class Manga( get() = chapterFlags and CHAPTER_BOOKMARKED_MASK val unreadFilter: TriState - get() = when (unreadFilterRaw) { - CHAPTER_SHOW_UNREAD -> TriState.ENABLED_IS - CHAPTER_SHOW_READ -> TriState.ENABLED_NOT - else -> TriState.DISABLED - } + get() = + when (unreadFilterRaw) { + CHAPTER_SHOW_UNREAD -> TriState.ENABLED_IS + CHAPTER_SHOW_READ -> TriState.ENABLED_NOT + else -> TriState.DISABLED + } val bookmarkedFilter: TriState - get() = when (bookmarkedFilterRaw) { - CHAPTER_SHOW_BOOKMARKED -> TriState.ENABLED_IS - CHAPTER_SHOW_NOT_BOOKMARKED -> TriState.ENABLED_NOT - else -> TriState.DISABLED - } - - fun sortDescending(): Boolean { - return chapterFlags and CHAPTER_SORT_DIR_MASK == CHAPTER_SORT_DESC - } + get() = + when (bookmarkedFilterRaw) { + CHAPTER_SHOW_BOOKMARKED -> TriState.ENABLED_IS + CHAPTER_SHOW_NOT_BOOKMARKED -> TriState.ENABLED_NOT + else -> TriState.DISABLED + } + + fun sortDescending(): Boolean = chapterFlags and CHAPTER_SORT_DIR_MASK == CHAPTER_SORT_DESC companion object { // Generic filter that does not filter anything @@ -100,30 +100,31 @@ data class Manga( const val CHAPTER_DISPLAY_NUMBER = 0x00100000L const val CHAPTER_DISPLAY_MASK = 0x00100000L - fun create() = Manga( - id = -1L, - url = "", - title = "", - source = -1L, - favorite = false, - lastUpdate = 0L, - nextUpdate = 0L, - fetchInterval = 0, - dateAdded = 0L, - viewerFlags = 0L, - chapterFlags = 0L, - coverLastModified = 0L, - artist = null, - author = null, - description = null, - genre = null, - status = 0L, - thumbnailUrl = null, - updateStrategy = UpdateStrategy.ALWAYS_UPDATE, - initialized = false, - lastModifiedAt = 0L, - favoriteModifiedAt = null, - version = 0L, - ) + fun create() = + Manga( + id = -1L, + url = "", + title = "", + source = -1L, + favorite = false, + lastUpdate = 0L, + nextUpdate = 0L, + fetchInterval = 0, + dateAdded = 0L, + viewerFlags = 0L, + chapterFlags = 0L, + coverLastModified = 0L, + artist = null, + author = null, + description = null, + genre = null, + status = 0L, + thumbnailUrl = null, + updateStrategy = UpdateStrategy.ALWAYS_UPDATE, + initialized = false, + lastModifiedAt = 0L, + favoriteModifiedAt = null, + version = 0L, + ) } } diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/MangaCover.kt b/domain/src/main/java/tachiyomi/domain/manga/model/MangaCover.kt index d2ca3b103f..20db2b58f8 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/MangaCover.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/MangaCover.kt @@ -11,12 +11,11 @@ data class MangaCover( val lastModified: Long, ) -fun Manga.asMangaCover(): MangaCover { - return MangaCover( +fun Manga.asMangaCover(): MangaCover = + MangaCover( mangaId = id, sourceId = source, isMangaFavorite = favorite, url = thumbnailUrl, lastModified = coverLastModified, ) -} diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt index 7f1cc2f87d..3e8b035b33 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt @@ -26,8 +26,8 @@ data class MangaUpdate( val version: Long? = null, ) -fun Manga.toMangaUpdate(): MangaUpdate { - return MangaUpdate( +fun Manga.toMangaUpdate(): MangaUpdate = + MangaUpdate( id = id, source = source, favorite = favorite, @@ -50,4 +50,3 @@ fun Manga.toMangaUpdate(): MangaUpdate { initialized = initialized, version = version, ) -} diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/TriState.kt b/domain/src/main/java/tachiyomi/domain/manga/model/TriState.kt index 49b8347722..095688a120 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/TriState.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/TriState.kt @@ -2,8 +2,12 @@ package tachiyomi.domain.manga.model import tachiyomi.core.common.preference.TriState -inline fun applyFilter(filter: TriState, predicate: () -> Boolean): Boolean = when (filter) { - TriState.DISABLED -> true - TriState.ENABLED_IS -> predicate() - TriState.ENABLED_NOT -> !predicate() -} +inline fun applyFilter( + filter: TriState, + predicate: () -> Boolean, +): Boolean = + when (filter) { + TriState.DISABLED -> true + TriState.ENABLED_IS -> predicate() + TriState.ENABLED_NOT -> !predicate() + } diff --git a/domain/src/main/java/tachiyomi/domain/manga/repository/MangaRepository.kt b/domain/src/main/java/tachiyomi/domain/manga/repository/MangaRepository.kt index 8c74851f3e..5e3fd50323 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/repository/MangaRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/repository/MangaRepository.kt @@ -6,14 +6,19 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate interface MangaRepository { - suspend fun getMangaById(id: Long): Manga suspend fun getMangaByIdAsFlow(id: Long): Flow - suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? + suspend fun getMangaByUrlAndSourceId( + url: String, + sourceId: Long, + ): Manga? - fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow + fun getMangaByUrlAndSourceIdAsFlow( + url: String, + sourceId: Long, + ): Flow suspend fun getFavorites(): List @@ -23,13 +28,19 @@ interface MangaRepository { fun getFavoritesBySourceId(sourceId: Long): Flow> - suspend fun getDuplicateLibraryManga(id: Long, title: String): List + suspend fun getDuplicateLibraryManga( + id: Long, + title: String, + ): List suspend fun getUpcomingManga(statuses: Set): Flow> suspend fun resetViewerFlags(): Boolean - suspend fun setMangaCategories(mangaId: Long, categoryIds: List) + suspend fun setMangaCategories( + mangaId: Long, + categoryIds: List, + ) suspend fun insert(manga: Manga): Long? diff --git a/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt b/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt index e1ba48c79d..9f70905112 100644 --- a/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt +++ b/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt @@ -11,7 +11,6 @@ class GetApplicationRelease( private val service: ReleaseService, private val preferenceStore: PreferenceStore, ) { - private val lastChecked: Preference by lazy { preferenceStore.getLong(Preference.appStateKey("last_app_check"), 0) } @@ -20,7 +19,8 @@ class GetApplicationRelease( val now = Instant.now() // Limit checks to once every 3 days at most - if (!arguments.forceCheck && now.isBefore( + if (!arguments.forceCheck && + now.isBefore( Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS), ) ) { @@ -32,12 +32,13 @@ class GetApplicationRelease( lastChecked.set(now.toEpochMilli()) // Check if latest version is different from current version - val isNewVersion = isNewVersion( - arguments.isPreview, - arguments.commitCount, - arguments.versionName, - release.version, - ) + val isNewVersion = + isNewVersion( + arguments.isPreview, + arguments.commitCount, + arguments.versionName, + release.version, + ) return when { isNewVersion && arguments.isThirdParty -> Result.ThirdPartyInstallation isNewVersion -> Result.NewUpdate(release) @@ -85,9 +86,14 @@ class GetApplicationRelease( ) sealed interface Result { - data class NewUpdate(val release: Release) : Result + data class NewUpdate( + val release: Release, + ) : Result + data object NoNewUpdate : Result + data object OsTooOld : Result + data object ThirdPartyInstallation : Result } } diff --git a/domain/src/main/java/tachiyomi/domain/release/model/Release.kt b/domain/src/main/java/tachiyomi/domain/release/model/Release.kt index 311d2f34ff..dbb6b4a695 100644 --- a/domain/src/main/java/tachiyomi/domain/release/model/Release.kt +++ b/domain/src/main/java/tachiyomi/domain/release/model/Release.kt @@ -11,19 +11,19 @@ data class Release( val releaseLink: String, private val assets: List, ) { - /** * Get download link of latest release from the assets. * @return download link of latest release. */ fun getDownloadLink(): String { - val apkVariant = when (Build.SUPPORTED_ABIS[0]) { - "arm64-v8a" -> "-arm64-v8a" - "armeabi-v7a" -> "-armeabi-v7a" - "x86" -> "-x86" - "x86_64" -> "-x86_64" - else -> "" - } + val apkVariant = + when (Build.SUPPORTED_ABIS[0]) { + "arm64-v8a" -> "-arm64-v8a" + "armeabi-v7a" -> "-armeabi-v7a" + "x86" -> "-x86" + "x86_64" -> "-x86_64" + else -> "" + } return assets.find { it.contains("mihon$apkVariant-") } ?: assets[0] } @@ -31,5 +31,7 @@ data class Release( /** * Assets class containing download url. */ - data class Assets(val downloadLink: String) + data class Assets( + val downloadLink: String, + ) } diff --git a/domain/src/main/java/tachiyomi/domain/release/service/ReleaseService.kt b/domain/src/main/java/tachiyomi/domain/release/service/ReleaseService.kt index 61bbdb3518..1774122dd0 100644 --- a/domain/src/main/java/tachiyomi/domain/release/service/ReleaseService.kt +++ b/domain/src/main/java/tachiyomi/domain/release/service/ReleaseService.kt @@ -3,6 +3,5 @@ package tachiyomi.domain.release.service import tachiyomi.domain.release.model.Release interface ReleaseService { - suspend fun latest(repository: String): Release } diff --git a/domain/src/main/java/tachiyomi/domain/source/interactor/GetRemoteManga.kt b/domain/src/main/java/tachiyomi/domain/source/interactor/GetRemoteManga.kt index 61e5e513e1..89ccf6482f 100644 --- a/domain/src/main/java/tachiyomi/domain/source/interactor/GetRemoteManga.kt +++ b/domain/src/main/java/tachiyomi/domain/source/interactor/GetRemoteManga.kt @@ -7,14 +7,16 @@ import tachiyomi.domain.source.repository.SourceRepository class GetRemoteManga( private val repository: SourceRepository, ) { - - fun subscribe(sourceId: Long, query: String, filterList: FilterList): SourcePagingSourceType { - return when (query) { + fun subscribe( + sourceId: Long, + query: String, + filterList: FilterList, + ): SourcePagingSourceType = + when (query) { QUERY_POPULAR -> repository.getPopular(sourceId) QUERY_LATEST -> repository.getLatest(sourceId) else -> repository.search(sourceId, query, filterList) } - } companion object { const val QUERY_POPULAR = "eu.kanade.domain.source.interactor.POPULAR" diff --git a/domain/src/main/java/tachiyomi/domain/source/interactor/GetSourcesWithNonLibraryManga.kt b/domain/src/main/java/tachiyomi/domain/source/interactor/GetSourcesWithNonLibraryManga.kt index d53445d0c6..7fea86739f 100644 --- a/domain/src/main/java/tachiyomi/domain/source/interactor/GetSourcesWithNonLibraryManga.kt +++ b/domain/src/main/java/tachiyomi/domain/source/interactor/GetSourcesWithNonLibraryManga.kt @@ -7,8 +7,5 @@ import tachiyomi.domain.source.repository.SourceRepository class GetSourcesWithNonLibraryManga( private val repository: SourceRepository, ) { - - fun subscribe(): Flow> { - return repository.getSourcesWithNonLibraryManga() - } + fun subscribe(): Flow> = repository.getSourcesWithNonLibraryManga() } diff --git a/domain/src/main/java/tachiyomi/domain/source/model/Pin.kt b/domain/src/main/java/tachiyomi/domain/source/model/Pin.kt index ccb629b476..00a42406ec 100644 --- a/domain/src/main/java/tachiyomi/domain/source/model/Pin.kt +++ b/domain/src/main/java/tachiyomi/domain/source/model/Pin.kt @@ -1,21 +1,25 @@ package tachiyomi.domain.source.model -sealed class Pin(val code: Int) { +sealed class Pin( + val code: Int, +) { data object Unpinned : Pin(0b00) + data object Pinned : Pin(0b01) - data object Actual : Pin(0b10) -} -inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins { - return Pins.PinsBuilder().apply(builder).flags() + data object Actual : Pin(0b10) } -fun Pins(vararg pins: Pin) = Pins { - pins.forEach { +it } -} +inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins = Pins.PinsBuilder().apply(builder).flags() -data class Pins(val code: Int = Pin.Unpinned.code) { +fun Pins(vararg pins: Pin) = + Pins { + pins.forEach { +it } + } +data class Pins( + val code: Int = Pin.Unpinned.code, +) { operator fun contains(pin: Pin): Boolean = pin.code and code == pin.code operator fun plus(pin: Pin): Pins = Pins(code or pin.code) @@ -28,7 +32,9 @@ data class Pins(val code: Int = Pin.Unpinned.code) { val pinned = Pins(Pin.Pinned, Pin.Actual) } - class PinsBuilder(var code: Int = 0) { + class PinsBuilder( + var code: Int = 0, + ) { operator fun Pin.unaryPlus() { this@PinsBuilder.code = code or this@PinsBuilder.code } diff --git a/domain/src/main/java/tachiyomi/domain/source/model/Source.kt b/domain/src/main/java/tachiyomi/domain/source/model/Source.kt index 4dfb988a1c..ab18008311 100644 --- a/domain/src/main/java/tachiyomi/domain/source/model/Source.kt +++ b/domain/src/main/java/tachiyomi/domain/source/model/Source.kt @@ -9,12 +9,12 @@ data class Source( val pin: Pins = Pins.unpinned, val isUsedLast: Boolean = false, ) { - val visualName: String - get() = when { - lang.isEmpty() -> name - else -> "$name (${lang.uppercase()})" - } + get() = + when { + lang.isEmpty() -> name + else -> "$name (${lang.uppercase()})" + } val key: () -> String = { when { diff --git a/domain/src/main/java/tachiyomi/domain/source/model/SourceWithCount.kt b/domain/src/main/java/tachiyomi/domain/source/model/SourceWithCount.kt index 02270c3c27..39af17c230 100644 --- a/domain/src/main/java/tachiyomi/domain/source/model/SourceWithCount.kt +++ b/domain/src/main/java/tachiyomi/domain/source/model/SourceWithCount.kt @@ -4,7 +4,6 @@ data class SourceWithCount( val source: Source, val count: Long, ) { - val id: Long get() = source.id diff --git a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt index 0fee2af183..2c4e87d219 100644 --- a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt +++ b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt @@ -10,24 +10,18 @@ class StubSource( override val lang: String, override val name: String, ) : Source { - private val isInvalid: Boolean = name.isBlank() || lang.isBlank() - override suspend fun getMangaDetails(manga: SManga): SManga = - throw SourceNotInstalledException() + override suspend fun getMangaDetails(manga: SManga): SManga = throw SourceNotInstalledException() + + override suspend fun getChapterList(manga: SManga): List = throw SourceNotInstalledException() - override suspend fun getChapterList(manga: SManga): List = - throw SourceNotInstalledException() - override suspend fun getPageList(chapter: SChapter): List = - throw SourceNotInstalledException() + override suspend fun getPageList(chapter: SChapter): List = throw SourceNotInstalledException() - override fun toString(): String = - if (!isInvalid) "$name (${lang.uppercase()})" else id.toString() + override fun toString(): String = if (!isInvalid) "$name (${lang.uppercase()})" else id.toString() companion object { - fun from(source: Source): StubSource { - return StubSource(id = source.id, lang = source.lang, name = source.name) - } + fun from(source: Source): StubSource = StubSource(id = source.id, lang = source.lang, name = source.name) } } diff --git a/domain/src/main/java/tachiyomi/domain/source/repository/SourceRepository.kt b/domain/src/main/java/tachiyomi/domain/source/repository/SourceRepository.kt index f5550c2f08..39aafc1b7d 100644 --- a/domain/src/main/java/tachiyomi/domain/source/repository/SourceRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/source/repository/SourceRepository.kt @@ -10,7 +10,6 @@ import tachiyomi.domain.source.model.SourceWithCount typealias SourcePagingSourceType = PagingSource interface SourceRepository { - fun getSources(): Flow> fun getOnlineSources(): Flow> @@ -19,7 +18,11 @@ interface SourceRepository { fun getSourcesWithNonLibraryManga(): Flow> - fun search(sourceId: Long, query: String, filterList: FilterList): SourcePagingSourceType + fun search( + sourceId: Long, + query: String, + filterList: FilterList, + ): SourcePagingSourceType fun getPopular(sourceId: Long): SourcePagingSourceType diff --git a/domain/src/main/java/tachiyomi/domain/source/repository/StubSourceRepository.kt b/domain/src/main/java/tachiyomi/domain/source/repository/StubSourceRepository.kt index 3671826372..8b0ccdf7a8 100644 --- a/domain/src/main/java/tachiyomi/domain/source/repository/StubSourceRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/source/repository/StubSourceRepository.kt @@ -8,5 +8,9 @@ interface StubSourceRepository { suspend fun getStubSource(id: Long): StubSource? - suspend fun upsertStubSource(id: Long, lang: String, name: String) + suspend fun upsertStubSource( + id: Long, + lang: String, + name: String, + ) } diff --git a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt index da4fb39291..ee7e42abb7 100644 --- a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt +++ b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.StateFlow import tachiyomi.domain.source.model.StubSource interface SourceManager { - val isInitialized: StateFlow val catalogueSources: Flow> diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt index 4527fe89a0..73cb651957 100644 --- a/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt @@ -19,17 +19,20 @@ class StorageManager( private val context: Context, storagePreferences: StoragePreferences, ) { - private val scope = CoroutineScope(Dispatchers.IO) private var baseDir: UniFile? = getBaseDir(storagePreferences.baseStorageDirectory().get()) private val _changes: Channel = Channel(Channel.UNLIMITED) - val changes = _changes.receiveAsFlow() - .shareIn(scope, SharingStarted.Lazily, 1) + val changes = + _changes + .receiveAsFlow() + .shareIn(scope, SharingStarted.Lazily, 1) init { - storagePreferences.baseStorageDirectory().changes() + storagePreferences + .baseStorageDirectory() + .changes() .drop(1) .distinctUntilChanged() .onEach { uri -> @@ -42,26 +45,19 @@ class StorageManager( } } _changes.send(Unit) - } - .launchIn(scope) + }.launchIn(scope) } - private fun getBaseDir(uri: String): UniFile? { - return UniFile.fromUri(context, uri.toUri()) + private fun getBaseDir(uri: String): UniFile? = + UniFile + .fromUri(context, uri.toUri()) .takeIf { it?.exists() == true } - } - fun getAutomaticBackupsDirectory(): UniFile? { - return baseDir?.createDirectory(AUTOMATIC_BACKUPS_PATH) - } + fun getAutomaticBackupsDirectory(): UniFile? = baseDir?.createDirectory(AUTOMATIC_BACKUPS_PATH) - fun getDownloadsDirectory(): UniFile? { - return baseDir?.createDirectory(DOWNLOADS_PATH) - } + fun getDownloadsDirectory(): UniFile? = baseDir?.createDirectory(DOWNLOADS_PATH) - fun getLocalSourceDirectory(): UniFile? { - return baseDir?.createDirectory(LOCAL_SOURCE_PATH) - } + fun getLocalSourceDirectory(): UniFile? = baseDir?.createDirectory(LOCAL_SOURCE_PATH) } private const val AUTOMATIC_BACKUPS_PATH = "autobackup" diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt index f29949cff0..0b4468ae59 100644 --- a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt @@ -8,6 +8,5 @@ class StoragePreferences( private val folderProvider: FolderProvider, private val preferenceStore: PreferenceStore, ) { - fun baseStorageDirectory() = preferenceStore.getString(Preference.appStateKey("storage_dir"), folderProvider.path()) } diff --git a/domain/src/main/java/tachiyomi/domain/track/interactor/DeleteTrack.kt b/domain/src/main/java/tachiyomi/domain/track/interactor/DeleteTrack.kt index 4204b3872e..78e3277098 100644 --- a/domain/src/main/java/tachiyomi/domain/track/interactor/DeleteTrack.kt +++ b/domain/src/main/java/tachiyomi/domain/track/interactor/DeleteTrack.kt @@ -7,8 +7,10 @@ import tachiyomi.domain.track.repository.TrackRepository class DeleteTrack( private val trackRepository: TrackRepository, ) { - - suspend fun await(mangaId: Long, trackerId: Long) { + suspend fun await( + mangaId: Long, + trackerId: Long, + ) { try { trackRepository.delete(mangaId, trackerId) } catch (e: Exception) { diff --git a/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracks.kt b/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracks.kt index 851ecd0ed9..9fe52ca86f 100644 --- a/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracks.kt +++ b/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracks.kt @@ -9,26 +9,21 @@ import tachiyomi.domain.track.repository.TrackRepository class GetTracks( private val trackRepository: TrackRepository, ) { - - suspend fun awaitOne(id: Long): Track? { - return try { + suspend fun awaitOne(id: Long): Track? = + try { trackRepository.getTrackById(id) } catch (e: Exception) { logcat(LogPriority.ERROR, e) null } - } - suspend fun await(mangaId: Long): List { - return try { + suspend fun await(mangaId: Long): List = + try { trackRepository.getTracksByMangaId(mangaId) } catch (e: Exception) { logcat(LogPriority.ERROR, e) emptyList() } - } - fun subscribe(mangaId: Long): Flow> { - return trackRepository.getTracksByMangaIdAsFlow(mangaId) - } + fun subscribe(mangaId: Long): Flow> = trackRepository.getTracksByMangaIdAsFlow(mangaId) } diff --git a/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracksPerManga.kt b/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracksPerManga.kt index 36478fdfea..f1c47672a4 100644 --- a/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracksPerManga.kt +++ b/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracksPerManga.kt @@ -8,8 +8,8 @@ import tachiyomi.domain.track.repository.TrackRepository class GetTracksPerManga( private val trackRepository: TrackRepository, ) { - - fun subscribe(): Flow>> { - return trackRepository.getTracksAsFlow().map { tracks -> tracks.groupBy { it.mangaId } } - } + fun subscribe(): Flow>> = + trackRepository.getTracksAsFlow().map { tracks -> + tracks.groupBy { it.mangaId } + } } diff --git a/domain/src/main/java/tachiyomi/domain/track/interactor/InsertTrack.kt b/domain/src/main/java/tachiyomi/domain/track/interactor/InsertTrack.kt index 3277e9b9ce..864dd0a6a8 100644 --- a/domain/src/main/java/tachiyomi/domain/track/interactor/InsertTrack.kt +++ b/domain/src/main/java/tachiyomi/domain/track/interactor/InsertTrack.kt @@ -8,7 +8,6 @@ import tachiyomi.domain.track.repository.TrackRepository class InsertTrack( private val trackRepository: TrackRepository, ) { - suspend fun await(track: Track) { try { trackRepository.insert(track) diff --git a/domain/src/main/java/tachiyomi/domain/track/repository/TrackRepository.kt b/domain/src/main/java/tachiyomi/domain/track/repository/TrackRepository.kt index 1814a7a080..4ce66f9edd 100644 --- a/domain/src/main/java/tachiyomi/domain/track/repository/TrackRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/track/repository/TrackRepository.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.flow.Flow import tachiyomi.domain.track.model.Track interface TrackRepository { - suspend fun getTrackById(id: Long): Track? suspend fun getTracksByMangaId(mangaId: Long): List @@ -13,7 +12,10 @@ interface TrackRepository { fun getTracksByMangaIdAsFlow(mangaId: Long): Flow> - suspend fun delete(mangaId: Long, trackerId: Long) + suspend fun delete( + mangaId: Long, + trackerId: Long, + ) suspend fun insert(track: Track) diff --git a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt index 361505642a..4d279c3527 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt @@ -8,16 +8,16 @@ import java.time.Instant class GetUpdates( private val repository: UpdatesRepository, ) { + suspend fun await( + read: Boolean, + after: Long, + ): List = repository.awaitWithRead(read, after, limit = 500) - suspend fun await(read: Boolean, after: Long): List { - return repository.awaitWithRead(read, after, limit = 500) - } + fun subscribe(instant: Instant): Flow> = + repository.subscribeAll(instant.toEpochMilli(), limit = 500) - fun subscribe(instant: Instant): Flow> { - return repository.subscribeAll(instant.toEpochMilli(), limit = 500) - } - - fun subscribe(read: Boolean, after: Long): Flow> { - return repository.subscribeWithRead(read, after, limit = 500) - } + fun subscribe( + read: Boolean, + after: Long, + ): Flow> = repository.subscribeWithRead(read, after, limit = 500) } diff --git a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt index 3b583ea90c..18fd7e7ab1 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt @@ -4,10 +4,20 @@ import kotlinx.coroutines.flow.Flow import tachiyomi.domain.updates.model.UpdatesWithRelations interface UpdatesRepository { + suspend fun awaitWithRead( + read: Boolean, + after: Long, + limit: Long, + ): List - suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List + fun subscribeAll( + after: Long, + limit: Long, + ): Flow> - fun subscribeAll(after: Long, limit: Long): Flow> - - fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow> + fun subscribeWithRead( + read: Boolean, + after: Long, + limit: Long, + ): Flow> } diff --git a/domain/src/test/java/tachiyomi/domain/chapter/service/ChapterRecognitionTest.kt b/domain/src/test/java/tachiyomi/domain/chapter/service/ChapterRecognitionTest.kt index 8369bfa5cd..c52164018e 100644 --- a/domain/src/test/java/tachiyomi/domain/chapter/service/ChapterRecognitionTest.kt +++ b/domain/src/test/java/tachiyomi/domain/chapter/service/ChapterRecognitionTest.kt @@ -7,7 +7,6 @@ import org.junit.jupiter.api.parallel.ExecutionMode @Execution(ExecutionMode.CONCURRENT) class ChapterRecognitionTest { - @Test fun `Basic Ch prefix`() { val mangaTitle = "Mokushiroku Alice" @@ -271,7 +270,11 @@ class ChapterRecognitionTest { assertChapter(mangaTitle, "The 4th Night", 4.0) } - private fun assertChapter(mangaTitle: String, name: String, expected: Double) { + private fun assertChapter( + mangaTitle: String, + name: String, + expected: Double, + ) { ChapterRecognition.parseChapterNumber(mangaTitle, name) shouldBe expected } } diff --git a/domain/src/test/java/tachiyomi/domain/chapter/service/MissingChaptersTest.kt b/domain/src/test/java/tachiyomi/domain/chapter/service/MissingChaptersTest.kt index 3435cc866b..6334000fa5 100644 --- a/domain/src/test/java/tachiyomi/domain/chapter/service/MissingChaptersTest.kt +++ b/domain/src/test/java/tachiyomi/domain/chapter/service/MissingChaptersTest.kt @@ -8,7 +8,6 @@ import tachiyomi.domain.chapter.model.Chapter @Execution(ExecutionMode.CONCURRENT) class MissingChaptersTest { - @Test fun `missingChaptersCount returns 0 when empty list`() { emptyList().missingChaptersCount() shouldBe 0 @@ -51,7 +50,8 @@ class MissingChaptersTest { calculateChapterGap(99.0, -1.0) shouldBe 0 } - private fun chapter(number: Double) = Chapter.create().copy( - chapterNumber = number, - ) + private fun chapter(number: Double) = + Chapter.create().copy( + chapterNumber = number, + ) } diff --git a/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt b/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt index a3a2237822..64bbd5bc38 100644 --- a/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt +++ b/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt @@ -8,7 +8,6 @@ import org.junit.jupiter.api.parallel.ExecutionMode @Execution(ExecutionMode.CONCURRENT) class LibraryFlagsTest { - @Test fun `Check the amount of flags`() { LibraryDisplayMode.values.size shouldBe 4 @@ -34,10 +33,11 @@ class LibraryFlagsTest { @Test fun `Test Flag plus operator with old flag as base`() { - val currentSort = LibrarySort( - LibrarySort.Type.UnreadCount, - LibrarySort.Direction.Descending, - ) + val currentSort = + LibrarySort( + LibrarySort.Type.UnreadCount, + LibrarySort.Direction.Descending, + ) currentSort.flag shouldBe 0b00001100 val sort = LibrarySort(LibrarySort.Type.DateAdded, LibrarySort.Direction.Ascending) diff --git a/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt b/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt index ccaaf24da5..e1d3535296 100644 --- a/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt +++ b/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt @@ -17,37 +17,41 @@ import kotlin.time.toJavaDuration @Execution(ExecutionMode.CONCURRENT) class FetchIntervalTest { - private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z") private val testZoneId = ZoneOffset.UTC - private var chapter = Chapter.create().copy( - dateFetch = testTime.toEpochSecond() * 1000, - dateUpload = testTime.toEpochSecond() * 1000, - ) + private var chapter = + Chapter.create().copy( + dateFetch = testTime.toEpochSecond() * 1000, + dateUpload = testTime.toEpochSecond() * 1000, + ) private val fetchInterval = FetchInterval(mockk()) @Test fun `returns default interval of 7 days when not enough distinct days`() { - val chaptersWithUploadDate = (1..50).map { - chapterWithTime(chapter, 1.days) - } + val chaptersWithUploadDate = + (1..50).map { + chapterWithTime(chapter, 1.days) + } fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 7 - val chaptersWithoutUploadDate = chaptersWithUploadDate.map { - it.copy(dateUpload = 0L) - } + val chaptersWithoutUploadDate = + chaptersWithUploadDate.map { + it.copy(dateUpload = 0L) + } fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 7 } @Test fun `returns interval based on more recent chapters`() { - val oldChapters = (1..5).map { - chapterWithTime(chapter, (it * 7).days) // Would have interval of 7 days - } - val newChapters = (1..10).map { - chapterWithTime(chapter, oldChapters.lastUploadDate() + it.days) - } + val oldChapters = + (1..5).map { + chapterWithTime(chapter, (it * 7).days) // Would have interval of 7 days + } + val newChapters = + (1..10).map { + chapterWithTime(chapter, oldChapters.lastUploadDate() + it.days) + } val chapters = oldChapters + newChapters @@ -56,13 +60,15 @@ class FetchIntervalTest { @Test fun `returns interval based on smaller subset of recent chapters if very few chapters`() { - val oldChapters = (1..3).map { - chapterWithTime(chapter, (it * 7).days) - } + val oldChapters = + (1..3).map { + chapterWithTime(chapter, (it * 7).days) + } // Significant gap between chapters - val newChapters = (1..3).map { - chapterWithTime(chapter, oldChapters.lastUploadDate() + 365.days + (it * 7).days) - } + val newChapters = + (1..3).map { + chapterWithTime(chapter, oldChapters.lastUploadDate() + 365.days + (it * 7).days) + } val chapters = oldChapters + newChapters @@ -71,72 +77,83 @@ class FetchIntervalTest { @Test fun `returns interval of 7 days when multiple chapters in 1 day`() { - val chapters = (1..10).map { - chapterWithTime(chapter, 10.hours) - } + val chapters = + (1..10).map { + chapterWithTime(chapter, 10.hours) + } fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7 } @Test fun `returns interval of 7 days when multiple chapters in 2 days`() { - val chapters = (1..2).map { - chapterWithTime(chapter, 1.days) - } + (1..5).map { - chapterWithTime(chapter, 2.days) - } + val chapters = + (1..2).map { + chapterWithTime(chapter, 1.days) + } + + (1..5).map { + chapterWithTime(chapter, 2.days) + } fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7 } @Test fun `returns interval of 1 day when chapters are released every 1 day`() { - val chapters = (1..20).map { - chapterWithTime(chapter, it.days) - } + val chapters = + (1..20).map { + chapterWithTime(chapter, it.days) + } fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 } @Test fun `returns interval of 1 day when delta is less than 1 day`() { - val chapters = (1..20).map { - chapterWithTime(chapter, (15 * it).hours) - } + val chapters = + (1..20).map { + chapterWithTime(chapter, (15 * it).hours) + } fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 } @Test fun `returns interval of 2 days when chapters are released every 2 days`() { - val chapters = (1..20).map { - chapterWithTime(chapter, (2 * it).days) - } + val chapters = + (1..20).map { + chapterWithTime(chapter, (2 * it).days) + } fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 2 } @Test fun `returns interval with floored value when interval is decimal`() { - val chaptersWithUploadDate = (1..5).map { - chapterWithTime(chapter, (25 * it).hours) - } + val chaptersWithUploadDate = + (1..5).map { + chapterWithTime(chapter, (25 * it).hours) + } fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 1 - val chaptersWithoutUploadDate = chaptersWithUploadDate.map { - it.copy(dateUpload = 0L) - } + val chaptersWithoutUploadDate = + chaptersWithUploadDate.map { + it.copy(dateUpload = 0L) + } fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 1 } @Test fun `returns interval of 1 day when chapters are released just below every 2 days`() { - val chapters = (1..20).map { - chapterWithTime(chapter, (43 * it).hours) - } + val chapters = + (1..20).map { + chapterWithTime(chapter, (43 * it).hours) + } fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 } - private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter { + private fun chapterWithTime( + chapter: Chapter, + duration: Duration, + ): Chapter { val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000 return chapter.copy(dateFetch = newTime, dateUpload = newTime) } - private fun List.lastUploadDate() = - last().dateUpload.toDuration(DurationUnit.MILLISECONDS) + private fun List.lastUploadDate() = last().dateUpload.toDuration(DurationUnit.MILLISECONDS) } diff --git a/domain/src/test/java/tachiyomi/domain/release/interactor/GetApplicationReleaseTest.kt b/domain/src/test/java/tachiyomi/domain/release/interactor/GetApplicationReleaseTest.kt index fe564050a2..661af20254 100644 --- a/domain/src/test/java/tachiyomi/domain/release/interactor/GetApplicationReleaseTest.kt +++ b/domain/src/test/java/tachiyomi/domain/release/interactor/GetApplicationReleaseTest.kt @@ -15,7 +15,6 @@ import tachiyomi.domain.release.service.ReleaseService import java.time.Instant class GetApplicationReleaseTest { - lateinit var getApplicationRelease: GetApplicationRelease lateinit var releaseService: ReleaseService lateinit var preference: Preference @@ -31,140 +30,159 @@ class GetApplicationReleaseTest { } @Test - fun `When has update but is third party expect third party installation`() = runTest { - every { preference.get() } returns 0 - every { preference.set(any()) }.answers { } - - coEvery { releaseService.latest(any()) } returns Release( - "v2.0.0", - "info", - "http://example.com/release_link", - listOf("http://example.com/assets"), - ) - - val result = getApplicationRelease.await( - GetApplicationRelease.Arguments( - isPreview = false, - isThirdParty = true, - commitCount = 0, - versionName = "v1.0.0", - repository = "test", - ), - ) - - result shouldBe GetApplicationRelease.Result.ThirdPartyInstallation - } + fun `When has update but is third party expect third party installation`() = + runTest { + every { preference.get() } returns 0 + every { preference.set(any()) }.answers { } + + coEvery { releaseService.latest(any()) } returns + Release( + "v2.0.0", + "info", + "http://example.com/release_link", + listOf("http://example.com/assets"), + ) + + val result = + getApplicationRelease.await( + GetApplicationRelease.Arguments( + isPreview = false, + isThirdParty = true, + commitCount = 0, + versionName = "v1.0.0", + repository = "test", + ), + ) + + result shouldBe GetApplicationRelease.Result.ThirdPartyInstallation + } @Test - fun `When has update but is preview expect new update`() = runTest { - every { preference.get() } returns 0 - every { preference.set(any()) }.answers { } - - val release = Release( - "r2000", - "info", - "http://example.com/release_link", - listOf("http://example.com/assets"), - ) - - coEvery { releaseService.latest(any()) } returns release - - val result = getApplicationRelease.await( - GetApplicationRelease.Arguments( - isPreview = true, - isThirdParty = false, - commitCount = 1000, - versionName = "", - repository = "test", - ), - ) - - (result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate( - release, - ).release - } + fun `When has update but is preview expect new update`() = + runTest { + every { preference.get() } returns 0 + every { preference.set(any()) }.answers { } + + val release = + Release( + "r2000", + "info", + "http://example.com/release_link", + listOf("http://example.com/assets"), + ) + + coEvery { releaseService.latest(any()) } returns release + + val result = + getApplicationRelease.await( + GetApplicationRelease.Arguments( + isPreview = true, + isThirdParty = false, + commitCount = 1000, + versionName = "", + repository = "test", + ), + ) + + (result as GetApplicationRelease.Result.NewUpdate).release shouldBe + GetApplicationRelease.Result + .NewUpdate( + release, + ).release + } @Test - fun `When has update expect new update`() = runTest { - every { preference.get() } returns 0 - every { preference.set(any()) }.answers { } - - val release = Release( - "v2.0.0", - "info", - "http://example.com/release_link", - listOf("http://example.com/assets"), - ) - - coEvery { releaseService.latest(any()) } returns release - - val result = getApplicationRelease.await( - GetApplicationRelease.Arguments( - isPreview = false, - isThirdParty = false, - commitCount = 0, - versionName = "v1.0.0", - repository = "test", - ), - ) - - (result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate( - release, - ).release - } + fun `When has update expect new update`() = + runTest { + every { preference.get() } returns 0 + every { preference.set(any()) }.answers { } + + val release = + Release( + "v2.0.0", + "info", + "http://example.com/release_link", + listOf("http://example.com/assets"), + ) + + coEvery { releaseService.latest(any()) } returns release + + val result = + getApplicationRelease.await( + GetApplicationRelease.Arguments( + isPreview = false, + isThirdParty = false, + commitCount = 0, + versionName = "v1.0.0", + repository = "test", + ), + ) + + (result as GetApplicationRelease.Result.NewUpdate).release shouldBe + GetApplicationRelease.Result + .NewUpdate( + release, + ).release + } @Test - fun `When has no update expect no new update`() = runTest { - every { preference.get() } returns 0 - every { preference.set(any()) }.answers { } - - val release = Release( - "v1.0.0", - "info", - "http://example.com/release_link", - listOf("http://example.com/assets"), - ) - - coEvery { releaseService.latest(any()) } returns release - - val result = getApplicationRelease.await( - GetApplicationRelease.Arguments( - isPreview = false, - isThirdParty = false, - commitCount = 0, - versionName = "v2.0.0", - repository = "test", - ), - ) - - result shouldBe GetApplicationRelease.Result.NoNewUpdate - } + fun `When has no update expect no new update`() = + runTest { + every { preference.get() } returns 0 + every { preference.set(any()) }.answers { } + + val release = + Release( + "v1.0.0", + "info", + "http://example.com/release_link", + listOf("http://example.com/assets"), + ) + + coEvery { releaseService.latest(any()) } returns release + + val result = + getApplicationRelease.await( + GetApplicationRelease.Arguments( + isPreview = false, + isThirdParty = false, + commitCount = 0, + versionName = "v2.0.0", + repository = "test", + ), + ) + + result shouldBe GetApplicationRelease.Result.NoNewUpdate + } @Test - fun `When now is before three days expect no new update`() = runTest { - every { preference.get() } returns Instant.now().toEpochMilli() - every { preference.set(any()) }.answers { } - - val release = Release( - "v1.0.0", - "info", - "http://example.com/release_link", - listOf("http://example.com/assets"), - ) - - coEvery { releaseService.latest(any()) } returns release - - val result = getApplicationRelease.await( - GetApplicationRelease.Arguments( - isPreview = false, - isThirdParty = false, - commitCount = 0, - versionName = "v2.0.0", - repository = "test", - ), - ) - - coVerify(exactly = 0) { releaseService.latest(any()) } - result shouldBe GetApplicationRelease.Result.NoNewUpdate - } + fun `When now is before three days expect no new update`() = + runTest { + every { preference.get() } returns Instant.now().toEpochMilli() + every { preference.set(any()) }.answers { } + + val release = + Release( + "v1.0.0", + "info", + "http://example.com/release_link", + listOf("http://example.com/assets"), + ) + + coEvery { releaseService.latest(any()) } returns release + + val result = + getApplicationRelease.await( + GetApplicationRelease.Arguments( + isPreview = false, + isThirdParty = false, + commitCount = 0, + versionName = "v2.0.0", + repository = "test", + ), + ) + + coVerify(exactly = 0) { releaseService.latest(any()) } + result shouldBe GetApplicationRelease.Result.NoNewUpdate + } } diff --git a/ktlint.jar b/ktlint.jar new file mode 100644 index 0000000000..a03ba8105c Binary files /dev/null and b/ktlint.jar differ diff --git a/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/BaselineProfileGenerator.kt b/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/BaselineProfileGenerator.kt index f612ccd50c..8e36f67f11 100644 --- a/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/BaselineProfileGenerator.kt +++ b/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/BaselineProfileGenerator.kt @@ -6,28 +6,28 @@ import org.junit.Rule import org.junit.Test class BaselineProfileGenerator { - @get:Rule val baselineProfileRule = BaselineProfileRule() @Test - fun generate() = baselineProfileRule.collect( - packageName = "eu.kanade.tachiyomi.benchmark", - profileBlock = { - pressHome() - startActivityAndWait() - - device.findObject(By.text("Updates")).click() - - device.findObject(By.text("History")).click() - - // TODO: automate storage permissions and possibly open manga details screen too? - // device.findObject(By.text("Browse")).click() - // device.findObject(By.text("Extensions")).click() - // device.swipe(150, 150, 50, 150, 1) - - device.findObject(By.text("More")).click() - device.findObject(By.text("Settings")).click() - }, - ) + fun generate() = + baselineProfileRule.collect( + packageName = "eu.kanade.tachiyomi.benchmark", + profileBlock = { + pressHome() + startActivityAndWait() + + device.findObject(By.text("Updates")).click() + + device.findObject(By.text("History")).click() + + // TODO: automate storage permissions and possibly open manga details screen too? + // device.findObject(By.text("Browse")).click() + // device.findObject(By.text("Extensions")).click() + // device.swipe(150, 150, 50, 150, 1) + + device.findObject(By.text("More")).click() + device.findObject(By.text("Settings")).click() + }, + ) } diff --git a/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/StartupBenchmark.kt b/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/StartupBenchmark.kt index 87c1d33c1c..28b1b44e6c 100644 --- a/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/StartupBenchmark.kt +++ b/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/StartupBenchmark.kt @@ -51,7 +51,9 @@ class HotStartupBenchmark : AbstractStartupBenchmark(StartupMode.HOT) * Base class for benchmarks with different startup modes. * Enables app startups from various states of baseline profile or [CompilationMode]s. */ -abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) { +abstract class AbstractStartupBenchmark( + private val startupMode: StartupMode, +) { @get:Rule val benchmarkRule = MacrobenchmarkRule() @@ -59,31 +61,34 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) { fun startupNoCompilation() = startup(CompilationMode.None()) @Test - fun startupBaselineProfileDisabled() = startup( - CompilationMode.Partial( - baselineProfileMode = BaselineProfileMode.Disable, - warmupIterations = 1, - ), - ) + fun startupBaselineProfileDisabled() = + startup( + CompilationMode.Partial( + baselineProfileMode = BaselineProfileMode.Disable, + warmupIterations = 1, + ), + ) @Test - fun startupBaselineProfile() = startup( - CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require), - ) + fun startupBaselineProfile() = + startup( + CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require), + ) @Test fun startupFullCompilation() = startup(CompilationMode.Full()) - private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( - packageName = "eu.kanade.tachiyomi.benchmark", - metrics = listOf(StartupTimingMetric()), - compilationMode = compilationMode, - iterations = 10, - startupMode = startupMode, - setupBlock = { - pressHome() - }, - ) { - startActivityAndWait() - } + private fun startup(compilationMode: CompilationMode) = + benchmarkRule.measureRepeated( + packageName = "eu.kanade.tachiyomi.benchmark", + metrics = listOf(StartupTimingMetric()), + compilationMode = compilationMode, + iterations = 10, + startupMode = startupMode, + setupBlock = { + pressHome() + }, + ) { + startActivityAndWait() + } } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt index bb9ed8e857..f8319966bb 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt @@ -73,27 +73,27 @@ fun AdaptiveSheet( } } Box( - modifier = Modifier - .clickable( - interactionSource = null, - indication = null, - onClick = internalOnDismissRequest, - ) - .fillMaxSize() - .alpha(alpha), - contentAlignment = Alignment.Center, - ) { - Surface( - modifier = Modifier - .requiredWidthIn(max = 460.dp) + modifier = + Modifier .clickable( interactionSource = null, indication = null, - onClick = {}, - ) - .systemBarsPadding() - .padding(vertical = 16.dp) - .then(modifier), + onClick = internalOnDismissRequest, + ).fillMaxSize() + .alpha(alpha), + contentAlignment = Alignment.Center, + ) { + Surface( + modifier = + Modifier + .requiredWidthIn(max = 460.dp) + .clickable( + interactionSource = null, + indication = null, + onClick = {}, + ).systemBarsPadding() + .padding(vertical = 16.dp) + .then(modifier), shape = MaterialTheme.shapes.extraLarge, color = MaterialTheme.colorScheme.surfaceContainerHigh, content = { @@ -108,75 +108,74 @@ fun AdaptiveSheet( } } else { val decayAnimationSpec = rememberSplineBasedDecay() - val anchoredDraggableState = remember { - AnchoredDraggableState( - initialValue = 1, - positionalThreshold = { with(density) { 56.dp.toPx() } }, - velocityThreshold = { with(density) { 125.dp.toPx() } }, - snapAnimationSpec = sheetAnimationSpec, - decayAnimationSpec = decayAnimationSpec, - ) - } + val anchoredDraggableState = + remember { + AnchoredDraggableState( + initialValue = 1, + positionalThreshold = { with(density) { 56.dp.toPx() } }, + velocityThreshold = { with(density) { 125.dp.toPx() } }, + snapAnimationSpec = sheetAnimationSpec, + decayAnimationSpec = decayAnimationSpec, + ) + } val internalOnDismissRequest = { if (anchoredDraggableState.settledValue == 0) { scope.launch { anchoredDraggableState.animateTo(1) } } } Box( - modifier = Modifier - .clickable( - interactionSource = null, - indication = null, - onClick = internalOnDismissRequest, - ) - .fillMaxSize() - .onSizeChanged { - val anchors = DraggableAnchors { - 0 at 0f - 1 at it.height.toFloat() - } - anchoredDraggableState.updateAnchors(anchors) - }, - contentAlignment = Alignment.BottomCenter, - ) { - Surface( - modifier = Modifier - .widthIn(max = 460.dp) + modifier = + Modifier .clickable( interactionSource = null, indication = null, - onClick = {}, - ) - .then( - if (enableSwipeDismiss) { - Modifier.nestedScroll( - remember(anchoredDraggableState) { - anchoredDraggableState.preUpPostDownNestedScrollConnection( - onFling = { scope.launch { anchoredDraggableState.settle(it) } } - ) - }, + onClick = internalOnDismissRequest, + ).fillMaxSize() + .onSizeChanged { + val anchors = + DraggableAnchors { + 0 at 0f + 1 at it.height.toFloat() + } + anchoredDraggableState.updateAnchors(anchors) + }, + contentAlignment = Alignment.BottomCenter, + ) { + Surface( + modifier = + Modifier + .widthIn(max = 460.dp) + .clickable( + interactionSource = null, + indication = null, + onClick = {}, + ).then( + if (enableSwipeDismiss) { + Modifier.nestedScroll( + remember(anchoredDraggableState) { + anchoredDraggableState.preUpPostDownNestedScrollConnection( + onFling = { scope.launch { anchoredDraggableState.settle(it) } }, + ) + }, + ) + } else { + Modifier + }, + ).then(modifier) + .offset { + IntOffset( + 0, + anchoredDraggableState.offset + .takeIf { it.isFinite() } + ?.roundToInt() + ?: 0, ) - } else { - Modifier - }, - ) - .then(modifier) - .offset { - IntOffset( - 0, - anchoredDraggableState.offset - .takeIf { it.isFinite() } - ?.roundToInt() - ?: 0, - ) - } - .anchoredDraggable( - state = anchoredDraggableState, - orientation = Orientation.Vertical, - enabled = enableSwipeDismiss, - ) - .navigationBarsPadding() - .statusBarsPadding(), + }.anchoredDraggable( + state = anchoredDraggableState, + orientation = Orientation.Vertical, + enabled = enableSwipeDismiss, + ).navigationBarsPadding() + .statusBarsPadding(), shape = MaterialTheme.shapes.extraLarge, color = MaterialTheme.colorScheme.surfaceContainerHigh, content = { @@ -201,51 +200,55 @@ fun AdaptiveSheet( } } -private fun AnchoredDraggableState.preUpPostDownNestedScrollConnection( - onFling: (velocity: Float) -> Unit -) = object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.UserInput) { - dispatchRawDelta(delta).toOffset() - } else { - Offset.Zero +private fun AnchoredDraggableState.preUpPostDownNestedScrollConnection(onFling: (velocity: Float) -> Unit) = + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.UserInput) { + dispatchRawDelta(delta).toOffset() + } else { + Offset.Zero + } } - } - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - return if (source == NestedScrollSource.UserInput) { - dispatchRawDelta(available.toFloat()).toOffset() - } else { - Offset.Zero - } - } + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset = + if (source == NestedScrollSource.UserInput) { + dispatchRawDelta(available.toFloat()).toOffset() + } else { + Offset.Zero + } - override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = available.toFloat() - return if (toFling < 0 && offset > anchors.minAnchor()) { - onFling(toFling) - // since we go to the anchor with tween settling, consume all for the best UX - available - } else { - Velocity.Zero + override suspend fun onPreFling(available: Velocity): Velocity { + val toFling = available.toFloat() + return if (toFling < 0 && offset > anchors.minAnchor()) { + onFling(toFling) + // since we go to the anchor with tween settling, consume all for the best UX + available + } else { + Velocity.Zero + } } - } - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - onFling(available.toFloat()) - return available - } + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity, + ): Velocity { + onFling(available.toFloat()) + return available + } - private fun Float.toOffset(): Offset = Offset(0f, this) + private fun Float.toOffset(): Offset = Offset(0f, this) - @JvmName("velocityToFloat") - private fun Velocity.toFloat() = this.y + @JvmName("velocityToFloat") + private fun Velocity.toFloat() = this.y - @JvmName("offsetToFloat") - private fun Offset.toFloat(): Float = this.y -} + @JvmName("offsetToFloat") + private fun Offset.toFloat(): Float = this.y + } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Badges.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Badges.kt index 448ee64e29..b570900534 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Badges.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Badges.kt @@ -44,10 +44,11 @@ fun Badge( ) { Text( text = text, - modifier = modifier - .clip(shape) - .background(color) - .padding(horizontal = 3.dp, vertical = 1.dp), + modifier = + modifier + .clip(shape) + .background(color) + .padding(horizontal = 3.dp, vertical = 1.dp), color = textColor, fontWeight = FontWeight.Medium, maxLines = 1, @@ -64,35 +65,38 @@ fun Badge( shape: Shape = RectangleShape, ) { val iconContentPlaceholder = "[icon]" - val text = buildAnnotatedString { - appendInlineContent(iconContentPlaceholder) - } - val inlineContent = persistentMapOf( - Pair( - iconContentPlaceholder, - InlineTextContent( - Placeholder( - width = MaterialTheme.typography.bodySmall.fontSize, - height = MaterialTheme.typography.bodySmall.fontSize, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center, - ), - ) { - Icon( - imageVector = imageVector, - tint = iconColor, - contentDescription = null, - ) - }, - ), - ) + val text = + buildAnnotatedString { + appendInlineContent(iconContentPlaceholder) + } + val inlineContent = + persistentMapOf( + Pair( + iconContentPlaceholder, + InlineTextContent( + Placeholder( + width = MaterialTheme.typography.bodySmall.fontSize, + height = MaterialTheme.typography.bodySmall.fontSize, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + ) { + Icon( + imageVector = imageVector, + tint = iconColor, + contentDescription = null, + ) + }, + ), + ) Text( text = text, inlineContent = inlineContent, - modifier = modifier - .clip(shape) - .background(color) - .padding(horizontal = 3.dp, vertical = 1.dp), + modifier = + modifier + .clip(shape) + .background(color) + .padding(horizontal = 3.dp, vertical = 1.dp), color = iconColor, fontWeight = FontWeight.Medium, maxLines = 1, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/CircularProgressIndicator.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/CircularProgressIndicator.kt index b123e26e5b..8716e227af 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/CircularProgressIndicator.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/CircularProgressIndicator.kt @@ -56,10 +56,11 @@ fun CombinedCircularProgressIndicator( val rotation by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 360f, - animationSpec = infiniteRepeatable( - animation = tween(2000, easing = LinearEasing), - repeatMode = RepeatMode.Restart, - ), + animationSpec = + infiniteRepeatable( + animation = tween(2000, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), label = "rotation", ) val animatedProgress by animateFloatAsState( @@ -85,14 +86,15 @@ private fun CombinedCircularProgressIndicatorPreview() { Button( modifier = Modifier.fillMaxWidth(), onClick = { - progress = when (progress) { - 0f -> 0.15f - 0.15f -> 0.25f - 0.25f -> 0.5f - 0.5f -> 0.75f - 0.75f -> 0.95f - else -> 0f - } + progress = + when (progress) { + 0f -> 0.15f + 0.15f -> 0.25f + 0.25f -> 0.5f + 0.5f -> 0.75f + 0.75f -> 0.95f + else -> 0f + } }, ) { Text("change") @@ -101,9 +103,10 @@ private fun CombinedCircularProgressIndicatorPreview() { ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxSize() - .padding(it), + modifier = + Modifier + .fillMaxSize() + .padding(it), ) { CombinedCircularProgressIndicator(progress = { progress }) } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt index b70cb27050..301711fad8 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt @@ -31,10 +31,11 @@ fun CollapsibleBox( Column { Row( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = !expanded } - .padding(horizontal = 24.dp, vertical = 12.dp), + modifier = + Modifier + .fillMaxWidth() + .clickable { expanded = !expanded } + .padding(horizontal = 24.dp, vertical = 12.dp), ) { Text( text = heading, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt index a66bf0d184..0bb978635b 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt @@ -25,18 +25,19 @@ fun LabeledCheckbox( enabled: Boolean = true, ) { Row( - modifier = modifier - .clip(MaterialTheme.shapes.small) - .fillMaxWidth() - .heightIn(min = 48.dp) - .clickable( - role = Role.Checkbox, - onClick = { - if (enabled) { - onCheckedChange(!checked) - } - }, - ), + modifier = + modifier + .clip(MaterialTheme.shapes.small) + .fillMaxWidth() + .heightIn(min = 48.dp) + .clickable( + role = Role.Checkbox, + onClick = { + if (enabled) { + onCheckedChange(!checked) + } + }, + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyColumnWithAction.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyColumnWithAction.kt index de2f7636b9..9bdbd1bc44 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyColumnWithAction.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyColumnWithAction.kt @@ -25,9 +25,10 @@ fun LazyColumnWithAction( content: LazyListScope.() -> Unit, ) { Column( - modifier = modifier - .padding(contentPadding) - .fillMaxSize(), + modifier = + modifier + .padding(contentPadding) + .fillMaxSize(), ) { LazyColumn( modifier = Modifier.weight(1f), @@ -37,9 +38,10 @@ fun LazyColumnWithAction( HorizontalDivider() Button( - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 8.dp) - .fillMaxWidth(), + modifier = + Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), enabled = actionEnabled, onClick = onClickAction, ) { diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyList.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyList.kt index e5d27e0e10..650cc65612 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyList.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyList.kt @@ -33,16 +33,18 @@ fun ScrollbarLazyColumn( ) { val direction = LocalLayoutDirection.current val density = LocalDensity.current - val positionOffset = remember(contentPadding) { - with(density) { contentPadding.calculateEndPadding(direction).toPx() } - } + val positionOffset = + remember(contentPadding) { + with(density) { contentPadding.calculateEndPadding(direction).toPx() } + } LazyColumn( - modifier = modifier - .drawVerticalScrollbar( - state = state, - reverseScrolling = reverseLayout, - positionOffsetPx = positionOffset, - ), + modifier = + modifier + .drawVerticalScrollbar( + state = state, + reverseScrolling = reverseLayout, + positionOffsetPx = positionOffset, + ), state = state, contentPadding = contentPadding, reverseLayout = reverseLayout, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt index 1aaba373ed..a1ea277400 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt @@ -15,11 +15,12 @@ fun ListGroupHeader( ) { Text( text = text, - modifier = modifier - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ), + modifier = + modifier + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.bodyMedium, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pill.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pill.kt index 1d8cf2d5f5..972a5dac86 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pill.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pill.kt @@ -22,15 +22,17 @@ fun Pill( fontSize: TextUnit = LocalTextStyle.current.fontSize, ) { Surface( - modifier = modifier - .padding(start = 4.dp), + modifier = + modifier + .padding(start = 4.dp), shape = MaterialTheme.shapes.extraLarge, color = color, contentColor = contentColor, ) { Box( - modifier = Modifier - .padding(6.dp, 1.dp), + modifier = + Modifier + .padding(6.dp, 1.dp), contentAlignment = Alignment.Center, ) { Text( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt index f40fcdfb6f..5dcf1d18a5 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt @@ -28,12 +28,13 @@ fun LazyItemScope.SectionCard( } ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ), + modifier = + Modifier + .fillMaxWidth() + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), shape = MaterialTheme.shapes.extraLarge, ) { Column(modifier = Modifier.padding(MaterialTheme.padding.medium)) { diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt index 4fd4b75718..acc2670e14 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt @@ -67,17 +67,22 @@ fun HeadingItem(text: String) { Text( text = text, style = MaterialTheme.typography.header, - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = SettingsItemsPaddings.Horizontal, - vertical = SettingsItemsPaddings.Vertical, - ), + modifier = + Modifier + .fillMaxWidth() + .padding( + horizontal = SettingsItemsPaddings.Horizontal, + vertical = SettingsItemsPaddings.Vertical, + ), ) } @Composable -fun IconItem(label: String, icon: ImageVector, onClick: () -> Unit) { +fun IconItem( + label: String, + icon: ImageVector, + onClick: () -> Unit, +) { BaseSettingsItem( label = label, widget = { @@ -92,12 +97,17 @@ fun IconItem(label: String, icon: ImageVector, onClick: () -> Unit) { } @Composable -fun SortItem(label: String, sortDescending: Boolean?, onClick: () -> Unit) { - val arrowIcon = when (sortDescending) { - true -> Icons.Default.ArrowDownward - false -> Icons.Default.ArrowUpward - null -> null - } +fun SortItem( + label: String, + sortDescending: Boolean?, + onClick: () -> Unit, +) { + val arrowIcon = + when (sortDescending) { + true -> Icons.Default.ArrowDownward + false -> Icons.Default.ArrowUpward + null -> null + } BaseSettingsItem( label = label, @@ -117,7 +127,10 @@ fun SortItem(label: String, sortDescending: Boolean?, onClick: () -> Unit) { } @Composable -fun CheckboxItem(label: String, pref: Preference) { +fun CheckboxItem( + label: String, + pref: Preference, +) { val checked by pref.collectAsState() CheckboxItem( label = label, @@ -127,7 +140,11 @@ fun CheckboxItem(label: String, pref: Preference) { } @Composable -fun CheckboxItem(label: String, checked: Boolean, onClick: () -> Unit) { +fun CheckboxItem( + label: String, + checked: Boolean, + onClick: () -> Unit, +) { BaseSettingsItem( label = label, widget = { @@ -141,7 +158,11 @@ fun CheckboxItem(label: String, checked: Boolean, onClick: () -> Unit) { } @Composable -fun RadioItem(label: String, selected: Boolean, onClick: () -> Unit) { +fun RadioItem( + label: String, + selected: Boolean, + onClick: () -> Unit, +) { BaseSettingsItem( label = label, widget = { @@ -166,12 +187,13 @@ fun SliderItem( val haptic = LocalHapticFeedback.current Row( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = SettingsItemsPaddings.Horizontal, - vertical = SettingsItemsPaddings.Vertical, - ), + modifier = + Modifier + .fillMaxWidth() + .padding( + horizontal = SettingsItemsPaddings.Horizontal, + vertical = SettingsItemsPaddings.Vertical, + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp), ) { @@ -213,13 +235,14 @@ fun SelectItem( onExpandedChange = { expanded = !expanded }, ) { OutlinedTextField( - modifier = Modifier - .menuAnchor() - .fillMaxWidth() - .padding( - horizontal = SettingsItemsPaddings.Horizontal, - vertical = SettingsItemsPaddings.Vertical, - ), + modifier = + Modifier + .menuAnchor() + .fillMaxWidth() + .padding( + horizontal = SettingsItemsPaddings.Horizontal, + vertical = SettingsItemsPaddings.Vertical, + ), label = { Text(text = label) }, value = options[selectedIndex].toString(), onValueChange = {}, @@ -260,42 +283,44 @@ fun TriStateItem( onClick: ((TriState) -> Unit)?, ) { Row( - modifier = Modifier - .clickable( - enabled = enabled && onClick != null, - onClick = { - when (state) { - TriState.DISABLED -> onClick?.invoke(TriState.ENABLED_IS) - TriState.ENABLED_IS -> onClick?.invoke(TriState.ENABLED_NOT) - TriState.ENABLED_NOT -> onClick?.invoke(TriState.DISABLED) - } - }, - ) - .fillMaxWidth() - .padding( - horizontal = SettingsItemsPaddings.Horizontal, - vertical = SettingsItemsPaddings.Vertical, - ), + modifier = + Modifier + .clickable( + enabled = enabled && onClick != null, + onClick = { + when (state) { + TriState.DISABLED -> onClick?.invoke(TriState.ENABLED_IS) + TriState.ENABLED_IS -> onClick?.invoke(TriState.ENABLED_NOT) + TriState.ENABLED_NOT -> onClick?.invoke(TriState.DISABLED) + } + }, + ).fillMaxWidth() + .padding( + horizontal = SettingsItemsPaddings.Horizontal, + vertical = SettingsItemsPaddings.Vertical, + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.large), ) { val stateAlpha = if (enabled && onClick != null) 1f else DisabledContentAlpha Icon( - imageVector = when (state) { - TriState.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank - TriState.ENABLED_IS -> Icons.Rounded.CheckBox - TriState.ENABLED_NOT -> Icons.Rounded.DisabledByDefault - }, + imageVector = + when (state) { + TriState.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank + TriState.ENABLED_IS -> Icons.Rounded.CheckBox + TriState.ENABLED_NOT -> Icons.Rounded.DisabledByDefault + }, contentDescription = null, - tint = if (!enabled || state == TriState.DISABLED) { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha) - } else { - when (onClick) { - null -> MaterialTheme.colorScheme.onSurface.copy(alpha = DisabledContentAlpha) - else -> MaterialTheme.colorScheme.primary - } - }, + tint = + if (!enabled || state == TriState.DISABLED) { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha) + } else { + when (onClick) { + null -> MaterialTheme.colorScheme.onSurface.copy(alpha = DisabledContentAlpha) + else -> MaterialTheme.colorScheme.primary + } + }, ) Text( text = label, @@ -306,11 +331,16 @@ fun TriStateItem( } @Composable -fun TextItem(label: String, value: String, onChange: (String) -> Unit) { +fun TextItem( + label: String, + value: String, + onChange: (String) -> Unit, +) { OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = 4.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = 4.dp), label = { Text(text = label) }, value = value, onValueChange = onChange, @@ -319,16 +349,20 @@ fun TextItem(label: String, value: String, onChange: (String) -> Unit) { } @Composable -fun SettingsChipRow(labelRes: StringResource, content: @Composable FlowRowScope.() -> Unit) { +fun SettingsChipRow( + labelRes: StringResource, + content: @Composable FlowRowScope.() -> Unit, +) { Column { HeadingItem(labelRes) FlowRow( - modifier = Modifier.padding( - start = SettingsItemsPaddings.Horizontal, - top = 0.dp, - end = SettingsItemsPaddings.Horizontal, - bottom = SettingsItemsPaddings.Vertical, - ), + modifier = + Modifier.padding( + start = SettingsItemsPaddings.Horizontal, + top = 0.dp, + end = SettingsItemsPaddings.Horizontal, + bottom = SettingsItemsPaddings.Vertical, + ), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), content = content, ) @@ -336,16 +370,20 @@ fun SettingsChipRow(labelRes: StringResource, content: @Composable FlowRowScope. } @Composable -fun SettingsIconGrid(labelRes: StringResource, content: LazyGridScope.() -> Unit) { +fun SettingsIconGrid( + labelRes: StringResource, + content: LazyGridScope.() -> Unit, +) { Column { HeadingItem(labelRes) LazyVerticalGrid( columns = GridCells.Adaptive(128.dp), - modifier = Modifier.padding( - start = SettingsItemsPaddings.Horizontal, - end = SettingsItemsPaddings.Horizontal, - bottom = SettingsItemsPaddings.Vertical, - ), + modifier = + Modifier.padding( + start = SettingsItemsPaddings.Horizontal, + end = SettingsItemsPaddings.Horizontal, + bottom = SettingsItemsPaddings.Vertical, + ), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), content = content, @@ -360,13 +398,14 @@ private fun BaseSettingsItem( onClick: () -> Unit, ) { Row( - modifier = Modifier - .clickable(onClick = onClick) - .fillMaxWidth() - .padding( - horizontal = SettingsItemsPaddings.Horizontal, - vertical = SettingsItemsPaddings.Vertical, - ), + modifier = + Modifier + .clickable(onClick = onClick) + .fillMaxWidth() + .padding( + horizontal = SettingsItemsPaddings.Horizontal, + vertical = SettingsItemsPaddings.Vertical, + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp), ) { diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/TwoPanelBox.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/TwoPanelBox.kt index 8dbc88693a..a0dc7f07c7 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/TwoPanelBox.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/TwoPanelBox.kt @@ -31,15 +31,17 @@ fun TwoPanelBox( val firstWidth = (width / 2).coerceAtMost(450.dp) val secondWidth = width - firstWidth Box( - modifier = Modifier - .align(Alignment.TopStart) - .width(firstWidth + startPadding), + modifier = + Modifier + .align(Alignment.TopStart) + .width(firstWidth + startPadding), content = startContent, ) Box( - modifier = Modifier - .align(Alignment.TopEnd) - .width(secondWidth + endPadding), + modifier = + Modifier + .align(Alignment.TopEnd) + .width(secondWidth + endPadding), content = endContent, ) } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt index 83c847820a..1af238c3cd 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt @@ -77,106 +77,110 @@ fun VerticalFastScroller( val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0 val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0) - val scrollerPlaceable = subcompose("scroller") { - val layoutInfo = listState.layoutInfo - val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount - if (!showScroller) return@subcompose - - val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() } - var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) } - - val dragInteractionSource = remember { MutableInteractionSource() } - val isThumbDragged by dragInteractionSource.collectIsDraggedAsState() - val scrolled = remember { - MutableSharedFlow( - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - } + val scrollerPlaceable = + subcompose("scroller") { + val layoutInfo = listState.layoutInfo + val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount + if (!showScroller) return@subcompose + + val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() } + var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) } + + val dragInteractionSource = remember { MutableInteractionSource() } + val isThumbDragged by dragInteractionSource.collectIsDraggedAsState() + val scrolled = + remember { + MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + } - val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() } - val heightPx = contentHeight.toFloat() - - thumbTopPadding - - thumbBottomPadding - - listState.layoutInfo.afterContentPadding - val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() } - val trackHeightPx = heightPx - thumbHeightPx - - // When thumb dragged - LaunchedEffect(thumbOffsetY) { - if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect - val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx - val scrollItem = layoutInfo.totalItemsCount * scrollRatio - val scrollItemRounded = scrollItem.roundToInt() - val scrollItemSize = layoutInfo.visibleItemsInfo.find { it.index == scrollItemRounded }?.size ?: 0 - val scrollItemOffset = scrollItemSize * (scrollItem - scrollItemRounded) - listState.scrollToItem(index = scrollItemRounded, scrollOffset = scrollItemOffset.roundToInt()) - scrolled.tryEmit(Unit) - } + val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() } + val heightPx = + contentHeight.toFloat() - + thumbTopPadding - + thumbBottomPadding - + listState.layoutInfo.afterContentPadding + val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() } + val trackHeightPx = heightPx - thumbHeightPx + + // When thumb dragged + LaunchedEffect(thumbOffsetY) { + if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect + val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx + val scrollItem = layoutInfo.totalItemsCount * scrollRatio + val scrollItemRounded = scrollItem.roundToInt() + val scrollItemSize = layoutInfo.visibleItemsInfo.find { it.index == scrollItemRounded }?.size ?: 0 + val scrollItemOffset = scrollItemSize * (scrollItem - scrollItemRounded) + listState.scrollToItem(index = scrollItemRounded, scrollOffset = scrollItemOffset.roundToInt()) + scrolled.tryEmit(Unit) + } - // When list scrolled - LaunchedEffect(listState.firstVisibleItemScrollOffset) { - if (listState.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect - val scrollOffset = computeScrollOffset(state = listState) - val scrollRange = computeScrollRange(state = listState) - val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx) - thumbOffsetY = trackHeightPx * proportion + thumbTopPadding - scrolled.tryEmit(Unit) - } + // When list scrolled + LaunchedEffect(listState.firstVisibleItemScrollOffset) { + if (listState.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect + val scrollOffset = computeScrollOffset(state = listState) + val scrollRange = computeScrollRange(state = listState) + val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx) + thumbOffsetY = trackHeightPx * proportion + thumbTopPadding + scrolled.tryEmit(Unit) + } - // Thumb alpha - val alpha = remember { Animatable(0f) } - val isThumbVisible = alpha.value > 0f - LaunchedEffect(scrolled, alpha) { - scrolled - .sample(100) - .collectLatest { - if (thumbAllowed()) { - alpha.snapTo(1f) - alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) - } else { - alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec) + // Thumb alpha + val alpha = remember { Animatable(0f) } + val isThumbVisible = alpha.value > 0f + LaunchedEffect(scrolled, alpha) { + scrolled + .sample(100) + .collectLatest { + if (thumbAllowed()) { + alpha.snapTo(1f) + alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + } else { + alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec) + } } - } - } + } - Box( - modifier = Modifier - .offset { IntOffset(0, thumbOffsetY.roundToInt()) } - .then( - // Recompose opts - if (isThumbVisible && !listState.isScrollInProgress) { - Modifier.draggable( - interactionSource = dragInteractionSource, - orientation = Orientation.Vertical, - state = rememberDraggableState { delta -> - val newOffsetY = thumbOffsetY + delta - thumbOffsetY = newOffsetY.coerceIn( - thumbTopPadding, - thumbTopPadding + trackHeightPx, + Box( + modifier = + Modifier + .offset { IntOffset(0, thumbOffsetY.roundToInt()) } + .then( + // Recompose opts + if (isThumbVisible && !listState.isScrollInProgress) { + Modifier.draggable( + interactionSource = dragInteractionSource, + orientation = Orientation.Vertical, + state = + rememberDraggableState { delta -> + val newOffsetY = thumbOffsetY + delta + thumbOffsetY = + newOffsetY.coerceIn( + thumbTopPadding, + thumbTopPadding + trackHeightPx, + ) + }, ) + } else { + Modifier }, - ) - } else { - Modifier - }, - ) - .then( - // Exclude thumb from gesture area only when needed - if (isThumbVisible && !isThumbDragged && !listState.isScrollInProgress) { - Modifier.systemGestureExclusion() - } else { - Modifier - }, - ) - .height(ThumbLength) - .padding(horizontal = 8.dp) - .padding(end = endContentPadding) - .width(ThumbThickness) - .alpha(alpha.value) - .background(color = thumbColor, shape = ThumbShape), - ) - }.map { it.measure(scrollerConstraints) } + ).then( + // Exclude thumb from gesture area only when needed + if (isThumbVisible && !isThumbDragged && !listState.isScrollInProgress) { + Modifier.systemGestureExclusion() + } else { + Modifier + }, + ).height(ThumbLength) + .padding(horizontal = 8.dp) + .padding(end = endContentPadding) + .width(ThumbThickness) + .alpha(alpha.value) + .background(color = thumbColor, shape = ThumbShape), + ) + }.map { it.measure(scrollerConstraints) } val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0 layout(contentWidth, contentHeight) { @@ -200,13 +204,13 @@ private fun rememberColumnWidthSums( horizontalArrangement, contentPadding, ) { - { - constraints -> + { constraints -> require(constraints.maxWidth != Constraints.Infinity) { "LazyVerticalGrid's width should be bound by parent" } - val horizontalPadding = contentPadding.calculateStartPadding(LayoutDirection.Ltr) + - contentPadding.calculateEndPadding(LayoutDirection.Ltr) + val horizontalPadding = + contentPadding.calculateStartPadding(LayoutDirection.Ltr) + + contentPadding.calculateEndPadding(LayoutDirection.Ltr) val gridWidth = constraints.maxWidth - horizontalPadding.roundToPx() with(columns) { calculateCrossAxisCellSizes( @@ -235,11 +239,12 @@ fun VerticalGridFastScroller( endContentPadding: Dp = Dp.Hairline, content: @Composable () -> Unit, ) { - val slotSizesSums = rememberColumnWidthSums( - columns = columns, - horizontalArrangement = arrangement, - contentPadding = contentPadding, - ) + val slotSizesSums = + rememberColumnWidthSums( + columns = columns, + horizontalArrangement = arrangement, + contentPadding = contentPadding, + ) SubcomposeLayout(modifier = modifier) { constraints -> val contentPlaceable = subcompose("content", content).map { it.measure(constraints) } @@ -247,121 +252,130 @@ fun VerticalGridFastScroller( val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0 val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0) - val scrollerPlaceable = subcompose("scroller") { - val layoutInfo = state.layoutInfo - val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount - if (!showScroller) return@subcompose - val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() } - var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) } - - val dragInteractionSource = remember { MutableInteractionSource() } - val isThumbDragged by dragInteractionSource.collectIsDraggedAsState() - val scrolled = remember { - MutableSharedFlow( - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - } - - val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() } - val heightPx = contentHeight.toFloat() - - thumbTopPadding - - thumbBottomPadding - - state.layoutInfo.afterContentPadding - val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() } - val trackHeightPx = heightPx - thumbHeightPx - - val columnCount = remember { slotSizesSums(constraints).size } - - // When thumb dragged - LaunchedEffect(thumbOffsetY) { - if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect - val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx - val scrollItem = layoutInfo.totalItemsCount * scrollRatio - // I can't think of anything else rn but this'll do - val scrollItemWhole = scrollItem.toInt() - val columnNum = ((scrollItemWhole + 1) % columnCount).takeIf { it != 0 } ?: columnCount - val scrollItemFraction = if (scrollItemWhole == 0) scrollItem else scrollItem % scrollItemWhole - val offsetPerItem = 1f / columnCount - val offsetRatio = (offsetPerItem * scrollItemFraction) + (offsetPerItem * (columnNum - 1)) - - // TODO: Sometimes item height is not available when scrolling up - val scrollItemSize = (1..columnCount).maxOf { num -> - val actualIndex = if (num != columnNum) { - scrollItemWhole + num - columnCount - } else { - scrollItemWhole + val scrollerPlaceable = + subcompose("scroller") { + val layoutInfo = state.layoutInfo + val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount + if (!showScroller) return@subcompose + val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() } + var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) } + + val dragInteractionSource = remember { MutableInteractionSource() } + val isThumbDragged by dragInteractionSource.collectIsDraggedAsState() + val scrolled = + remember { + MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) } - layoutInfo.visibleItemsInfo.find { it.index == actualIndex }?.size?.height ?: 0 - } - val scrollItemOffset = scrollItemSize * offsetRatio - state.scrollToItem(index = scrollItemWhole, scrollOffset = scrollItemOffset.roundToInt()) - scrolled.tryEmit(Unit) - } + val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() } + val heightPx = + contentHeight.toFloat() - + thumbTopPadding - + thumbBottomPadding - + state.layoutInfo.afterContentPadding + val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() } + val trackHeightPx = heightPx - thumbHeightPx + + val columnCount = remember { slotSizesSums(constraints).size } + + // When thumb dragged + LaunchedEffect(thumbOffsetY) { + if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect + val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx + val scrollItem = layoutInfo.totalItemsCount * scrollRatio + // I can't think of anything else rn but this'll do + val scrollItemWhole = scrollItem.toInt() + val columnNum = ((scrollItemWhole + 1) % columnCount).takeIf { it != 0 } ?: columnCount + val scrollItemFraction = if (scrollItemWhole == 0) scrollItem else scrollItem % scrollItemWhole + val offsetPerItem = 1f / columnCount + val offsetRatio = (offsetPerItem * scrollItemFraction) + (offsetPerItem * (columnNum - 1)) + + // TODO: Sometimes item height is not available when scrolling up + val scrollItemSize = + (1..columnCount).maxOf { num -> + val actualIndex = + if (num != columnNum) { + scrollItemWhole + num - columnCount + } else { + scrollItemWhole + } + layoutInfo.visibleItemsInfo + .find { it.index == actualIndex } + ?.size + ?.height ?: 0 + } + val scrollItemOffset = scrollItemSize * offsetRatio - // When list scrolled - LaunchedEffect(state.firstVisibleItemScrollOffset) { - if (state.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect - val scrollOffset = computeScrollOffset(state = state) - val scrollRange = computeScrollRange(state = state) - val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx) - thumbOffsetY = trackHeightPx * proportion + thumbTopPadding - scrolled.tryEmit(Unit) - } + state.scrollToItem(index = scrollItemWhole, scrollOffset = scrollItemOffset.roundToInt()) + scrolled.tryEmit(Unit) + } + + // When list scrolled + LaunchedEffect(state.firstVisibleItemScrollOffset) { + if (state.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect + val scrollOffset = computeScrollOffset(state = state) + val scrollRange = computeScrollRange(state = state) + val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx) + thumbOffsetY = trackHeightPx * proportion + thumbTopPadding + scrolled.tryEmit(Unit) + } - // Thumb alpha - val alpha = remember { Animatable(0f) } - val isThumbVisible = alpha.value > 0f - LaunchedEffect(scrolled, alpha) { - scrolled - .sample(100) - .collectLatest { - if (thumbAllowed()) { - alpha.snapTo(1f) - alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) - } else { - alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec) + // Thumb alpha + val alpha = remember { Animatable(0f) } + val isThumbVisible = alpha.value > 0f + LaunchedEffect(scrolled, alpha) { + scrolled + .sample(100) + .collectLatest { + if (thumbAllowed()) { + alpha.snapTo(1f) + alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + } else { + alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec) + } } - } - } + } - Box( - modifier = Modifier - .offset { IntOffset(0, thumbOffsetY.roundToInt()) } - .then( - // Recompose opts - if (isThumbVisible && !state.isScrollInProgress) { - Modifier.draggable( - interactionSource = dragInteractionSource, - orientation = Orientation.Vertical, - state = rememberDraggableState { delta -> - val newOffsetY = thumbOffsetY + delta - thumbOffsetY = newOffsetY.coerceIn( - thumbTopPadding, - thumbTopPadding + trackHeightPx, + Box( + modifier = + Modifier + .offset { IntOffset(0, thumbOffsetY.roundToInt()) } + .then( + // Recompose opts + if (isThumbVisible && !state.isScrollInProgress) { + Modifier.draggable( + interactionSource = dragInteractionSource, + orientation = Orientation.Vertical, + state = + rememberDraggableState { delta -> + val newOffsetY = thumbOffsetY + delta + thumbOffsetY = + newOffsetY.coerceIn( + thumbTopPadding, + thumbTopPadding + trackHeightPx, + ) + }, ) + } else { + Modifier + }, + ).then( + // Exclude thumb from gesture area only when needed + if (isThumbVisible && !isThumbDragged && !state.isScrollInProgress) { + Modifier.systemGestureExclusion() + } else { + Modifier }, - ) - } else { - Modifier - }, - ) - .then( - // Exclude thumb from gesture area only when needed - if (isThumbVisible && !isThumbDragged && !state.isScrollInProgress) { - Modifier.systemGestureExclusion() - } else { - Modifier - }, - ) - .height(ThumbLength) - .padding(end = endContentPadding) - .width(ThumbThickness) - .alpha(alpha.value) - .background(color = thumbColor, shape = ThumbShape), - ) - }.map { it.measure(scrollerConstraints) } + ).height(ThumbLength) + .padding(end = endContentPadding) + .width(ThumbThickness) + .alpha(alpha.value) + .background(color = thumbColor, shape = ThumbShape), + ) + }.map { it.measure(scrollerConstraints) } val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0 layout(contentWidth, contentHeight) { @@ -403,8 +417,9 @@ private fun computeScrollRange(state: LazyGridState): Int { private fun computeScrollOffset(state: LazyListState): Int { if (state.layoutInfo.totalItemsCount == 0) return 0 val visibleItems = state.layoutInfo.visibleItemsInfo - val startChild = visibleItems - .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!! + val startChild = + visibleItems + .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!! val endChild = visibleItems.last() val minPosition = min(startChild.index, endChild.index) val maxPosition = max(startChild.index, endChild.index) @@ -419,8 +434,9 @@ private fun computeScrollOffset(state: LazyListState): Int { private fun computeScrollRange(state: LazyListState): Int { if (state.layoutInfo.totalItemsCount == 0) return 0 val visibleItems = state.layoutInfo.visibleItemsInfo - val startChild = visibleItems - .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!! + val startChild = + visibleItems + .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!! val endChild = visibleItems.last() val laidOutArea = endChild.bottom - startChild.top val laidOutRange = abs(startChild.index - endChild.index) + 1 @@ -434,13 +450,15 @@ object Scroller { private val ThumbLength = 48.dp private val ThumbThickness = 12.dp private val ThumbShape = RoundedCornerShape(ThumbThickness / 2) -private val FadeOutAnimationSpec = tween( - durationMillis = ViewConfiguration.getScrollBarFadeDuration(), - delayMillis = 2000, -) -private val ImmediateFadeOutAnimationSpec = tween( - durationMillis = ViewConfiguration.getScrollBarFadeDuration(), -) +private val FadeOutAnimationSpec = + tween( + durationMillis = ViewConfiguration.getScrollBarFadeDuration(), + delayMillis = 2000, + ) +private val ImmediateFadeOutAnimationSpec = + tween( + durationMillis = ViewConfiguration.getScrollBarFadeDuration(), + ) private val LazyListItemInfo.top: Int get() = offset diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt index 9cbb156c64..caf335e52e 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt @@ -132,9 +132,10 @@ private fun WheelPicker( } Box( - modifier = modifier - .height(size.height) - .width(size.width), + modifier = + modifier + .height(size.height) + .width(size.width), contentAlignment = Alignment.Center, ) { backgroundContent?.invoke(size) @@ -148,61 +149,66 @@ private fun WheelPicker( val scope = rememberCoroutineScope() BasicTextField( - modifier = Modifier - .align(Alignment.Center) - .showSoftKeyboard(true) - .clearFocusOnSoftKeyboardHide { - scope.launch { - items - .indexOfFirst { it.toString() == value.text } - .takeIf { it >= 0 } - ?.apply { - internalOnSelectionChanged(this) - lazyListState.scrollToItem(this) - } + modifier = + Modifier + .align(Alignment.Center) + .showSoftKeyboard(true) + .clearFocusOnSoftKeyboardHide { + scope.launch { + items + .indexOfFirst { it.toString() == value.text } + .takeIf { it >= 0 } + ?.apply { + internalOnSelectionChanged(this) + lazyListState.scrollToItem(this) + } - showManualInput = false - } - }, + showManualInput = false + } + }, value = value, onValueChange = { value = it }, singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = manualInputType!!, - imeAction = ImeAction.Done, - ), - textStyle = MaterialTheme.typography.titleMedium + - TextStyle( - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, + keyboardOptions = + KeyboardOptions( + keyboardType = manualInputType!!, + imeAction = ImeAction.Done, ), + textStyle = + MaterialTheme.typography.titleMedium + + TextStyle( + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ), cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), ) } else { LazyColumn( - modifier = Modifier - .let { - if (manualInputType != null) { - it.clickableNoIndication { showManualInput = true } - } else { - it - } - }, + modifier = + Modifier + .let { + if (manualInputType != null) { + it.clickableNoIndication { showManualInput = true } + } else { + it + } + }, state = lazyListState, contentPadding = PaddingValues(vertical = size.height / RowCount * ((RowCount - 1) / 2)), flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), ) { itemsIndexed(items) { index, item -> Box( - modifier = Modifier - .height(size.height / RowCount) - .width(size.width) - .alpha( - calculateAnimatedAlpha( - lazyListState = lazyListState, - index = index, + modifier = + Modifier + .height(size.height / RowCount) + .width(size.width) + .alpha( + calculateAnimatedAlpha( + lazyListState = lazyListState, + index = index, + ), ), - ), contentAlignment = Alignment.Center, ) { itemContent(item) @@ -232,7 +238,9 @@ private fun calculateAnimatedAlpha( index: Int, ): Float { val distanceToIndexSnap = lazyListState.distanceToSnapForIndex(index).absoluteValue - val viewPortHeight = lazyListState.layoutInfo.viewportSize.height.toFloat() + val viewPortHeight = + lazyListState.layoutInfo.viewportSize.height + .toFloat() val singleViewPortHeight = viewPortHeight / RowCount return if (distanceToIndexSnap in 0..singleViewPortHeight.toInt()) { 1.2f - (distanceToIndexSnap / singleViewPortHeight) @@ -241,18 +249,18 @@ private fun calculateAnimatedAlpha( } } -private fun calculateSnappedItemIndex(lazyListState: LazyListState): Int { - return lazyListState.layoutInfo.visibleItemsInfo +private fun calculateSnappedItemIndex(lazyListState: LazyListState): Int = + lazyListState.layoutInfo.visibleItemsInfo .maxBy { calculateAnimatedAlpha(lazyListState, it.index) } .index -} object WheelPickerDefaults { @Composable fun Background(size: DpSize) { androidx.compose.material3.Surface( - modifier = Modifier - .size(size.width, size.height / RowCount), + modifier = + Modifier + .size(size.width, size.height / RowCount), shape = RoundedCornerShape(MaterialTheme.padding.medium), color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt index 3db94c0183..cfe414d3f5 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt @@ -48,13 +48,13 @@ fun AlertDialogContent( } Box( - modifier = Modifier - .padding( - start = DialogPadding, - end = DialogPadding, - bottom = DialogPadding, - ) - .align(Alignment.End), + modifier = + Modifier + .padding( + start = DialogPadding, + end = DialogPadding, + bottom = DialogPadding, + ).align(Alignment.End), ) { CompositionLocalProvider( LocalContentColor provides MaterialTheme.colorScheme.primary, @@ -76,18 +76,19 @@ fun AlertDialogContent( content: @Composable (ColumnScope.() -> Unit)? = null, ) { Column( - modifier = modifier - .sizeIn(minWidth = MinWidth, maxWidth = MaxWidth), + modifier = + modifier + .sizeIn(minWidth = MinWidth, maxWidth = MaxWidth), ) { if (icon != null || title != null) { Column( - modifier = Modifier - .padding( - start = DialogPadding, - top = DialogPadding, - end = DialogPadding, - ) - .fillMaxWidth(), + modifier = + Modifier + .padding( + start = DialogPadding, + top = DialogPadding, + end = DialogPadding, + ).fillMaxWidth(), ) { icon?.let { CompositionLocalProvider( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Button.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Button.kt index 5e65c89a2e..6e9fd3e6f0 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Button.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Button.kt @@ -56,12 +56,13 @@ fun TextButton( elevation: ButtonElevation? = null, shape: Shape = M3ButtonDefaults.textShape, border: BorderStroke? = null, - colors: ButtonColors = ButtonColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.primary, - disabledContainerColor = Color.Transparent, - disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), - ), + colors: ButtonColors = + ButtonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.primary, + disabledContainerColor = Color.Transparent, + disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + ), contentPadding: PaddingValues = M3ButtonDefaults.TextButtonContentPadding, content: @Composable RowScope.() -> Unit, ) = Button( @@ -116,11 +117,11 @@ fun Button( CompositionLocalProvider(LocalContentColor provides contentColor) { ProvideTextStyle(value = MaterialTheme.typography.labelLarge) { Row( - Modifier.defaultMinSize( - minWidth = M3ButtonDefaults.MinWidth, - minHeight = M3ButtonDefaults.MinHeight, - ) - .padding(contentPadding), + Modifier + .defaultMinSize( + minWidth = M3ButtonDefaults.MinWidth, + minHeight = M3ButtonDefaults.MinHeight, + ).padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, @@ -146,12 +147,13 @@ object ButtonDefaults { contentColor: Color = MaterialTheme.colorScheme.onPrimary, disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), - ): ButtonColors = ButtonColors( - containerColor = containerColor, - contentColor = contentColor, - disabledContainerColor = disabledContainerColor, - disabledContentColor = disabledContentColor, - ) + ): ButtonColors = + ButtonColors( + containerColor = containerColor, + contentColor = contentColor, + disabledContainerColor = disabledContainerColor, + disabledContentColor = disabledContentColor, + ) /** * Creates a [ButtonElevation] that will animate between the provided values according to the @@ -171,13 +173,14 @@ object ButtonDefaults { focusedElevation: Dp = 0.dp, hoveredElevation: Dp = 1.dp, disabledElevation: Dp = 0.dp, - ): ButtonElevation = ButtonElevation( - defaultElevation = defaultElevation, - pressedElevation = pressedElevation, - focusedElevation = focusedElevation, - hoveredElevation = hoveredElevation, - disabledElevation = disabledElevation, - ) + ): ButtonElevation = + ButtonElevation( + defaultElevation = defaultElevation, + pressedElevation = pressedElevation, + focusedElevation = focusedElevation, + hoveredElevation = hoveredElevation, + disabledElevation = disabledElevation, + ) } /** @@ -209,9 +212,10 @@ class ButtonElevation internal constructor( * @param interactionSource the [InteractionSource] for this button */ @Composable - internal fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State { - return animateElevation(enabled = enabled, interactionSource = interactionSource) - } + internal fun tonalElevation( + enabled: Boolean, + interactionSource: InteractionSource, + ): State = animateElevation(enabled = enabled, interactionSource = interactionSource) /** * Represents the shadow elevation used in a button, depending on its [enabled] state and @@ -228,9 +232,7 @@ class ButtonElevation internal constructor( internal fun shadowElevation( enabled: Boolean, interactionSource: InteractionSource, - ): State { - return animateElevation(enabled = enabled, interactionSource = interactionSource) - } + ): State = animateElevation(enabled = enabled, interactionSource = interactionSource) @Composable private fun animateElevation( @@ -287,12 +289,13 @@ class ButtonElevation internal constructor( LaunchedEffect(target) { animatable.snapTo(target) } } else { LaunchedEffect(target) { - val lastInteraction = when (animatable.targetValue) { - pressedElevation -> PressInteraction.Press(Offset.Zero) - hoveredElevation -> HoverInteraction.Enter() - focusedElevation -> FocusInteraction.Focus() - else -> null - } + val lastInteraction = + when (animatable.targetValue) { + pressedElevation -> PressInteraction.Press(Offset.Zero) + hoveredElevation -> HoverInteraction.Enter() + focusedElevation -> FocusInteraction.Focus() + else -> null + } animatable.animateElevation( from = lastInteraction, to = interaction, @@ -347,9 +350,8 @@ class ButtonColors internal constructor( * @param enabled whether the button is enabled */ @Composable - internal fun containerColor(enabled: Boolean): State { - return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor) - } + internal fun containerColor(enabled: Boolean): State = + rememberUpdatedState(if (enabled) containerColor else disabledContainerColor) /** * Represents the content color for this button, depending on [enabled]. @@ -357,9 +359,8 @@ class ButtonColors internal constructor( * @param enabled whether the button is enabled */ @Composable - internal fun contentColor(enabled: Boolean): State { - return rememberUpdatedState(if (enabled) contentColor else disabledContentColor) - } + internal fun contentColor(enabled: Boolean): State = + rememberUpdatedState(if (enabled) contentColor else disabledContentColor) override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Constants.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Constants.kt index d86bece61e..0d342da276 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Constants.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Constants.kt @@ -10,7 +10,6 @@ const val ReadItemAlpha = .38f const val SecondaryItemAlpha = .78f class Padding { - val extraLarge = 32.dp val large = 24.dp diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/FloatingActionButton.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/FloatingActionButton.kt index fd1fa8c4be..f5daeb8c1a 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/FloatingActionButton.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/FloatingActionButton.kt @@ -95,31 +95,39 @@ private val ExtendedFabIconSize = 24.0.dp private val ExtendedFabIconPadding = 12.dp private val ExtendedFabTextPadding = 20.dp -private val ExtendedFabCollapseAnimation = fadeOut( - animationSpec = tween( - durationMillis = 100, - easing = EasingLinearCubicBezier, - ), -) + shrinkHorizontally( - animationSpec = tween( - durationMillis = 500, - easing = EasingEmphasizedCubicBezier, - ), - shrinkTowards = Alignment.Start, -) +private val ExtendedFabCollapseAnimation = + fadeOut( + animationSpec = + tween( + durationMillis = 100, + easing = EasingLinearCubicBezier, + ), + ) + + shrinkHorizontally( + animationSpec = + tween( + durationMillis = 500, + easing = EasingEmphasizedCubicBezier, + ), + shrinkTowards = Alignment.Start, + ) -private val ExtendedFabExpandAnimation = fadeIn( - animationSpec = tween( - durationMillis = 200, - delayMillis = 100, - easing = EasingLinearCubicBezier, - ), -) + expandHorizontally( - animationSpec = tween( - durationMillis = 500, - easing = EasingEmphasizedCubicBezier, - ), - expandFrom = Alignment.Start, -) +private val ExtendedFabExpandAnimation = + fadeIn( + animationSpec = + tween( + durationMillis = 200, + delayMillis = 100, + easing = EasingLinearCubicBezier, + ), + ) + + expandHorizontally( + animationSpec = + tween( + durationMillis = 500, + easing = EasingEmphasizedCubicBezier, + ), + expandFrom = Alignment.Start, + ) private val FabContainerWidth = 56.0.dp diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconToggleButton.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconToggleButton.kt index d00cc86574..bacb50e30e 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconToggleButton.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconToggleButton.kt @@ -27,15 +27,17 @@ fun IconToggleButton( FilledIconToggleButton( checked = checked, onCheckedChange = onCheckedChange, - modifier = modifier - .height(48.dp), + modifier = + modifier + .height(48.dp), ) { Row( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(MaterialTheme.padding.small), + modifier = + Modifier + .fillMaxWidth() + .padding(MaterialTheme.padding.small), ) { Icon( imageVector = imageVector, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationBar.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationBar.kt index 2f7f431e77..ec753eb9a5 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationBar.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationBar.kt @@ -37,11 +37,12 @@ fun NavigationBar( modifier = modifier, ) { Row( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(windowInsets) - .height(80.dp) - .selectableGroup(), + modifier = + Modifier + .fillMaxWidth() + .windowInsetsPadding(windowInsets) + .height(80.dp) + .selectableGroup(), content = content, ) } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt index e33f6e4fea..319f4e140d 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt @@ -48,10 +48,11 @@ fun NavigationRail( .padding(vertical = MaterialTheme.padding.extraSmall) .selectableGroup(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.extraSmall, - alignment = Alignment.CenterVertically, - ), + verticalArrangement = + Arrangement.spacedBy( + MaterialTheme.padding.extraSmall, + alignment = Alignment.CenterVertically, + ), ) { if (header != null) { header() diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt index 08b10cdaaa..438fd74f5d 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt @@ -30,20 +30,22 @@ fun PullRefresh( ) { val state = rememberPullToRefreshState() Box( - modifier = modifier - .pullToRefresh( - isRefreshing = refreshing, - state = state, - enabled = enabled, - onRefresh = onRefresh, - ), + modifier = + modifier + .pullToRefresh( + isRefreshing = refreshing, + state = state, + enabled = enabled, + onRefresh = onRefresh, + ), ) { content() PullToRefreshDefaults.Indicator( - modifier = Modifier - .align(Alignment.TopCenter) - .padding(indicatorPadding), + modifier = + Modifier + .align(Alignment.TopCenter) + .padding(indicatorPadding), isRefreshing = refreshing, state = state, containerColor = MaterialTheme.colorScheme.surfaceVariant, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt index 6420084a75..7ddbf7b2c8 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt @@ -99,9 +99,10 @@ import kotlin.math.max @Composable fun Scaffold( modifier: Modifier = Modifier, - topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior( - rememberTopAppBarState(), - ), + topBarScrollBehavior: TopAppBarScrollBehavior = + TopAppBarDefaults.pinnedScrollBehavior( + rememberTopAppBarState(), + ), topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {}, bottomBar: @Composable () -> Unit = {}, startBar: @Composable () -> Unit = {}, @@ -116,14 +117,15 @@ fun Scaffold( // Tachiyomi: Handle consumed window insets val remainingWindowInsets = remember { MutableWindowInsets() } androidx.compose.material3.Surface( - modifier = Modifier - .nestedScroll(topBarScrollBehavior.nestedScrollConnection) - .onConsumedWindowInsetsChanged { - remainingWindowInsets.insets = contentWindowInsets.exclude( - it, - ) - } - .then(modifier), + modifier = + Modifier + .nestedScroll(topBarScrollBehavior.nestedScrollConnection) + .onConsumedWindowInsetsChanged { + remainingWindowInsets.insets = + contentWindowInsets.exclude( + it, + ) + }.then(modifier), color = containerColor, contentColor = contentColor, ) { @@ -182,33 +184,37 @@ private fun ScaffoldLayout( val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout) // Tachiyomi: Add startBar slot for Navigation Rail - val startBarPlaceables = subcompose(ScaffoldLayoutContent.StartBar, startBar).fastMap { - it.measure(looseConstraints) - } + val startBarPlaceables = + subcompose(ScaffoldLayoutContent.StartBar, startBar).fastMap { + it.measure(looseConstraints) + } val startBarWidth = startBarPlaceables.fastMaxBy { it.width }?.width ?: 0 // Tachiyomi: layoutWidth after horizontal insets val insetLayoutWidth = layoutWidth - leftInset - rightInset - startBarWidth - val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap { - it.measure(topBarConstraints) - } + val topBarPlaceables = + subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap { + it.measure(topBarConstraints) + } val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0 - val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap { - it.measure(looseConstraints) - } + val snackbarPlaceables = + subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap { + it.measure(looseConstraints) + } val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0 val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0 // Tachiyomi: Calculate insets for snackbar placement offset - val snackbarLeft = if (snackbarPlaceables.isNotEmpty()) { - (insetLayoutWidth - snackbarWidth) / 2 + leftInset - } else { - 0 - } + val snackbarLeft = + if (snackbarPlaceables.isNotEmpty()) { + (insetLayoutWidth - snackbarWidth) / 2 + leftInset + } else { + 0 + } val fabPlaceables = subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable -> @@ -218,71 +224,80 @@ private fun ScaffoldLayout( val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0 val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0 - val fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) { - // FAB distance from the left of the layout, taking into account LTR / RTL - // Tachiyomi: Calculate insets for fab placement offset - val fabLeftOffset = if (fabPosition == FabPosition.End) { - if (layoutDirection == LayoutDirection.Ltr) { - layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset - } else { - FabSpacing.roundToPx() + leftInset - } + val fabPlacement = + if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) { + // FAB distance from the left of the layout, taking into account LTR / RTL + // Tachiyomi: Calculate insets for fab placement offset + val fabLeftOffset = + if (fabPosition == FabPosition.End) { + if (layoutDirection == LayoutDirection.Ltr) { + layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset + } else { + FabSpacing.roundToPx() + leftInset + } + } else { + leftInset + ((insetLayoutWidth - fabWidth) / 2) + } + + FabPlacement( + left = fabLeftOffset, + width = fabWidth, + height = fabHeight, + ) } else { - leftInset + ((insetLayoutWidth - fabWidth) / 2) + null } - FabPlacement( - left = fabLeftOffset, - width = fabWidth, - height = fabHeight, - ) - } else { - null - } - - val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) { - bottomBar() - }.fastMap { it.measure(looseConstraints) } + val bottomBarPlaceables = + subcompose(ScaffoldLayoutContent.BottomBar) { + bottomBar() + }.fastMap { it.measure(looseConstraints) } - val bottomBarHeight = bottomBarPlaceables - .fastMaxBy { it.height } - ?.height - ?.takeIf { it != 0 } - val fabOffsetFromBottom = fabPlacement?.let { - max(bottomBarHeight ?: 0, bottomInset) + it.height + FabSpacing.roundToPx() - } + val bottomBarHeight = + bottomBarPlaceables + .fastMaxBy { it.height } + ?.height + ?.takeIf { it != 0 } + val fabOffsetFromBottom = + fabPlacement?.let { + max(bottomBarHeight ?: 0, bottomInset) + it.height + FabSpacing.roundToPx() + } - val snackbarOffsetFromBottom = if (snackbarHeight != 0) { - snackbarHeight + (fabOffsetFromBottom ?: max(bottomBarHeight ?: 0, bottomInset)) - } else { - 0 - } + val snackbarOffsetFromBottom = + if (snackbarHeight != 0) { + snackbarHeight + (fabOffsetFromBottom ?: max(bottomBarHeight ?: 0, bottomInset)) + } else { + 0 + } - val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) { - val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout) - val fabOffsetDp = fabOffsetFromBottom?.toDp() ?: 0.dp - val bottomBarHeightPx = bottomBarHeight ?: 0 - val innerPadding = PaddingValues( - top = - if (topBarPlaceables.isEmpty()) { - insets.calculateTopPadding() - } else { - topBarHeight.toDp() - }, - bottom = // Tachiyomi: Also take account of fab height when providing inner padding - if (bottomBarPlaceables.isEmpty() || bottomBarHeightPx == 0) { - max(insets.calculateBottomPadding(), fabOffsetDp) - } else { - max(bottomBarHeightPx.toDp(), fabOffsetDp) - }, - start = max( - insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection), - startBarWidth.toDp(), - ), - end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection), - ) - content(innerPadding) - }.fastMap { it.measure(looseConstraints) } + val bodyContentPlaceables = + subcompose(ScaffoldLayoutContent.MainContent) { + val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout) + val fabOffsetDp = fabOffsetFromBottom?.toDp() ?: 0.dp + val bottomBarHeightPx = bottomBarHeight ?: 0 + val innerPadding = + PaddingValues( + top = + if (topBarPlaceables.isEmpty()) { + insets.calculateTopPadding() + } else { + topBarHeight.toDp() + }, + bottom = // Tachiyomi: Also take account of fab height when providing inner padding + if (bottomBarPlaceables.isEmpty() || bottomBarHeightPx == 0) { + max(insets.calculateBottomPadding(), fabOffsetDp) + } else { + max(bottomBarHeightPx.toDp(), fabOffsetDp) + }, + start = + max( + insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection), + startBarWidth.toDp(), + ), + end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection), + ) + content(innerPadding) + }.fastMap { it.measure(looseConstraints) } // Placing to control drawing order to match default elevation of each placeable @@ -318,7 +333,9 @@ private fun ScaffoldLayout( */ @ExperimentalMaterial3Api @JvmInline -value class FabPosition internal constructor(@Suppress("unused") private val value: Int) { +value class FabPosition internal constructor( + @Suppress("unused") private val value: Int, +) { companion object { /** * Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it @@ -333,12 +350,11 @@ value class FabPosition internal constructor(@Suppress("unused") private val val val End = FabPosition(1) } - override fun toString(): String { - return when (this) { + override fun toString(): String = + when (this) { Center -> "FabPosition.Center" else -> "FabPosition.End" } - } } /** diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Surface.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Surface.kt index 0e857ef75c..d349dd2d0a 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Surface.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Surface.kt @@ -57,25 +57,26 @@ fun Surface( LocalAbsoluteTonalElevation provides absoluteElevation, ) { Box( - modifier = modifier - .minimumInteractiveComponentSize() - .surface( - shape = shape, - backgroundColor = surfaceColorAtElevation( - color = color, - elevation = absoluteElevation, + modifier = + modifier + .minimumInteractiveComponentSize() + .surface( + shape = shape, + backgroundColor = + surfaceColorAtElevation( + color = color, + elevation = absoluteElevation, + ), + border = border, + shadowElevation = shadowElevation, + ).combinedClickable( + interactionSource = interactionSource, + indication = ripple(), + enabled = enabled, + role = Role.Button, + onLongClick = onLongClick, + onClick = onClick, ), - border = border, - shadowElevation = shadowElevation, - ) - .combinedClickable( - interactionSource = interactionSource, - indication = ripple(), - enabled = enabled, - role = Role.Button, - onLongClick = onLongClick, - onClick = onClick, - ), propagateMinConstraints = true, ) { content() @@ -96,13 +97,15 @@ private fun Modifier.surface( @Composable @ReadOnlyComposable -private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color { - return if (color == MaterialTheme.colorScheme.surface) { +private fun surfaceColorAtElevation( + color: Color, + elevation: Dp, +): Color = + if (color == MaterialTheme.colorScheme.surface) { MaterialTheme.colorScheme.surfaceColorAtElevation(elevation) } else { color } -} private fun ColorScheme.surfaceColorAtElevation(elevation: Dp): Color { if (elevation == 0.dp) return surface diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt index 30f6766d88..f74ff909c2 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt @@ -11,7 +11,10 @@ import androidx.compose.ui.unit.sp import tachiyomi.presentation.core.components.Pill @Composable -fun TabText(text: String, badgeCount: Int? = null) { +fun TabText( + text: String, + badgeCount: Int? = null, +) { val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f Row( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/i18n/Localize.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/i18n/Localize.kt index 93013ba2bf..f909294a37 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/i18n/Localize.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/i18n/Localize.kt @@ -10,24 +10,26 @@ import tachiyomi.core.common.i18n.stringResource @Composable @ReadOnlyComposable -fun stringResource(resource: StringResource): String { - return LocalContext.current.stringResource(resource) -} +fun stringResource(resource: StringResource): String = LocalContext.current.stringResource(resource) @Composable @ReadOnlyComposable -fun stringResource(resource: StringResource, vararg args: Any): String { - return LocalContext.current.stringResource(resource, *args) -} +fun stringResource( + resource: StringResource, + vararg args: Any, +): String = LocalContext.current.stringResource(resource, *args) @Composable @ReadOnlyComposable -fun pluralStringResource(resource: PluralsResource, count: Int): String { - return LocalContext.current.pluralStringResource(resource, count) -} +fun pluralStringResource( + resource: PluralsResource, + count: Int, +): String = LocalContext.current.pluralStringResource(resource, count) @Composable @ReadOnlyComposable -fun pluralStringResource(resource: PluralsResource, count: Int, vararg args: Any): String { - return LocalContext.current.pluralStringResource(resource, count, *args) -} +fun pluralStringResource( + resource: PluralsResource, + count: Int, + vararg args: Any, +): String = LocalContext.current.pluralStringResource(resource, count, *args) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Discord.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Discord.kt index f9c80ff8e4..905591bf5e 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Discord.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Discord.kt @@ -15,62 +15,69 @@ val CustomIcons.Discord: ImageVector if (_discord != null) { return _discord!! } - _discord = Builder( - name = "Discord", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, - viewportWidth = 24.0f, viewportHeight = 24.0f, - ).apply { - path( - fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, - strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, - pathFillType = NonZero, - ) { - moveTo(20.317f, 4.3698f) - arcToRelative(19.7913f, 19.7913f, 0.0f, false, false, -4.8851f, -1.5152f) - arcToRelative(0.0741f, 0.0741f, 0.0f, false, false, -0.0785f, 0.0371f) - curveToRelative(-0.211f, 0.3753f, -0.4447f, 0.8648f, -0.6083f, 1.2495f) - curveToRelative(-1.8447f, -0.2762f, -3.68f, -0.2762f, -5.4868f, 0.0f) - curveToRelative(-0.1636f, -0.3933f, -0.4058f, -0.8742f, -0.6177f, -1.2495f) - arcToRelative(0.077f, 0.077f, 0.0f, false, false, -0.0785f, -0.037f) - arcToRelative(19.7363f, 19.7363f, 0.0f, false, false, -4.8852f, 1.515f) - arcToRelative(0.0699f, 0.0699f, 0.0f, false, false, -0.0321f, 0.0277f) - curveTo(0.5334f, 9.0458f, -0.319f, 13.5799f, 0.0992f, 18.0578f) - arcToRelative(0.0824f, 0.0824f, 0.0f, false, false, 0.0312f, 0.0561f) - curveToRelative(2.0528f, 1.5076f, 4.0413f, 2.4228f, 5.9929f, 3.0294f) - arcToRelative(0.0777f, 0.0777f, 0.0f, false, false, 0.0842f, -0.0276f) - curveToRelative(0.4616f, -0.6304f, 0.8731f, -1.2952f, 1.226f, -1.9942f) - arcToRelative(0.076f, 0.076f, 0.0f, false, false, -0.0416f, -0.1057f) - curveToRelative(-0.6528f, -0.2476f, -1.2743f, -0.5495f, -1.8722f, -0.8923f) - arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0076f, -0.1277f) - curveToRelative(0.1258f, -0.0943f, 0.2517f, -0.1923f, 0.3718f, -0.2914f) - arcToRelative(0.0743f, 0.0743f, 0.0f, false, true, 0.0776f, -0.0105f) - curveToRelative(3.9278f, 1.7933f, 8.18f, 1.7933f, 12.0614f, 0.0f) - arcToRelative(0.0739f, 0.0739f, 0.0f, false, true, 0.0785f, 0.0095f) - curveToRelative(0.1202f, 0.099f, 0.246f, 0.1981f, 0.3728f, 0.2924f) - arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0066f, 0.1276f) - arcToRelative(12.2986f, 12.2986f, 0.0f, false, true, -1.873f, 0.8914f) - arcToRelative(0.0766f, 0.0766f, 0.0f, false, false, -0.0407f, 0.1067f) - curveToRelative(0.3604f, 0.698f, 0.7719f, 1.3628f, 1.225f, 1.9932f) - arcToRelative(0.076f, 0.076f, 0.0f, false, false, 0.0842f, 0.0286f) - curveToRelative(1.961f, -0.6067f, 3.9495f, -1.5219f, 6.0023f, -3.0294f) - arcToRelative(0.077f, 0.077f, 0.0f, false, false, 0.0313f, -0.0552f) - curveToRelative(0.5004f, -5.177f, -0.8382f, -9.6739f, -3.5485f, -13.6604f) - arcToRelative(0.061f, 0.061f, 0.0f, false, false, -0.0312f, -0.0286f) - close() - moveTo(8.02f, 15.3312f) - curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f) - curveToRelative(0.0f, -1.3332f, 0.9555f, -2.4189f, 2.157f, -2.4189f) - curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f) - curveToRelative(0.0f, 1.3332f, -0.9555f, 2.4189f, -2.1569f, 2.4189f) - close() - moveTo(15.9948f, 15.3312f) - curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f) - curveToRelative(0.0f, -1.3332f, 0.9554f, -2.4189f, 2.1569f, -2.4189f) - curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f) - curveToRelative(0.0f, 1.3332f, -0.946f, 2.4189f, -2.1568f, 2.4189f) - close() - } - } - .build() + _discord = + Builder( + name = "Discord", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(20.317f, 4.3698f) + arcToRelative(19.7913f, 19.7913f, 0.0f, false, false, -4.8851f, -1.5152f) + arcToRelative(0.0741f, 0.0741f, 0.0f, false, false, -0.0785f, 0.0371f) + curveToRelative(-0.211f, 0.3753f, -0.4447f, 0.8648f, -0.6083f, 1.2495f) + curveToRelative(-1.8447f, -0.2762f, -3.68f, -0.2762f, -5.4868f, 0.0f) + curveToRelative(-0.1636f, -0.3933f, -0.4058f, -0.8742f, -0.6177f, -1.2495f) + arcToRelative(0.077f, 0.077f, 0.0f, false, false, -0.0785f, -0.037f) + arcToRelative(19.7363f, 19.7363f, 0.0f, false, false, -4.8852f, 1.515f) + arcToRelative(0.0699f, 0.0699f, 0.0f, false, false, -0.0321f, 0.0277f) + curveTo(0.5334f, 9.0458f, -0.319f, 13.5799f, 0.0992f, 18.0578f) + arcToRelative(0.0824f, 0.0824f, 0.0f, false, false, 0.0312f, 0.0561f) + curveToRelative(2.0528f, 1.5076f, 4.0413f, 2.4228f, 5.9929f, 3.0294f) + arcToRelative(0.0777f, 0.0777f, 0.0f, false, false, 0.0842f, -0.0276f) + curveToRelative(0.4616f, -0.6304f, 0.8731f, -1.2952f, 1.226f, -1.9942f) + arcToRelative(0.076f, 0.076f, 0.0f, false, false, -0.0416f, -0.1057f) + curveToRelative(-0.6528f, -0.2476f, -1.2743f, -0.5495f, -1.8722f, -0.8923f) + arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0076f, -0.1277f) + curveToRelative(0.1258f, -0.0943f, 0.2517f, -0.1923f, 0.3718f, -0.2914f) + arcToRelative(0.0743f, 0.0743f, 0.0f, false, true, 0.0776f, -0.0105f) + curveToRelative(3.9278f, 1.7933f, 8.18f, 1.7933f, 12.0614f, 0.0f) + arcToRelative(0.0739f, 0.0739f, 0.0f, false, true, 0.0785f, 0.0095f) + curveToRelative(0.1202f, 0.099f, 0.246f, 0.1981f, 0.3728f, 0.2924f) + arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0066f, 0.1276f) + arcToRelative(12.2986f, 12.2986f, 0.0f, false, true, -1.873f, 0.8914f) + arcToRelative(0.0766f, 0.0766f, 0.0f, false, false, -0.0407f, 0.1067f) + curveToRelative(0.3604f, 0.698f, 0.7719f, 1.3628f, 1.225f, 1.9932f) + arcToRelative(0.076f, 0.076f, 0.0f, false, false, 0.0842f, 0.0286f) + curveToRelative(1.961f, -0.6067f, 3.9495f, -1.5219f, 6.0023f, -3.0294f) + arcToRelative(0.077f, 0.077f, 0.0f, false, false, 0.0313f, -0.0552f) + curveToRelative(0.5004f, -5.177f, -0.8382f, -9.6739f, -3.5485f, -13.6604f) + arcToRelative(0.061f, 0.061f, 0.0f, false, false, -0.0312f, -0.0286f) + close() + moveTo(8.02f, 15.3312f) + curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f) + curveToRelative(0.0f, -1.3332f, 0.9555f, -2.4189f, 2.157f, -2.4189f) + curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f) + curveToRelative(0.0f, 1.3332f, -0.9555f, 2.4189f, -2.1569f, 2.4189f) + close() + moveTo(15.9948f, 15.3312f) + curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f) + curveToRelative(0.0f, -1.3332f, 0.9554f, -2.4189f, 2.1569f, -2.4189f) + curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f) + curveToRelative(0.0f, 1.3332f, -0.946f, 2.4189f, -2.1568f, 2.4189f) + close() + } + }.build() return _discord!! } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Facebook.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Facebook.kt index 1bb4bda345..c4c3499567 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Facebook.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Facebook.kt @@ -15,39 +15,46 @@ val CustomIcons.Facebook: ImageVector if (_facebook != null) { return _facebook!! } - _facebook = Builder( - name = "Facebook", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, - viewportWidth = 24.0f, viewportHeight = 24.0f, - ).apply { - path( - fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, - strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, - pathFillType = NonZero, - ) { - moveTo(24.0f, 12.073f) - curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f) - reflectiveCurveToRelative(-12.0f, 5.373f, -12.0f, 12.0f) - curveToRelative(0.0f, 5.99f, 4.388f, 10.954f, 10.125f, 11.854f) - verticalLineToRelative(-8.385f) - horizontalLineTo(7.078f) - verticalLineToRelative(-3.47f) - horizontalLineToRelative(3.047f) - verticalLineTo(9.43f) - curveToRelative(0.0f, -3.007f, 1.792f, -4.669f, 4.533f, -4.669f) - curveToRelative(1.312f, 0.0f, 2.686f, 0.235f, 2.686f, 0.235f) - verticalLineToRelative(2.953f) - horizontalLineTo(15.83f) - curveToRelative(-1.491f, 0.0f, -1.956f, 0.925f, -1.956f, 1.874f) - verticalLineToRelative(2.25f) - horizontalLineToRelative(3.328f) - lineToRelative(-0.532f, 3.47f) - horizontalLineToRelative(-2.796f) - verticalLineToRelative(8.385f) - curveTo(19.612f, 23.027f, 24.0f, 18.062f, 24.0f, 12.073f) - close() - } - } - .build() + _facebook = + Builder( + name = "Facebook", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(24.0f, 12.073f) + curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f) + reflectiveCurveToRelative(-12.0f, 5.373f, -12.0f, 12.0f) + curveToRelative(0.0f, 5.99f, 4.388f, 10.954f, 10.125f, 11.854f) + verticalLineToRelative(-8.385f) + horizontalLineTo(7.078f) + verticalLineToRelative(-3.47f) + horizontalLineToRelative(3.047f) + verticalLineTo(9.43f) + curveToRelative(0.0f, -3.007f, 1.792f, -4.669f, 4.533f, -4.669f) + curveToRelative(1.312f, 0.0f, 2.686f, 0.235f, 2.686f, 0.235f) + verticalLineToRelative(2.953f) + horizontalLineTo(15.83f) + curveToRelative(-1.491f, 0.0f, -1.956f, 0.925f, -1.956f, 1.874f) + verticalLineToRelative(2.25f) + horizontalLineToRelative(3.328f) + lineToRelative(-0.532f, 3.47f) + horizontalLineToRelative(-2.796f) + verticalLineToRelative(8.385f) + curveTo(19.612f, 23.027f, 24.0f, 18.062f, 24.0f, 12.073f) + close() + } + }.build() return _facebook!! } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Github.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Github.kt index 25d16b5b96..0d28b59458 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Github.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Github.kt @@ -15,44 +15,51 @@ val CustomIcons.Github: ImageVector if (_github != null) { return _github!! } - _github = Builder( - name = "Github", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, - viewportWidth = 24.0f, viewportHeight = 24.0f, - ).apply { - path( - fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, - strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, - pathFillType = NonZero, - ) { - moveTo(12.0f, 0.297f) - curveToRelative(-6.63f, 0.0f, -12.0f, 5.373f, -12.0f, 12.0f) - curveToRelative(0.0f, 5.303f, 3.438f, 9.8f, 8.205f, 11.385f) - curveToRelative(0.6f, 0.113f, 0.82f, -0.258f, 0.82f, -0.577f) - curveToRelative(0.0f, -0.285f, -0.01f, -1.04f, -0.015f, -2.04f) - curveToRelative(-3.338f, 0.724f, -4.042f, -1.61f, -4.042f, -1.61f) - curveTo(4.422f, 18.07f, 3.633f, 17.7f, 3.633f, 17.7f) - curveToRelative(-1.087f, -0.744f, 0.084f, -0.729f, 0.084f, -0.729f) - curveToRelative(1.205f, 0.084f, 1.838f, 1.236f, 1.838f, 1.236f) - curveToRelative(1.07f, 1.835f, 2.809f, 1.305f, 3.495f, 0.998f) - curveToRelative(0.108f, -0.776f, 0.417f, -1.305f, 0.76f, -1.605f) - curveToRelative(-2.665f, -0.3f, -5.466f, -1.332f, -5.466f, -5.93f) - curveToRelative(0.0f, -1.31f, 0.465f, -2.38f, 1.235f, -3.22f) - curveToRelative(-0.135f, -0.303f, -0.54f, -1.523f, 0.105f, -3.176f) - curveToRelative(0.0f, 0.0f, 1.005f, -0.322f, 3.3f, 1.23f) - curveToRelative(0.96f, -0.267f, 1.98f, -0.399f, 3.0f, -0.405f) - curveToRelative(1.02f, 0.006f, 2.04f, 0.138f, 3.0f, 0.405f) - curveToRelative(2.28f, -1.552f, 3.285f, -1.23f, 3.285f, -1.23f) - curveToRelative(0.645f, 1.653f, 0.24f, 2.873f, 0.12f, 3.176f) - curveToRelative(0.765f, 0.84f, 1.23f, 1.91f, 1.23f, 3.22f) - curveToRelative(0.0f, 4.61f, -2.805f, 5.625f, -5.475f, 5.92f) - curveToRelative(0.42f, 0.36f, 0.81f, 1.096f, 0.81f, 2.22f) - curveToRelative(0.0f, 1.606f, -0.015f, 2.896f, -0.015f, 3.286f) - curveToRelative(0.0f, 0.315f, 0.21f, 0.69f, 0.825f, 0.57f) - curveTo(20.565f, 22.092f, 24.0f, 17.592f, 24.0f, 12.297f) - curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f) - } - } - .build() + _github = + Builder( + name = "Github", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(12.0f, 0.297f) + curveToRelative(-6.63f, 0.0f, -12.0f, 5.373f, -12.0f, 12.0f) + curveToRelative(0.0f, 5.303f, 3.438f, 9.8f, 8.205f, 11.385f) + curveToRelative(0.6f, 0.113f, 0.82f, -0.258f, 0.82f, -0.577f) + curveToRelative(0.0f, -0.285f, -0.01f, -1.04f, -0.015f, -2.04f) + curveToRelative(-3.338f, 0.724f, -4.042f, -1.61f, -4.042f, -1.61f) + curveTo(4.422f, 18.07f, 3.633f, 17.7f, 3.633f, 17.7f) + curveToRelative(-1.087f, -0.744f, 0.084f, -0.729f, 0.084f, -0.729f) + curveToRelative(1.205f, 0.084f, 1.838f, 1.236f, 1.838f, 1.236f) + curveToRelative(1.07f, 1.835f, 2.809f, 1.305f, 3.495f, 0.998f) + curveToRelative(0.108f, -0.776f, 0.417f, -1.305f, 0.76f, -1.605f) + curveToRelative(-2.665f, -0.3f, -5.466f, -1.332f, -5.466f, -5.93f) + curveToRelative(0.0f, -1.31f, 0.465f, -2.38f, 1.235f, -3.22f) + curveToRelative(-0.135f, -0.303f, -0.54f, -1.523f, 0.105f, -3.176f) + curveToRelative(0.0f, 0.0f, 1.005f, -0.322f, 3.3f, 1.23f) + curveToRelative(0.96f, -0.267f, 1.98f, -0.399f, 3.0f, -0.405f) + curveToRelative(1.02f, 0.006f, 2.04f, 0.138f, 3.0f, 0.405f) + curveToRelative(2.28f, -1.552f, 3.285f, -1.23f, 3.285f, -1.23f) + curveToRelative(0.645f, 1.653f, 0.24f, 2.873f, 0.12f, 3.176f) + curveToRelative(0.765f, 0.84f, 1.23f, 1.91f, 1.23f, 3.22f) + curveToRelative(0.0f, 4.61f, -2.805f, 5.625f, -5.475f, 5.92f) + curveToRelative(0.42f, 0.36f, 0.81f, 1.096f, 0.81f, 2.22f) + curveToRelative(0.0f, 1.606f, -0.015f, 2.896f, -0.015f, 3.286f) + curveToRelative(0.0f, 0.315f, 0.21f, 0.69f, 0.825f, 0.57f) + curveTo(20.565f, 22.092f, 24.0f, 17.592f, 24.0f, 12.297f) + curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f) + } + }.build() return _github!! } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Reddit.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Reddit.kt index efed17f01b..b0d984a869 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Reddit.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Reddit.kt @@ -15,70 +15,77 @@ val CustomIcons.Reddit: ImageVector if (_reddit != null) { return _reddit!! } - _reddit = Builder( - name = "Reddit", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, - viewportWidth = 24.0f, viewportHeight = 24.0f, - ).apply { - path( - fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, - strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, - pathFillType = NonZero, - ) { - moveTo(12.0f, 0.0f) - arcTo(12.0f, 12.0f, 0.0f, false, false, 0.0f, 12.0f) - arcToRelative(12.0f, 12.0f, 0.0f, false, false, 12.0f, 12.0f) - arcToRelative(12.0f, 12.0f, 0.0f, false, false, 12.0f, -12.0f) - arcTo(12.0f, 12.0f, 0.0f, false, false, 12.0f, 0.0f) - close() - moveTo(17.01f, 4.744f) - curveToRelative(0.688f, 0.0f, 1.25f, 0.561f, 1.25f, 1.249f) - arcToRelative(1.25f, 1.25f, 0.0f, false, true, -2.498f, 0.056f) - lineToRelative(-2.597f, -0.547f) - lineToRelative(-0.8f, 3.747f) - curveToRelative(1.824f, 0.07f, 3.48f, 0.632f, 4.674f, 1.488f) - curveToRelative(0.308f, -0.309f, 0.73f, -0.491f, 1.207f, -0.491f) - curveToRelative(0.968f, 0.0f, 1.754f, 0.786f, 1.754f, 1.754f) - curveToRelative(0.0f, 0.716f, -0.435f, 1.333f, -1.01f, 1.614f) - arcToRelative(3.111f, 3.111f, 0.0f, false, true, 0.042f, 0.52f) - curveToRelative(0.0f, 2.694f, -3.13f, 4.87f, -7.004f, 4.87f) - curveToRelative(-3.874f, 0.0f, -7.004f, -2.176f, -7.004f, -4.87f) - curveToRelative(0.0f, -0.183f, 0.015f, -0.366f, 0.043f, -0.534f) - arcTo(1.748f, 1.748f, 0.0f, false, true, 4.028f, 12.0f) - curveToRelative(0.0f, -0.968f, 0.786f, -1.754f, 1.754f, -1.754f) - curveToRelative(0.463f, 0.0f, 0.898f, 0.196f, 1.207f, 0.49f) - curveToRelative(1.207f, -0.883f, 2.878f, -1.43f, 4.744f, -1.487f) - lineToRelative(0.885f, -4.182f) - arcToRelative(0.342f, 0.342f, 0.0f, false, true, 0.14f, -0.197f) - arcToRelative(0.35f, 0.35f, 0.0f, false, true, 0.238f, -0.042f) - lineToRelative(2.906f, 0.617f) - arcToRelative(1.214f, 1.214f, 0.0f, false, true, 1.108f, -0.701f) - close() - moveTo(9.25f, 12.0f) - curveTo(8.561f, 12.0f, 8.0f, 12.562f, 8.0f, 13.25f) - curveToRelative(0.0f, 0.687f, 0.561f, 1.248f, 1.25f, 1.248f) - curveToRelative(0.687f, 0.0f, 1.248f, -0.561f, 1.248f, -1.249f) - curveToRelative(0.0f, -0.688f, -0.561f, -1.249f, -1.249f, -1.249f) - close() - moveTo(14.75f, 12.0f) - curveToRelative(-0.687f, 0.0f, -1.248f, 0.561f, -1.248f, 1.25f) - curveToRelative(0.0f, 0.687f, 0.561f, 1.248f, 1.249f, 1.248f) - curveToRelative(0.688f, 0.0f, 1.249f, -0.561f, 1.249f, -1.249f) - curveToRelative(0.0f, -0.687f, -0.562f, -1.249f, -1.25f, -1.249f) - close() - moveTo(9.284f, 15.99f) - arcToRelative(0.327f, 0.327f, 0.0f, false, false, -0.231f, 0.094f) - arcToRelative(0.33f, 0.33f, 0.0f, false, false, 0.0f, 0.463f) - curveToRelative(0.842f, 0.842f, 2.484f, 0.913f, 2.961f, 0.913f) - curveToRelative(0.477f, 0.0f, 2.105f, -0.056f, 2.961f, -0.913f) - arcToRelative(0.361f, 0.361f, 0.0f, false, false, 0.029f, -0.463f) - arcToRelative(0.33f, 0.33f, 0.0f, false, false, -0.464f, 0.0f) - curveToRelative(-0.547f, 0.533f, -1.684f, 0.73f, -2.512f, 0.73f) - curveToRelative(-0.828f, 0.0f, -1.979f, -0.196f, -2.512f, -0.73f) - arcToRelative(0.326f, 0.326f, 0.0f, false, false, -0.232f, -0.095f) - close() - } - } - .build() + _reddit = + Builder( + name = "Reddit", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(12.0f, 0.0f) + arcTo(12.0f, 12.0f, 0.0f, false, false, 0.0f, 12.0f) + arcToRelative(12.0f, 12.0f, 0.0f, false, false, 12.0f, 12.0f) + arcToRelative(12.0f, 12.0f, 0.0f, false, false, 12.0f, -12.0f) + arcTo(12.0f, 12.0f, 0.0f, false, false, 12.0f, 0.0f) + close() + moveTo(17.01f, 4.744f) + curveToRelative(0.688f, 0.0f, 1.25f, 0.561f, 1.25f, 1.249f) + arcToRelative(1.25f, 1.25f, 0.0f, false, true, -2.498f, 0.056f) + lineToRelative(-2.597f, -0.547f) + lineToRelative(-0.8f, 3.747f) + curveToRelative(1.824f, 0.07f, 3.48f, 0.632f, 4.674f, 1.488f) + curveToRelative(0.308f, -0.309f, 0.73f, -0.491f, 1.207f, -0.491f) + curveToRelative(0.968f, 0.0f, 1.754f, 0.786f, 1.754f, 1.754f) + curveToRelative(0.0f, 0.716f, -0.435f, 1.333f, -1.01f, 1.614f) + arcToRelative(3.111f, 3.111f, 0.0f, false, true, 0.042f, 0.52f) + curveToRelative(0.0f, 2.694f, -3.13f, 4.87f, -7.004f, 4.87f) + curveToRelative(-3.874f, 0.0f, -7.004f, -2.176f, -7.004f, -4.87f) + curveToRelative(0.0f, -0.183f, 0.015f, -0.366f, 0.043f, -0.534f) + arcTo(1.748f, 1.748f, 0.0f, false, true, 4.028f, 12.0f) + curveToRelative(0.0f, -0.968f, 0.786f, -1.754f, 1.754f, -1.754f) + curveToRelative(0.463f, 0.0f, 0.898f, 0.196f, 1.207f, 0.49f) + curveToRelative(1.207f, -0.883f, 2.878f, -1.43f, 4.744f, -1.487f) + lineToRelative(0.885f, -4.182f) + arcToRelative(0.342f, 0.342f, 0.0f, false, true, 0.14f, -0.197f) + arcToRelative(0.35f, 0.35f, 0.0f, false, true, 0.238f, -0.042f) + lineToRelative(2.906f, 0.617f) + arcToRelative(1.214f, 1.214f, 0.0f, false, true, 1.108f, -0.701f) + close() + moveTo(9.25f, 12.0f) + curveTo(8.561f, 12.0f, 8.0f, 12.562f, 8.0f, 13.25f) + curveToRelative(0.0f, 0.687f, 0.561f, 1.248f, 1.25f, 1.248f) + curveToRelative(0.687f, 0.0f, 1.248f, -0.561f, 1.248f, -1.249f) + curveToRelative(0.0f, -0.688f, -0.561f, -1.249f, -1.249f, -1.249f) + close() + moveTo(14.75f, 12.0f) + curveToRelative(-0.687f, 0.0f, -1.248f, 0.561f, -1.248f, 1.25f) + curveToRelative(0.0f, 0.687f, 0.561f, 1.248f, 1.249f, 1.248f) + curveToRelative(0.688f, 0.0f, 1.249f, -0.561f, 1.249f, -1.249f) + curveToRelative(0.0f, -0.687f, -0.562f, -1.249f, -1.25f, -1.249f) + close() + moveTo(9.284f, 15.99f) + arcToRelative(0.327f, 0.327f, 0.0f, false, false, -0.231f, 0.094f) + arcToRelative(0.33f, 0.33f, 0.0f, false, false, 0.0f, 0.463f) + curveToRelative(0.842f, 0.842f, 2.484f, 0.913f, 2.961f, 0.913f) + curveToRelative(0.477f, 0.0f, 2.105f, -0.056f, 2.961f, -0.913f) + arcToRelative(0.361f, 0.361f, 0.0f, false, false, 0.029f, -0.463f) + arcToRelative(0.33f, 0.33f, 0.0f, false, false, -0.464f, 0.0f) + curveToRelative(-0.547f, 0.533f, -1.684f, 0.73f, -2.512f, 0.73f) + curveToRelative(-0.828f, 0.0f, -1.979f, -0.196f, -2.512f, -0.73f) + arcToRelative(0.326f, 0.326f, 0.0f, false, false, -0.232f, -0.095f) + close() + } + }.build() return _reddit!! } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/X.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/X.kt index 054030d03a..534a7ea36b 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/X.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/X.kt @@ -15,38 +15,44 @@ val CustomIcons.X: ImageVector if (_x != null) { return _x!! } - _x = Builder( - name = "X", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, - viewportWidth = - 24.0f, - viewportHeight = 24.0f, - ).apply { - path( - fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, - strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, - pathFillType = NonZero, - ) { - moveTo(18.901f, 1.153f) - horizontalLineToRelative(3.68f) - lineToRelative(-8.04f, 9.19f) - lineTo(24.0f, 22.846f) - horizontalLineToRelative(-7.406f) - lineToRelative(-5.8f, -7.584f) - lineToRelative(-6.638f, 7.584f) - horizontalLineTo(0.474f) - lineToRelative(8.6f, -9.83f) - lineTo(0.0f, 1.154f) - horizontalLineToRelative(7.594f) - lineToRelative(5.243f, 6.932f) - close() - moveTo(17.61f, 20.644f) - horizontalLineToRelative(2.039f) - lineTo(6.486f, 3.24f) - horizontalLineTo(4.298f) - close() - } - } - .build() + _x = + Builder( + name = "X", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = + 24.0f, + viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(18.901f, 1.153f) + horizontalLineToRelative(3.68f) + lineToRelative(-8.04f, 9.19f) + lineTo(24.0f, 22.846f) + horizontalLineToRelative(-7.406f) + lineToRelative(-5.8f, -7.584f) + lineToRelative(-6.638f, 7.584f) + horizontalLineTo(0.474f) + lineToRelative(8.6f, -9.83f) + lineTo(0.0f, 1.154f) + horizontalLineToRelative(7.594f) + lineToRelative(5.243f, 6.932f) + close() + moveTo(17.61f, 20.644f) + horizontalLineToRelative(2.039f) + lineTo(6.486f, 3.24f) + horizontalLineTo(4.298f) + close() + } + }.build() return _x!! } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt index 0922f925c8..64d93535d7 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt @@ -56,10 +56,11 @@ fun EmptyScreen( ) { val face = remember { getRandomErrorFace() } Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 24.dp), + modifier = + modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { @@ -73,17 +74,19 @@ fun EmptyScreen( Text( text = message, - modifier = Modifier - .paddingFromBaseline(top = 24.dp) - .secondaryItemAlpha(), + modifier = + Modifier + .paddingFromBaseline(top = 24.dp) + .secondaryItemAlpha(), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, ) if (!actions.isNullOrEmpty()) { Row( - modifier = Modifier - .padding(top = 24.dp), + modifier = + Modifier + .padding(top = 24.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { actions.fastForEach { @@ -99,15 +102,14 @@ fun EmptyScreen( } } -private val ErrorFaces = listOf( - "(・o・;)", - "Σ(ಠ_ಠ)", - "ಥ_ಥ", - "(˘・_・˘)", - "(; ̄Д ̄)", - "(・Д・。", -) +private val ErrorFaces = + listOf( + "(・o・;)", + "Σ(ಠ_ಠ)", + "ಥ_ಥ", + "(˘・_・˘)", + "(; ̄Д ̄)", + "(・Д・。", + ) -private fun getRandomErrorFace(): String { - return ErrorFaces[Random.nextInt(ErrorFaces.size)] -} +private fun getRandomErrorFace(): String = ErrorFaces[Random.nextInt(ErrorFaces.size)] diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt index e3b65079f4..f836e7e167 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt @@ -49,21 +49,21 @@ fun InfoScreen( val strokeWidth = Dp.Hairline val borderColor = MaterialTheme.colorScheme.outline Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .drawBehind { - drawLine( - borderColor, - Offset(0f, 0f), - Offset(size.width, 0f), - strokeWidth.value, - ) - } - .windowInsetsPadding(NavigationBarDefaults.windowInsets) - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ), + modifier = + Modifier + .background(MaterialTheme.colorScheme.background) + .drawBehind { + drawLine( + borderColor, + Offset(0f, 0f), + Offset(size.width, 0f), + strokeWidth.value, + ) + }.windowInsetsPadding(NavigationBarDefaults.windowInsets) + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), ) { Button( modifier = Modifier.fillMaxWidth(), @@ -85,28 +85,31 @@ fun InfoScreen( ) { paddingValues -> // Status bar scrim Box( - modifier = Modifier - .zIndex(2f) - .secondaryItemAlpha() - .background(MaterialTheme.colorScheme.background) - .fillMaxWidth() - .height(paddingValues.calculateTopPadding()), + modifier = + Modifier + .zIndex(2f) + .secondaryItemAlpha() + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth() + .height(paddingValues.calculateTopPadding()), ) Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .fillMaxWidth() - .padding(paddingValues) - .padding(top = 48.dp) - .padding(horizontal = MaterialTheme.padding.medium), + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .fillMaxWidth() + .padding(paddingValues) + .padding(top = 48.dp) + .padding(horizontal = MaterialTheme.padding.medium), ) { Icon( imageVector = icon, contentDescription = null, - modifier = Modifier - .padding(bottom = MaterialTheme.padding.small) - .size(48.dp), + modifier = + Modifier + .padding(bottom = MaterialTheme.padding.small) + .size(48.dp), tint = MaterialTheme.colorScheme.primary, ) Text( @@ -115,9 +118,10 @@ fun InfoScreen( ) Text( text = subtitleText, - modifier = Modifier - .secondaryItemAlpha() - .padding(vertical = MaterialTheme.padding.small), + modifier = + Modifier + .secondaryItemAlpha() + .padding(vertical = MaterialTheme.padding.small), style = MaterialTheme.typography.titleSmall, ) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/theme/Typography.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/theme/Typography.kt index c875b6f1c6..b3109b3728 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/theme/Typography.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/theme/Typography.kt @@ -8,7 +8,8 @@ import androidx.compose.ui.text.font.FontWeight val Typography.header: TextStyle @Composable - get() = bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.SemiBold, - ) + get() = + bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold, + ) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Elevation.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Elevation.kt index 67792fbaf3..937f67d622 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Elevation.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Elevation.kt @@ -52,15 +52,16 @@ suspend fun Animatable.animateElevation( from: Interaction? = null, to: Interaction? = null, ) { - val spec = when { - // Moving to a new state - to != null -> ElevationDefaults.incomingAnimationSpecForInteraction(to) - // Moving to default, from a previous state - from != null -> ElevationDefaults.outgoingAnimationSpecForInteraction(from) - // Loading the initial state, or moving back to the baseline state from a disabled / - // unknown state, so just snap to the final value. - else -> null - } + val spec = + when { + // Moving to a new state + to != null -> ElevationDefaults.incomingAnimationSpecForInteraction(to) + // Moving to default, from a previous state + from != null -> ElevationDefaults.outgoingAnimationSpecForInteraction(from) + // Loading the initial state, or moving back to the baseline state from a disabled / + // unknown state, so just snap to the final value. + else -> null + } if (spec != null) animateTo(target, spec) else snapTo(target) } @@ -80,15 +81,14 @@ private object ElevationDefaults { * * @param interaction the [Interaction] that is being animated to */ - fun incomingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec? { - return when (interaction) { + fun incomingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec? = + when (interaction) { is PressInteraction.Press -> DefaultIncomingSpec is DragInteraction.Start -> DefaultIncomingSpec is HoverInteraction.Enter -> DefaultIncomingSpec is FocusInteraction.Focus -> DefaultIncomingSpec else -> null } - } /** * Returns the [AnimationSpec]s used when animating elevation away from [interaction], to the @@ -96,30 +96,32 @@ private object ElevationDefaults { * * @param interaction the [Interaction] that is being animated away from */ - fun outgoingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec? { - return when (interaction) { + fun outgoingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec? = + when (interaction) { is PressInteraction.Press -> DefaultOutgoingSpec is DragInteraction.Start -> DefaultOutgoingSpec is HoverInteraction.Enter -> HoveredOutgoingSpec is FocusInteraction.Focus -> DefaultOutgoingSpec else -> null } - } } private val OutgoingSpecEasing: Easing = CubicBezierEasing(0.40f, 0.00f, 0.60f, 1.00f) -private val DefaultIncomingSpec = TweenSpec( - durationMillis = 120, - easing = FastOutSlowInEasing, -) +private val DefaultIncomingSpec = + TweenSpec( + durationMillis = 120, + easing = FastOutSlowInEasing, + ) -private val DefaultOutgoingSpec = TweenSpec( - durationMillis = 150, - easing = OutgoingSpecEasing, -) +private val DefaultOutgoingSpec = + TweenSpec( + durationMillis = 150, + easing = OutgoingSpecEasing, + ) -private val HoveredOutgoingSpec = TweenSpec( - durationMillis = 120, - easing = OutgoingSpecEasing, -) +private val HoveredOutgoingSpec = + TweenSpec( + durationMillis = 120, + easing = OutgoingSpecEasing, + ) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt index cdabf1b496..a5d006b296 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt @@ -6,13 +6,11 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember @Composable -fun LazyListState.shouldExpandFAB(): Boolean { - return remember { +fun LazyListState.shouldExpandFAB(): Boolean = + remember { derivedStateOf { (firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0) || lastScrolledBackward || !canScrollForward } - } - .value -} + }.value diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt index 875cd45832..327e804b04 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt @@ -24,17 +24,18 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.platform.LocalFocusManager import tachiyomi.presentation.core.components.material.SecondaryItemAlpha -fun Modifier.selectedBackground(isSelected: Boolean): Modifier = if (isSelected) { - composed { - val alpha = if (isSystemInDarkTheme()) 0.16f else 0.22f - val color = MaterialTheme.colorScheme.secondary.copy(alpha = alpha) - Modifier.drawBehind { - drawRect(color) +fun Modifier.selectedBackground(isSelected: Boolean): Modifier = + if (isSelected) { + composed { + val alpha = if (isSystemInDarkTheme()) 0.16f else 0.22f + val color = MaterialTheme.colorScheme.secondary.copy(alpha = alpha) + Modifier.drawBehind { + drawRect(color) + } } + } else { + this } -} else { - this -} fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(SecondaryItemAlpha) @@ -54,65 +55,66 @@ fun Modifier.clickableNoIndication( * * Naturally, the TextField should be set to single line only. */ -fun Modifier.runOnEnterKeyPressed(action: () -> Unit): Modifier = this.onPreviewKeyEvent { - when (it.key) { - Key.Enter, Key.NumPadEnter -> { - action() - true +fun Modifier.runOnEnterKeyPressed(action: () -> Unit): Modifier = + this.onPreviewKeyEvent { + when (it.key) { + Key.Enter, Key.NumPadEnter -> { + action() + true + } + else -> false } - else -> false } -} /** * For TextField on AppBar, this modifier will request focus * to the element the first time it's composed. */ -fun Modifier.showSoftKeyboard(show: Boolean): Modifier = if (show) { - composed { - val focusRequester = remember { FocusRequester() } - var openKeyboard by rememberSaveable { mutableStateOf(show) } - LaunchedEffect(focusRequester) { - if (openKeyboard) { - focusRequester.requestFocus() - openKeyboard = false +fun Modifier.showSoftKeyboard(show: Boolean): Modifier = + if (show) { + composed { + val focusRequester = remember { FocusRequester() } + var openKeyboard by rememberSaveable { mutableStateOf(show) } + LaunchedEffect(focusRequester) { + if (openKeyboard) { + focusRequester.requestFocus() + openKeyboard = false + } } - } - Modifier.focusRequester(focusRequester) + Modifier.focusRequester(focusRequester) + } + } else { + this } -} else { - this -} /** * For TextField, this modifier will clear focus when soft * keyboard is hidden. */ -fun Modifier.clearFocusOnSoftKeyboardHide( - onFocusCleared: (() -> Unit)? = null, -): Modifier = composed { - var isFocused by remember { mutableStateOf(false) } - var keyboardShowedSinceFocused by remember { mutableStateOf(false) } - if (isFocused) { - val imeVisible = WindowInsets.isImeVisible - val focusManager = LocalFocusManager.current - LaunchedEffect(imeVisible) { - if (imeVisible) { - keyboardShowedSinceFocused = true - } else if (keyboardShowedSinceFocused) { - focusManager.clearFocus() - onFocusCleared?.invoke() +fun Modifier.clearFocusOnSoftKeyboardHide(onFocusCleared: (() -> Unit)? = null): Modifier = + composed { + var isFocused by remember { mutableStateOf(false) } + var keyboardShowedSinceFocused by remember { mutableStateOf(false) } + if (isFocused) { + val imeVisible = WindowInsets.isImeVisible + val focusManager = LocalFocusManager.current + LaunchedEffect(imeVisible) { + if (imeVisible) { + keyboardShowedSinceFocused = true + } else if (keyboardShowedSinceFocused) { + focusManager.clearFocus() + onFocusCleared?.invoke() + } } } - } - Modifier.onFocusChanged { - if (isFocused != it.isFocused) { - if (isFocused) { - keyboardShowedSinceFocused = false + Modifier.onFocusChanged { + if (isFocused != it.isFocused) { + if (isFocused) { + keyboardShowedSinceFocused = false + } + isFocused = it.isFocused } - isFocused = it.isFocused } } -} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/PaddingValues.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/PaddingValues.kt index 6d3baad5ef..560ebc9476 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/PaddingValues.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/PaddingValues.kt @@ -12,10 +12,12 @@ import androidx.compose.ui.platform.LocalLayoutDirection operator fun PaddingValues.plus(other: PaddingValues): PaddingValues { val layoutDirection = LocalLayoutDirection.current return PaddingValues( - start = calculateStartPadding(layoutDirection) + - other.calculateStartPadding(layoutDirection), - end = calculateEndPadding(layoutDirection) + - other.calculateEndPadding(layoutDirection), + start = + calculateStartPadding(layoutDirection) + + other.calculateStartPadding(layoutDirection), + end = + calculateEndPadding(layoutDirection) + + other.calculateEndPadding(layoutDirection), top = calculateTopPadding() + other.calculateTopPadding(), bottom = calculateBottomPadding() + other.calculateBottomPadding(), ) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Scrollbar.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Scrollbar.kt index e0cd0a3e02..e5d4b7d4da 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Scrollbar.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Scrollbar.kt @@ -99,43 +99,56 @@ private fun Modifier.drawScrollbar( orientation: Orientation, reverseScrolling: Boolean, positionOffset: Float, -): Modifier = drawScrollbar( - orientation, - reverseScrolling, -) { reverseDirection, atEnd, thickness, color, alpha -> - val layoutInfo = state.layoutInfo - val viewportSize = if (orientation == Orientation.Horizontal) { - layoutInfo.viewportSize.width - } else { - layoutInfo.viewportSize.height - } - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding - val items = layoutInfo.visibleItemsInfo - val itemsSize = items.fastSumBy { it.size } - val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize - val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size - val totalSize = estimatedItemSize * layoutInfo.totalItemsCount - val thumbSize = viewportSize / totalSize * viewportSize - val startOffset = if (items.isEmpty()) { - 0f - } else { - items - .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!! - .run { - val startPadding = if (reverseDirection) { - layoutInfo.afterContentPadding - } else { - layoutInfo.beforeContentPadding - } - startPadding + ((estimatedItemSize * index - offset) / totalSize * viewportSize) +): Modifier = + drawScrollbar( + orientation, + reverseScrolling, + ) { reverseDirection, atEnd, thickness, color, alpha -> + val layoutInfo = state.layoutInfo + val viewportSize = + if (orientation == Orientation.Horizontal) { + layoutInfo.viewportSize.width + } else { + layoutInfo.viewportSize.height + } - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding + val items = layoutInfo.visibleItemsInfo + val itemsSize = items.fastSumBy { it.size } + val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize + val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size + val totalSize = estimatedItemSize * layoutInfo.totalItemsCount + val thumbSize = viewportSize / totalSize * viewportSize + val startOffset = + if (items.isEmpty()) { + 0f + } else { + items + .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!! + .run { + val startPadding = + if (reverseDirection) { + layoutInfo.afterContentPadding + } else { + layoutInfo.beforeContentPadding + } + startPadding + ((estimatedItemSize * index - offset) / totalSize * viewportSize) + } } + val drawScrollbar = + onDrawScrollbar( + orientation, + reverseDirection, + atEnd, + showScrollbar, + thickness, + color, + alpha, + thumbSize, + startOffset, + positionOffset, + ) + drawContent() + drawScrollbar() } - val drawScrollbar = onDrawScrollbar( - orientation, reverseDirection, atEnd, showScrollbar, - thickness, color, alpha, thumbSize, startOffset, positionOffset, - ) - drawContent() - drawScrollbar() -} private fun ContentDrawScope.onDrawScrollbar( orientation: Orientation, @@ -149,22 +162,24 @@ private fun ContentDrawScope.onDrawScrollbar( scrollOffset: Float, positionOffset: Float, ): DrawScope.() -> Unit { - val topLeft = if (orientation == Orientation.Horizontal) { - Offset( - if (reverseDirection) size.width - scrollOffset - thumbSize else scrollOffset, - if (atEnd) size.height - positionOffset - thickness else positionOffset, - ) - } else { - Offset( - if (atEnd) size.width - positionOffset - thickness else positionOffset, - if (reverseDirection) size.height - scrollOffset - thumbSize else scrollOffset, - ) - } - val size = if (orientation == Orientation.Horizontal) { - Size(thumbSize, thickness) - } else { - Size(thickness, thumbSize) - } + val topLeft = + if (orientation == Orientation.Horizontal) { + Offset( + if (reverseDirection) size.width - scrollOffset - thumbSize else scrollOffset, + if (atEnd) size.height - positionOffset - thickness else positionOffset, + ) + } else { + Offset( + if (atEnd) size.width - positionOffset - thickness else positionOffset, + if (reverseDirection) size.height - scrollOffset - thumbSize else scrollOffset, + ) + } + val size = + if (orientation == Orientation.Horizontal) { + Size(thumbSize, thickness) + } else { + Size(thickness, thumbSize) + } return { if (showScrollbar) { @@ -188,59 +203,64 @@ private fun Modifier.drawScrollbar( color: Color, alpha: () -> Float, ) -> Unit, -): Modifier = composed { - val scrolled = remember { - MutableSharedFlow( - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - } - val nestedScrollConnection = remember(orientation, scrolled) { - object : NestedScrollConnection { - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - val delta = if (orientation == Orientation.Horizontal) consumed.x else consumed.y - if (delta != 0f) scrolled.tryEmit(Unit) - return Offset.Zero +): Modifier = + composed { + val scrolled = + remember { + MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) } + val nestedScrollConnection = + remember(orientation, scrolled) { + object : NestedScrollConnection { + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + val delta = if (orientation == Orientation.Horizontal) consumed.x else consumed.y + if (delta != 0f) scrolled.tryEmit(Unit) + return Offset.Zero + } + } + } + + val alpha = remember { Animatable(0f) } + LaunchedEffect(scrolled, alpha) { + scrolled + .sample(100) + .collectLatest { + alpha.snapTo(1f) + alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + } } - } - val alpha = remember { Animatable(0f) } - LaunchedEffect(scrolled, alpha) { - scrolled - .sample(100) - .collectLatest { - alpha.snapTo(1f) - alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val reverseDirection = + if (orientation == Orientation.Horizontal) { + if (isLtr) reverseScrolling else !reverseScrolling + } else { + reverseScrolling } - } + val atEnd = if (orientation == Orientation.Vertical) isLtr else true - val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val reverseDirection = if (orientation == Orientation.Horizontal) { - if (isLtr) reverseScrolling else !reverseScrolling - } else { - reverseScrolling + val context = LocalContext.current + val thickness = remember { ViewConfiguration.get(context).scaledScrollBarSize.toFloat() } + val color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.364f) + Modifier + .nestedScroll(nestedScrollConnection) + .drawWithContent { + onDraw(reverseDirection, atEnd, thickness, color, alpha::value) + } } - val atEnd = if (orientation == Orientation.Vertical) isLtr else true - val context = LocalContext.current - val thickness = remember { ViewConfiguration.get(context).scaledScrollBarSize.toFloat() } - val color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.364f) - Modifier - .nestedScroll(nestedScrollConnection) - .drawWithContent { - onDraw(reverseDirection, atEnd, thickness, color, alpha::value) - } -} - -private val FadeOutAnimationSpec = tween( - durationMillis = ViewConfiguration.getScrollBarFadeDuration(), - delayMillis = ViewConfiguration.getScrollDefaultDelay(), -) +private val FadeOutAnimationSpec = + tween( + durationMillis = ViewConfiguration.getScrollBarFadeDuration(), + delayMillis = ViewConfiguration.getScrollDefaultDelay(), + ) @Preview(widthDp = 400, heightDp = 400, showBackground = true) @Composable @@ -253,9 +273,10 @@ fun LazyListScrollbarPreview() { items(50) { Text( text = "Item ${it + 1}", - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), ) } } @@ -272,8 +293,9 @@ fun LazyListHorizontalScrollbarPreview() { items(50) { Text( text = (it + 1).toString(), - modifier = Modifier - .padding(horizontal = 8.dp, vertical = 16.dp), + modifier = + Modifier + .padding(horizontal = 8.dp, vertical = 16.dp), ) } } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt index bc9c08faf9..0a57e0df3f 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt @@ -56,7 +56,6 @@ abstract class BaseUpdatesGridGlanceWidget( private val getUpdates: GetUpdates = Injekt.get(), private val preferences: SecurityPreferences = Injekt.get(), ) : GlanceAppWidget() { - override val sizeMode = SizeMode.Exact abstract val foreground: ColorProvider @@ -64,21 +63,26 @@ abstract class BaseUpdatesGridGlanceWidget( abstract val topPadding: Dp abstract val bottomPadding: Dp - override suspend fun provideGlance(context: Context, id: GlanceId) { + override suspend fun provideGlance( + context: Context, + id: GlanceId, + ) { val locked = preferences.useAuthenticator().get() - val containerModifier = GlanceModifier - .fillMaxSize() - .background(background) - .appWidgetBackground() - .padding(top = topPadding, bottom = bottomPadding) - .appWidgetBackgroundRadius() + val containerModifier = + GlanceModifier + .fillMaxSize() + .background(background) + .appWidgetBackground() + .padding(top = topPadding, bottom = bottomPadding) + .appWidgetBackgroundRadius() val manager = GlanceAppWidgetManager(context) val ids = manager.getGlanceIds(javaClass) - val (rowCount, columnCount) = ids - .flatMap { manager.getAppWidgetSizes(it) } - .maxBy { it.height.value * it.width.value } - .calculateRowAndColumnCount(topPadding, bottomPadding) + val (rowCount, columnCount) = + ids + .flatMap { manager.getAppWidgetSizes(it) } + .maxBy { it.height.value * it.width.value } + .calculateRowAndColumnCount(topPadding, bottomPadding) provideContent { // If app lock enabled, don't do anything @@ -90,13 +94,14 @@ abstract class BaseUpdatesGridGlanceWidget( return@provideContent } - val flow = remember { - getUpdates - .subscribe(false, DateLimit.toEpochMilli()) - .map { rawData -> - rawData.prepareData(rowCount, columnCount) - } - } + val flow = + remember { + getUpdates + .subscribe(false, DateLimit.toEpochMilli()) + .map { rawData -> + rawData.prepareData(rowCount, columnCount) + } + } val data by flow.collectAsState(initial = null) UpdatesWidget( data = data, @@ -122,35 +127,36 @@ abstract class BaseUpdatesGridGlanceWidget( .distinctBy { it.mangaId } .take(rowCount * columnCount) .map { updatesView -> - val request = ImageRequest.Builder(context) - .data( - MangaCover( - mangaId = updatesView.mangaId, - sourceId = updatesView.sourceId, - isMangaFavorite = true, - url = updatesView.coverData.url, - lastModified = updatesView.coverData.lastModified, - ), - ) - .memoryCachePolicy(CachePolicy.DISABLED) - .precision(Precision.EXACT) - .size(widthPx, heightPx) - .scale(Scale.FILL) - .let { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - it.transformations(RoundedCornersTransformation(roundPx)) - } else { - it // Handled by system - } - } - .build() - val bitmap = context.imageLoader.executeBlocking(request) - .image - ?.asDrawable(context.resources) - ?.toBitmap() + val request = + ImageRequest + .Builder(context) + .data( + MangaCover( + mangaId = updatesView.mangaId, + sourceId = updatesView.sourceId, + isMangaFavorite = true, + url = updatesView.coverData.url, + lastModified = updatesView.coverData.lastModified, + ), + ).memoryCachePolicy(CachePolicy.DISABLED) + .precision(Precision.EXACT) + .size(widthPx, heightPx) + .scale(Scale.FILL) + .let { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + it.transformations(RoundedCornersTransformation(roundPx)) + } else { + it // Handled by system + } + }.build() + val bitmap = + context.imageLoader + .executeBlocking(request) + .image + ?.asDrawable(context.resources) + ?.toBitmap() Pair(updatesView.mangaId, bitmap) - } - .toImmutableList() + }.toImmutableList() } } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt index eda0ff0a15..dec0b577e5 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt @@ -16,14 +16,12 @@ class WidgetManager( private val getUpdates: GetUpdates, private val securityPreferences: SecurityPreferences, ) { - fun Context.init(scope: LifecycleCoroutineScope) { combine( getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit.toEpochMilli()), securityPreferences.useAuthenticator().changes(), transform = { a, _ -> a }, - ) - .distinctUntilChanged() + ).distinctUntilChanged() .onEach { try { UpdatesGridGlanceWidget().updateAll(this) @@ -31,7 +29,6 @@ class WidgetManager( } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to update widget" } } - } - .launchIn(scope) + }.launchIn(scope) } } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt index afa422abf9..539cc07c05 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt @@ -24,22 +24,25 @@ fun LockedWidget( foreground: ColorProvider, modifier: GlanceModifier = GlanceModifier, ) { - val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + val intent = + Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } Box( - modifier = modifier - .clickable(actionStartActivity(intent)) - .padding(8.dp), + modifier = + modifier + .clickable(actionStartActivity(intent)) + .padding(8.dp), contentAlignment = Alignment.Center, ) { Text( text = stringResource(MR.strings.appwidget_unavailable_locked), - style = TextStyle( - color = foreground, - fontSize = 12.sp, - textAlign = TextAlign.Center, - ), + style = + TextStyle( + color = foreground, + fontSize = 12.sp, + textAlign = TextAlign.Center, + ), ) } } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt index 076d25d749..78989b9ec0 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt @@ -22,17 +22,19 @@ fun UpdatesMangaCover( modifier: GlanceModifier = GlanceModifier, ) { Box( - modifier = modifier - .size(width = CoverWidth, height = CoverHeight) - .appWidgetInnerRadius(), + modifier = + modifier + .size(width = CoverWidth, height = CoverHeight) + .appWidgetInnerRadius(), ) { if (cover != null) { Image( provider = ImageProvider(cover), contentDescription = null, - modifier = GlanceModifier - .fillMaxSize() - .appWidgetInnerRadius(), + modifier = + GlanceModifier + .fillMaxSize() + .appWidgetInnerRadius(), contentScale = ContentScale.Crop, ) } else { diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt index e90e079830..8c22b0459e 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt @@ -54,35 +54,39 @@ fun UpdatesWidget( horizontalAlignment = Alignment.CenterHorizontally, ) { (0.. - val coverRow = (0.. - data.getOrNull(j + (i * columnCount)) - } + val coverRow = + (0.. + data.getOrNull(j + (i * columnCount)) + } if (coverRow.isNotEmpty()) { Row( - modifier = GlanceModifier - .padding(vertical = 4.dp) - .fillMaxWidth(), + modifier = + GlanceModifier + .padding(vertical = 4.dp) + .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalAlignment = Alignment.CenterVertically, ) { coverRow.forEach { (mangaId, cover) -> Box( - modifier = GlanceModifier - .padding(horizontal = 3.dp), + modifier = + GlanceModifier + .padding(horizontal = 3.dp), contentAlignment = Alignment.Center, ) { - val intent = Intent( - LocalContext.current, - Class.forName(Constants.MAIN_ACTIVITY), - ).apply { - action = Constants.SHORTCUT_MANGA - putExtra(Constants.MANGA_EXTRA, mangaId) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + val intent = + Intent( + LocalContext.current, + Class.forName(Constants.MAIN_ACTIVITY), + ).apply { + action = Constants.SHORTCUT_MANGA + putExtra(Constants.MANGA_EXTRA, mangaId) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - // https://issuetracker.google.com/issues/238793260 - addCategory(mangaId.toString()) - } + // https://issuetracker.google.com/issues/238793260 + addCategory(mangaId.toString()) + } UpdatesMangaCover( cover = cover, modifier = GlanceModifier.clickable(actionStartActivity(intent)), diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt index d6d545c315..b051ad527a 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt @@ -6,13 +6,9 @@ import androidx.glance.GlanceModifier import androidx.glance.appwidget.cornerRadius import tachiyomi.presentation.widget.R -fun GlanceModifier.appWidgetBackgroundRadius(): GlanceModifier { - return this.cornerRadius(R.dimen.appwidget_background_radius) -} +fun GlanceModifier.appWidgetBackgroundRadius(): GlanceModifier = this.cornerRadius(R.dimen.appwidget_background_radius) -fun GlanceModifier.appWidgetInnerRadius(): GlanceModifier { - return this.cornerRadius(R.dimen.appwidget_inner_radius) -} +fun GlanceModifier.appWidgetInnerRadius(): GlanceModifier = this.cornerRadius(R.dimen.appwidget_inner_radius) /** * Calculates row-column count. diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt index 9be6735411..2342304588 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -6,7 +6,6 @@ import rx.Observable import tachiyomi.core.common.util.lang.awaitSingle interface CatalogueSource : Source { - /** * An ISO 639-1 compliant language code (two letters in lower case). */ @@ -24,9 +23,7 @@ interface CatalogueSource : Source { * @param page the page number to retrieve. */ @Suppress("DEPRECATION") - suspend fun getPopularManga(page: Int): MangasPage { - return fetchPopularManga(page).awaitSingle() - } + suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle() /** * Get a page with a list of manga. @@ -37,9 +34,11 @@ interface CatalogueSource : Source { * @param filters the list of filters to apply. */ @Suppress("DEPRECATION") - suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { - return fetchSearchManga(page, query, filters).awaitSingle() - } + suspend fun getSearchManga( + page: Int, + query: String, + filters: FilterList, + ): MangasPage = fetchSearchManga(page, query, filters).awaitSingle() /** * Get a page with a list of latest manga updates. @@ -48,9 +47,7 @@ interface CatalogueSource : Source { * @param page the page number to retrieve. */ @Suppress("DEPRECATION") - suspend fun getLatestUpdates(page: Int): MangasPage { - return fetchLatestUpdates(page).awaitSingle() - } + suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle() /** * Returns the list of filters for the source. @@ -61,20 +58,21 @@ interface CatalogueSource : Source { "Use the non-RxJava API instead", ReplaceWith("getPopularManga"), ) - fun fetchPopularManga(page: Int): Observable = - throw IllegalStateException("Not used") + fun fetchPopularManga(page: Int): Observable = throw IllegalStateException("Not used") @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getSearchManga"), ) - fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = - throw IllegalStateException("Not used") + fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable = throw IllegalStateException("Not used") @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getLatestUpdates"), ) - fun fetchLatestUpdates(page: Int): Observable = - throw IllegalStateException("Not used") + fun fetchLatestUpdates(page: Int): Observable = throw IllegalStateException("Not used") } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt index db9a985200..778049e685 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt @@ -7,7 +7,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get interface ConfigurableSource : Source { - /** * Gets instance of [SharedPreferences] scoped to the specific source. * diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index 83fcd79624..741118d1ac 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -10,7 +10,6 @@ import rx.Observable * A basic interface for creating a source. It could be an online source, a local source, etc. */ interface Source { - /** * ID for the source. Must be unique. */ @@ -32,9 +31,7 @@ interface Source { * @return the updated manga. */ @Suppress("DEPRECATION") - suspend fun getMangaDetails(manga: SManga): SManga { - return fetchMangaDetails(manga).awaitSingle() - } + suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle() /** * Get all the available chapters for a manga. @@ -44,9 +41,7 @@ interface Source { * @return the chapters for the manga. */ @Suppress("DEPRECATION") - suspend fun getChapterList(manga: SManga): List { - return fetchChapterList(manga).awaitSingle() - } + suspend fun getChapterList(manga: SManga): List = fetchChapterList(manga).awaitSingle() /** * Get the list of pages a chapter has. Pages should be returned @@ -57,28 +52,23 @@ interface Source { * @return the pages for the chapter. */ @Suppress("DEPRECATION") - suspend fun getPageList(chapter: SChapter): List { - return fetchPageList(chapter).awaitSingle() - } + suspend fun getPageList(chapter: SChapter): List = fetchPageList(chapter).awaitSingle() @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getMangaDetails"), ) - fun fetchMangaDetails(manga: SManga): Observable = - throw IllegalStateException("Not used") + fun fetchMangaDetails(manga: SManga): Observable = throw IllegalStateException("Not used") @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getChapterList"), ) - fun fetchChapterList(manga: SManga): Observable> = - throw IllegalStateException("Not used") + fun fetchChapterList(manga: SManga): Observable> = throw IllegalStateException("Not used") @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getPageList"), ) - fun fetchPageList(chapter: SChapter): Observable> = - throw IllegalStateException("Not used") + fun fetchPageList(chapter: SChapter): Observable> = throw IllegalStateException("Not used") } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt index 99c72e58e9..2cd9a138b2 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt @@ -1,17 +1,44 @@ package eu.kanade.tachiyomi.source.model -sealed class Filter(val name: String, var state: T) { - open class Header(name: String) : Filter(name, 0) - open class Separator(name: String = "") : Filter(name, 0) - abstract class Select(name: String, val values: Array, state: Int = 0) : Filter( - name, - state, - ) - abstract class Text(name: String, state: String = "") : Filter(name, state) - abstract class CheckBox(name: String, state: Boolean = false) : Filter(name, state) - abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter(name, state) { +sealed class Filter( + val name: String, + var state: T, +) { + open class Header( + name: String, + ) : Filter(name, 0) + + open class Separator( + name: String = "", + ) : Filter(name, 0) + + abstract class Select( + name: String, + val values: Array, + state: Int = 0, + ) : Filter( + name, + state, + ) + + abstract class Text( + name: String, + state: String = "", + ) : Filter(name, state) + + abstract class CheckBox( + name: String, + state: Boolean = false, + ) : Filter(name, state) + + abstract class TriState( + name: String, + state: Int = STATE_IGNORE, + ) : Filter(name, state) { fun isIgnored() = state == STATE_IGNORE + fun isIncluded() = state == STATE_INCLUDE + fun isExcluded() = state == STATE_EXCLUDE companion object { @@ -21,11 +48,20 @@ sealed class Filter(val name: String, var state: T) { } } - abstract class Group(name: String, state: List) : Filter>(name, state) + abstract class Group( + name: String, + state: List, + ) : Filter>(name, state) - abstract class Sort(name: String, val values: Array, state: Selection? = null) : - Filter(name, state) { - data class Selection(val index: Int, val ascending: Boolean) + abstract class Sort( + name: String, + val values: Array, + state: Selection? = null, + ) : Filter(name, state) { + data class Selection( + val index: Int, + val ascending: Boolean, + ) } override fun equals(other: Any?): Boolean { diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/FilterList.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/FilterList.kt index 6c99352665..7858e4c5e2 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/FilterList.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/FilterList.kt @@ -3,15 +3,12 @@ package eu.kanade.tachiyomi.source.model import androidx.compose.runtime.Stable @Stable -data class FilterList(val list: List>) : List> by list { - +data class FilterList( + val list: List>, +) : List> by list { constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) - override fun equals(other: Any?): Boolean { - return false - } + override fun equals(other: Any?): Boolean = false - override fun hashCode(): Int { - return list.hashCode() - } + override fun hashCode(): Int = list.hashCode() } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/MangasPage.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/MangasPage.kt index a377c36eaa..cda439bf0d 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/MangasPage.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/MangasPage.kt @@ -1,3 +1,6 @@ package eu.kanade.tachiyomi.source.model -data class MangasPage(val mangas: List, val hasNextPage: Boolean) +data class MangasPage( + val mangas: List, + val hasNextPage: Boolean, +) diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt index 7ce18934f3..896f94c448 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt @@ -14,7 +14,6 @@ open class Page( var imageUrl: String? = null, @Transient var uri: Uri? = null, // Deprecated but can't be deleted due to extensions ) : ProgressListener { - val number: Int get() = index + 1 @@ -40,12 +39,17 @@ open class Page( _progressFlow.value = value } - override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { - progress = if (contentLength > 0) { - (100 * bytesRead / contentLength).toInt() - } else { - -1 - } + override fun update( + bytesRead: Long, + contentLength: Long, + done: Boolean, + ) { + progress = + if (contentLength > 0) { + (100 * bytesRead / contentLength).toInt() + } else { + -1 + } } enum class State { diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapter.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapter.kt index f53bbe8f0a..79b7488586 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapter.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapter.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source.model import java.io.Serializable interface SChapter : Serializable { - var url: String var name: String @@ -23,8 +22,6 @@ interface SChapter : Serializable { } companion object { - fun create(): SChapter { - return SChapterImpl() - } + fun create(): SChapter = SChapterImpl() } } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapterImpl.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapterImpl.kt index 4d5e43f1e4..55b3f3ff0b 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapterImpl.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapterImpl.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.source.model class SChapterImpl : SChapter { - override lateinit var url: String override lateinit var name: String diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt index 490540abaf..8dfaf0f6b4 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source.model import java.io.Serializable interface SManga : Serializable { - var url: String var title: String @@ -26,21 +25,26 @@ interface SManga : Serializable { fun getGenres(): List? { if (genre.isNullOrBlank()) return null - return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct() + return genre + ?.split(", ") + ?.map { it.trim() } + ?.filterNot { it.isBlank() } + ?.distinct() } - fun copy() = create().also { - it.url = url - it.title = title - it.artist = artist - it.author = author - it.description = description - it.genre = genre - it.status = status - it.thumbnail_url = thumbnail_url - it.update_strategy = update_strategy - it.initialized = initialized - } + fun copy() = + create().also { + it.url = url + it.title = title + it.artist = artist + it.author = author + it.description = description + it.genre = genre + it.status = status + it.thumbnail_url = thumbnail_url + it.update_strategy = update_strategy + it.initialized = initialized + } companion object { const val UNKNOWN = 0 @@ -51,8 +55,6 @@ interface SManga : Serializable { const val CANCELLED = 5 const val ON_HIATUS = 6 - fun create(): SManga { - return SMangaImpl() - } + fun create(): SManga = SMangaImpl() } } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaImpl.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaImpl.kt index 91a7711cce..b2d6b33764 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaImpl.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaImpl.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.source.model class SMangaImpl : SManga { - override lateinit var url: String override lateinit var title: String 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 7ffa538b58..5c89aab1e3 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 @@ -27,7 +27,6 @@ import java.security.MessageDigest */ @Suppress("unused") abstract class HttpSource : CatalogueSource { - /** * Network service. */ @@ -84,7 +83,11 @@ abstract class HttpSource : CatalogueSource { * @return a unique ID for the source */ @Suppress("MemberVisibilityCanBePrivate") - protected fun generateId(name: String, lang: String, versionId: Int): Long { + protected fun generateId( + name: String, + lang: String, + versionId: Int, + ): Long { val key = "${name.lowercase()}/$lang/$versionId" val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE @@ -93,9 +96,10 @@ abstract class HttpSource : CatalogueSource { /** * Headers builder for requests. Implementations can override this method for custom headers. */ - protected open fun headersBuilder() = Headers.Builder().apply { - add("User-Agent", network.defaultUserAgentProvider()) - } + protected open fun headersBuilder() = + Headers.Builder().apply { + add("User-Agent", network.defaultUserAgentProvider()) + } /** * Visible name of the source. @@ -109,13 +113,13 @@ abstract class HttpSource : CatalogueSource { * @param page the page number to retrieve. */ @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga")) - override fun fetchPopularManga(page: Int): Observable { - return client.newCall(popularMangaRequest(page)) + override fun fetchPopularManga(page: Int): Observable = + client + .newCall(popularMangaRequest(page)) .asObservableSuccess() .map { response -> popularMangaParse(response) } - } /** * Returns the request for the popular manga given the page. @@ -144,20 +148,19 @@ abstract class HttpSource : CatalogueSource { page: Int, query: String, filters: FilterList, - ): Observable { - return Observable.defer { - try { - client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess() - } catch (e: NoClassDefFoundError) { - // RxJava doesn't handle Errors, which tends to happen during global searches - // if an old extension using non-existent classes is still around - throw RuntimeException(e) - } - } - .map { response -> + ): Observable = + Observable + .defer { + try { + client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess() + } catch (e: NoClassDefFoundError) { + // RxJava doesn't handle Errors, which tends to happen during global searches + // if an old extension using non-existent classes is still around + throw RuntimeException(e) + } + }.map { response -> searchMangaParse(response) } - } /** * Returns the request for the search manga given the page. @@ -185,13 +188,13 @@ abstract class HttpSource : CatalogueSource { * @param page the page number to retrieve. */ @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates")) - override fun fetchLatestUpdates(page: Int): Observable { - return client.newCall(latestUpdatesRequest(page)) + override fun fetchLatestUpdates(page: Int): Observable = + client + .newCall(latestUpdatesRequest(page)) .asObservableSuccess() .map { response -> latestUpdatesParse(response) } - } /** * Returns the request for latest manga given the page. @@ -215,18 +218,16 @@ abstract class HttpSource : CatalogueSource { * @return the updated manga. */ @Suppress("DEPRECATION") - override suspend fun getMangaDetails(manga: SManga): SManga { - return fetchMangaDetails(manga).awaitSingle() - } + override suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle() @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails")) - override fun fetchMangaDetails(manga: SManga): Observable { - return client.newCall(mangaDetailsRequest(manga)) + override fun fetchMangaDetails(manga: SManga): Observable = + client + .newCall(mangaDetailsRequest(manga)) .asObservableSuccess() .map { response -> mangaDetailsParse(response).apply { initialized = true } } - } /** * Returns the request for the details of a manga. Override only if it's needed to change the @@ -234,9 +235,7 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to be updated. */ - open fun mangaDetailsRequest(manga: SManga): Request { - return GET(baseUrl + manga.url, headers) - } + open fun mangaDetailsRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers) /** * Parses the response from the site and returns the details of a manga. @@ -263,9 +262,10 @@ abstract class HttpSource : CatalogueSource { } @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList")) - override fun fetchChapterList(manga: SManga): Observable> { - return if (manga.status != SManga.LICENSED) { - client.newCall(chapterListRequest(manga)) + override fun fetchChapterList(manga: SManga): Observable> = + if (manga.status != SManga.LICENSED) { + client + .newCall(chapterListRequest(manga)) .asObservableSuccess() .map { response -> chapterListParse(response) @@ -273,7 +273,6 @@ abstract class HttpSource : CatalogueSource { } else { Observable.error(LicensedMangaChaptersException()) } - } /** * Returns the request for updating the chapter list. Override only if it's needed to override @@ -281,9 +280,7 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to look for chapters. */ - protected open fun chapterListRequest(manga: SManga): Request { - return GET(baseUrl + manga.url, headers) - } + protected open fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers) /** * Parses the response from the site and returns a list of chapters. @@ -307,18 +304,16 @@ abstract class HttpSource : CatalogueSource { * @return the pages for the chapter. */ @Suppress("DEPRECATION") - override suspend fun getPageList(chapter: SChapter): List { - return fetchPageList(chapter).awaitSingle() - } + override suspend fun getPageList(chapter: SChapter): List = fetchPageList(chapter).awaitSingle() @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList")) - override fun fetchPageList(chapter: SChapter): Observable> { - return client.newCall(pageListRequest(chapter)) + override fun fetchPageList(chapter: SChapter): Observable> = + client + .newCall(pageListRequest(chapter)) .asObservableSuccess() .map { response -> pageListParse(response) } - } /** * Returns the request for getting the page list. Override only if it's needed to override the @@ -326,9 +321,7 @@ abstract class HttpSource : CatalogueSource { * * @param chapter the chapter whose page list has to be fetched. */ - protected open fun pageListRequest(chapter: SChapter): Request { - return GET(baseUrl + chapter.url, headers) - } + protected open fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + chapter.url, headers) /** * Parses the response from the site and returns a list of pages. @@ -345,16 +338,14 @@ abstract class HttpSource : CatalogueSource { * @param page the page whose source image has to be fetched. */ @Suppress("DEPRECATION") - open suspend fun getImageUrl(page: Page): String { - return fetchImageUrl(page).awaitSingle() - } + open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle() @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) - open fun fetchImageUrl(page: Page): Observable { - return client.newCall(imageUrlRequest(page)) + open fun fetchImageUrl(page: Page): Observable = + client + .newCall(imageUrlRequest(page)) .asObservableSuccess() .map { imageUrlParse(it) } - } /** * Returns the request for getting the url to the source image. Override only if it's needed to @@ -362,9 +353,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the chapter whose page list has to be fetched */ - protected open fun imageUrlRequest(page: Page): Request { - return GET(page.url, headers) - } + protected open fun imageUrlRequest(page: Page): Request = GET(page.url, headers) /** * Parses the response from the site and returns the absolute url to the source image. @@ -380,10 +369,10 @@ abstract class HttpSource : CatalogueSource { * @since extensions-lib 1.5 * @param page the page whose source image has to be downloaded. */ - open suspend fun getImage(page: Page): Response { - return client.newCachelessCallWithProgress(imageRequest(page), page) + open suspend fun getImage(page: Page): Response = + client + .newCachelessCallWithProgress(imageRequest(page), page) .awaitSuccess() - } /** * Returns the request for getting the source image. Override only if it's needed to override @@ -391,9 +380,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the chapter whose page list has to be fetched */ - protected open fun imageRequest(page: Page): Request { - return GET(page.imageUrl!!, headers) - } + protected open fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers) /** * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from @@ -420,8 +407,8 @@ abstract class HttpSource : CatalogueSource { * * @param orig the full url. */ - private fun getUrlWithoutDomain(orig: String): String { - return try { + private fun getUrlWithoutDomain(orig: String): String = + try { val uri = URI(orig.replace(" ", "%20")) var out = uri.path if (uri.query != null) { @@ -434,7 +421,6 @@ abstract class HttpSource : CatalogueSource { } catch (e: URISyntaxException) { orig } - } /** * Returns the url of the provided manga @@ -443,9 +429,7 @@ abstract class HttpSource : CatalogueSource { * @param manga the manga * @return url of the manga */ - open fun getMangaUrl(manga: SManga): String { - return mangaDetailsRequest(manga).url.toString() - } + open fun getMangaUrl(manga: SManga): String = mangaDetailsRequest(manga).url.toString() /** * Returns the url of the provided chapter @@ -454,9 +438,7 @@ abstract class HttpSource : CatalogueSource { * @param chapter the chapter * @return url of the chapter */ - open fun getChapterUrl(chapter: SChapter): String { - return pageListRequest(chapter).url.toString() - } + open fun getChapterUrl(chapter: SChapter): String = pageListRequest(chapter).url.toString() /** * Called before inserting a new chapter into database. Use it if you need to override chapter @@ -465,7 +447,10 @@ abstract class HttpSource : CatalogueSource { * @param chapter the chapter to be added. * @param manga the manga of the chapter. */ - open fun prepareNewChapter(chapter: SChapter, manga: SManga) {} + open fun prepareNewChapter( + chapter: SChapter, + manga: SManga, + ) {} /** * Returns the list of filters for the source. diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt index 34376f847e..9ad729d2aa 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt @@ -14,7 +14,6 @@ import org.jsoup.nodes.Element */ @Suppress("unused") abstract class ParsedHttpSource : HttpSource() { - /** * Parses the response from the site and returns a [MangasPage] object. * @@ -23,13 +22,15 @@ abstract class ParsedHttpSource : HttpSource() { override fun popularMangaParse(response: Response): MangasPage { val document = response.asJsoup() - val mangas = document.select(popularMangaSelector()).map { element -> - popularMangaFromElement(element) - } + val mangas = + document.select(popularMangaSelector()).map { element -> + popularMangaFromElement(element) + } - val hasNextPage = popularMangaNextPageSelector()?.let { selector -> - document.select(selector).first() - } != null + val hasNextPage = + popularMangaNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null return MangasPage(mangas, hasNextPage) } @@ -61,13 +62,15 @@ abstract class ParsedHttpSource : HttpSource() { override fun searchMangaParse(response: Response): MangasPage { val document = response.asJsoup() - val mangas = document.select(searchMangaSelector()).map { element -> - searchMangaFromElement(element) - } + val mangas = + document.select(searchMangaSelector()).map { element -> + searchMangaFromElement(element) + } - val hasNextPage = searchMangaNextPageSelector()?.let { selector -> - document.select(selector).first() - } != null + val hasNextPage = + searchMangaNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null return MangasPage(mangas, hasNextPage) } @@ -99,13 +102,15 @@ abstract class ParsedHttpSource : HttpSource() { override fun latestUpdatesParse(response: Response): MangasPage { val document = response.asJsoup() - val mangas = document.select(latestUpdatesSelector()).map { element -> - latestUpdatesFromElement(element) - } + val mangas = + document.select(latestUpdatesSelector()).map { element -> + latestUpdatesFromElement(element) + } - val hasNextPage = latestUpdatesNextPageSelector()?.let { selector -> - document.select(selector).first() - } != null + val hasNextPage = + latestUpdatesNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null return MangasPage(mangas, hasNextPage) } @@ -134,9 +139,7 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun mangaDetailsParse(response: Response): SManga { - return mangaDetailsParse(response.asJsoup()) - } + override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup()) /** * Returns the details of the manga from the given [document]. @@ -172,9 +175,7 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun pageListParse(response: Response): List { - return pageListParse(response.asJsoup()) - } + override fun pageListParse(response: Response): List = pageListParse(response.asJsoup()) /** * Returns a page list from the given document. @@ -188,9 +189,7 @@ abstract class ParsedHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun imageUrlParse(response: Response): String { - return imageUrlParse(response.asJsoup()) - } + override fun imageUrlParse(response: Response): String = imageUrlParse(response.asJsoup()) /** * Returns the absolute url to the source image from the document. diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt index dd06ff3015..59f732cf85 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.model.SManga * @since extensions-lib 1.5 */ interface ResolvableSource : Source { - /** * Returns what the given URI may open. * Returns [UriType.Unknown] if the source is not able to resolve the URI. @@ -38,6 +37,8 @@ interface ResolvableSource : Source { sealed interface UriType { data object Manga : UriType + data object Chapter : UriType + data object Unknown : UriType } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/JsoupExtensions.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/JsoupExtensions.kt index 6c166448a5..b916907664 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/JsoupExtensions.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/JsoupExtensions.kt @@ -5,22 +5,20 @@ import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.jsoup.nodes.Element -fun Element.selectText(css: String, defaultValue: String? = null): String? { - return select(css).first()?.text() ?: defaultValue -} +fun Element.selectText( + css: String, + defaultValue: String? = null, +): String? = select(css).first()?.text() ?: defaultValue -fun Element.selectInt(css: String, defaultValue: Int = 0): Int { - return select(css).first()?.text()?.toInt() ?: defaultValue -} +fun Element.selectInt( + css: String, + defaultValue: Int = 0, +): Int = select(css).first()?.text()?.toInt() ?: defaultValue -fun Element.attrOrText(css: String): String { - return if (css != "text") attr(css) else text() -} +fun Element.attrOrText(css: String): String = if (css != "text") attr(css) else text() /** * Returns a Jsoup document for this response. * @param html the body of the response. Use only if the body was read before calling this method. */ -fun Response.asJsoup(html: String? = null): Document { - return Jsoup.parse(html ?: body.string(), request.url.toString()) -} +fun Response.asJsoup(html: String? = null): Document = Jsoup.parse(html ?: body.string(), request.url.toString()) 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 2d9725ad85..72f850bf42 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -50,8 +50,8 @@ actual class LocalSource( private val context: Context, private val fileSystem: LocalSourceFileSystem, private val coverManager: LocalCoverManager, -) : CatalogueSource, UnmeteredSource { - +) : CatalogueSource, + UnmeteredSource { private val json: Json by injectLazy() private val xml: XML by injectLazy() @@ -73,137 +73,156 @@ actual class LocalSource( override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS) - override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = withIOContext { - val lastModifiedLimit = if (filters === LATEST_FILTERS) { - System.currentTimeMillis() - LATEST_THRESHOLD - } else { - 0L - } - - var mangaDirs = fileSystem.getFilesInBaseDirectory() - // Filter out files that are hidden and is not a folder - .filter { it.isDirectory && !it.name.orEmpty().startsWith('.') } - .distinctBy { it.name } - .filter { - if (lastModifiedLimit == 0L && query.isBlank()) { - true - } else if (lastModifiedLimit == 0L) { - it.name.orEmpty().contains(query, ignoreCase = true) + override suspend fun getSearchManga( + page: Int, + query: String, + filters: FilterList, + ): MangasPage = + withIOContext { + val lastModifiedLimit = + if (filters === LATEST_FILTERS) { + System.currentTimeMillis() - LATEST_THRESHOLD } else { - it.lastModified() >= lastModifiedLimit + 0L } - } - filters.forEach { filter -> - when (filter) { - is OrderBy.Popular -> { - mangaDirs = if (filter.state!!.ascending) { - mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() }) - } else { - mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() }) - } - } - is OrderBy.Latest -> { - mangaDirs = if (filter.state!!.ascending) { - mangaDirs.sortedBy(UniFile::lastModified) - } else { - mangaDirs.sortedByDescending(UniFile::lastModified) + var mangaDirs = + fileSystem + .getFilesInBaseDirectory() + // Filter out files that are hidden and is not a folder + .filter { it.isDirectory && !it.name.orEmpty().startsWith('.') } + .distinctBy { it.name } + .filter { + if (lastModifiedLimit == 0L && query.isBlank()) { + true + } else if (lastModifiedLimit == 0L) { + it.name.orEmpty().contains(query, ignoreCase = true) + } else { + it.lastModified() >= lastModifiedLimit + } } - } - else -> { - /* Do nothing */ - } - } - } - val mangas = mangaDirs - .map { mangaDir -> - async { - SManga.create().apply { - title = mangaDir.name.orEmpty() - url = mangaDir.name.orEmpty() - - // Try to find the cover - coverManager.find(mangaDir.name.orEmpty())?.let { - thumbnail_url = it.uri.toString() - } + filters.forEach { filter -> + when (filter) { + is OrderBy.Popular -> { + mangaDirs = + if (filter.state!!.ascending) { + mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() }) + } else { + mangaDirs.sortedWith( + compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() }, + ) + } + } + is OrderBy.Latest -> { + mangaDirs = + if (filter.state!!.ascending) { + mangaDirs.sortedBy(UniFile::lastModified) + } else { + mangaDirs.sortedByDescending(UniFile::lastModified) + } + } + else -> { + // Do nothing } } } - .awaitAll() - MangasPage(mangas, false) - } + val mangas = + mangaDirs + .map { mangaDir -> + async { + SManga.create().apply { + title = mangaDir.name.orEmpty() + url = mangaDir.name.orEmpty() + + // Try to find the cover + coverManager.find(mangaDir.name.orEmpty())?.let { + thumbnail_url = it.uri.toString() + } + } + } + }.awaitAll() - // Manga details related - override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { - coverManager.find(manga.url)?.let { - manga.thumbnail_url = it.uri.toString() + MangasPage(mangas, false) } - // Augment manga details based on metadata files - try { - val mangaDir = fileSystem.getMangaDirectory(manga.url) ?: error("${manga.url} is not a valid directory") - val mangaDirFiles = mangaDir.listFiles().orEmpty() - - val comicInfoFile = mangaDirFiles - .firstOrNull { it.name == COMIC_INFO_FILE } - val noXmlFile = mangaDirFiles - .firstOrNull { it.name == ".noxml" } - val legacyJsonDetailsFile = mangaDirFiles - .firstOrNull { it.extension == "json" } - - when { - // Top level ComicInfo.xml - comicInfoFile != null -> { - noXmlFile?.delete() - setMangaDetailsFromComicInfoFile(comicInfoFile.openInputStream(), manga) - } + // Manga details related + override suspend fun getMangaDetails(manga: SManga): SManga = + withIOContext { + coverManager.find(manga.url)?.let { + manga.thumbnail_url = it.uri.toString() + } - // Old custom JSON format - // TODO: remove support for this entirely after a while - legacyJsonDetailsFile != null -> { - json.decodeFromStream(legacyJsonDetailsFile.openInputStream()).run { - title?.let { manga.title = it } - author?.let { manga.author = it } - artist?.let { manga.artist = it } - description?.let { manga.description = it } - genre?.let { manga.genre = it.joinToString() } - status?.let { manga.status = it } + // Augment manga details based on metadata files + try { + val mangaDir = fileSystem.getMangaDirectory(manga.url) ?: error("${manga.url} is not a valid directory") + val mangaDirFiles = mangaDir.listFiles().orEmpty() + + val comicInfoFile = + mangaDirFiles + .firstOrNull { it.name == COMIC_INFO_FILE } + val noXmlFile = + mangaDirFiles + .firstOrNull { it.name == ".noxml" } + val legacyJsonDetailsFile = + mangaDirFiles + .firstOrNull { it.extension == "json" } + + when { + // Top level ComicInfo.xml + comicInfoFile != null -> { + noXmlFile?.delete() + setMangaDetailsFromComicInfoFile(comicInfoFile.openInputStream(), manga) } - // Replace with ComicInfo.xml file - val comicInfo = manga.getComicInfo() - mangaDir - .createFile(COMIC_INFO_FILE) - ?.openOutputStream() - ?.use { - val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo) - it.write(comicInfoString.toByteArray()) - legacyJsonDetailsFile.delete() + + // Old custom JSON format + // TODO: remove support for this entirely after a while + legacyJsonDetailsFile != null -> { + json.decodeFromStream(legacyJsonDetailsFile.openInputStream()).run { + title?.let { manga.title = it } + author?.let { manga.author = it } + artist?.let { manga.artist = it } + description?.let { manga.description = it } + genre?.let { manga.genre = it.joinToString() } + status?.let { manga.status = it } } - } + // Replace with ComicInfo.xml file + val comicInfo = manga.getComicInfo() + mangaDir + .createFile(COMIC_INFO_FILE) + ?.openOutputStream() + ?.use { + val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo) + it.write(comicInfoString.toByteArray()) + legacyJsonDetailsFile.delete() + } + } - // Copy ComicInfo.xml from chapter archive to top level if found - noXmlFile == null -> { - val chapterArchives = mangaDirFiles.filter(Archive::isSupported) + // Copy ComicInfo.xml from chapter archive to top level if found + noXmlFile == null -> { + val chapterArchives = mangaDirFiles.filter(Archive::isSupported) - val copiedFile = copyComicInfoFileFromArchive(chapterArchives, mangaDir) - if (copiedFile != null) { - setMangaDetailsFromComicInfoFile(copiedFile.openInputStream(), manga) - } else { - // Avoid re-scanning - mangaDir.createFile(".noxml") + val copiedFile = copyComicInfoFileFromArchive(chapterArchives, mangaDir) + if (copiedFile != null) { + setMangaDetailsFromComicInfoFile(copiedFile.openInputStream(), manga) + } else { + // Avoid re-scanning + mangaDir.createFile(".noxml") + } } } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) { "Error setting manga details from local metadata for ${manga.title}" } } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) { "Error setting manga details from local metadata for ${manga.title}" } - } - return@withIOContext manga - } + return@withIOContext manga + } - private fun copyComicInfoFileFromArchive(chapterArchives: List, folder: UniFile): UniFile? { + private fun copyComicInfoFileFromArchive( + chapterArchives: List, + folder: UniFile, + ): UniFile? { for (chapter in chapterArchives) { chapter.archiveReader(context).use { reader -> reader.getInputStream(COMIC_INFO_FILE)?.use { stream -> @@ -214,63 +233,73 @@ actual class LocalSource( return null } - private fun copyComicInfoFile(comicInfoFileStream: InputStream, folder: UniFile): UniFile? { - return folder.createFile(COMIC_INFO_FILE)?.apply { + private fun copyComicInfoFile( + comicInfoFileStream: InputStream, + folder: UniFile, + ): UniFile? = + folder.createFile(COMIC_INFO_FILE)?.apply { openOutputStream().use { outputStream -> comicInfoFileStream.use { it.copyTo(outputStream) } } } - } - private fun setMangaDetailsFromComicInfoFile(stream: InputStream, manga: SManga) { - val comicInfo = AndroidXmlReader(stream, StandardCharsets.UTF_8.name()).use { - xml.decodeFromReader(it) - } + private fun setMangaDetailsFromComicInfoFile( + stream: InputStream, + manga: SManga, + ) { + val comicInfo = + AndroidXmlReader(stream, StandardCharsets.UTF_8.name()).use { + xml.decodeFromReader(it) + } manga.copyFromComicInfo(comicInfo) } // Chapters - override suspend fun getChapterList(manga: SManga): List = withIOContext { - val chapters = fileSystem.getFilesInMangaDirectory(manga.url) - // Only keep supported formats - .filter { it.isDirectory || Archive.isSupported(it) || it.extension.equals("epub", true) } - .map { chapterFile -> - SChapter.create().apply { - url = "${manga.url}/${chapterFile.name}" - name = if (chapterFile.isDirectory) { - chapterFile.name - } else { - chapterFile.nameWithoutExtension - }.orEmpty() - date_upload = chapterFile.lastModified() - chapter_number = ChapterRecognition - .parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble()) - .toFloat() - - val format = Format.valueOf(chapterFile) - if (format is Format.Epub) { - EpubFile(format.file.archiveReader(context)).use { epub -> - epub.fillMetadata(manga, this) + override suspend fun getChapterList(manga: SManga): List = + withIOContext { + val chapters = + fileSystem + .getFilesInMangaDirectory(manga.url) + // Only keep supported formats + .filter { it.isDirectory || Archive.isSupported(it) || it.extension.equals("epub", true) } + .map { chapterFile -> + SChapter.create().apply { + url = "${manga.url}/${chapterFile.name}" + name = + if (chapterFile.isDirectory) { + chapterFile.name + } else { + chapterFile.nameWithoutExtension + }.orEmpty() + date_upload = chapterFile.lastModified() + chapter_number = + ChapterRecognition + .parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble()) + .toFloat() + + val format = Format.valueOf(chapterFile) + if (format is Format.Epub) { + EpubFile(format.file.archiveReader(context)).use { epub -> + epub.fillMetadata(manga, this) + } + } } + }.sortedWith { c1, c2 -> + val c = c2.chapter_number.compareTo(c1.chapter_number) + if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c } + + // Copy the cover from the first chapter found if not available + if (manga.thumbnail_url.isNullOrBlank()) { + chapters.lastOrNull()?.let { chapter -> + updateCover(chapter, manga) } } - .sortedWith { c1, c2 -> - val c = c2.chapter_number.compareTo(c1.chapter_number) - if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c - } - // Copy the cover from the first chapter found if not available - if (manga.thumbnail_url.isNullOrBlank()) { - chapters.lastOrNull()?.let { chapter -> - updateCover(chapter, manga) - } + chapters } - chapters - } - // Filters override fun getFilterList() = FilterList(OrderBy.Popular(context)) @@ -280,7 +309,8 @@ actual class LocalSource( fun getFormat(chapter: SChapter): Format { try { val (mangaDirName, chapterName) = chapter.url.split('/', limit = 2) - return fileSystem.getBaseDirectory() + return fileSystem + .getBaseDirectory() ?.findFile(mangaDirName) ?.findFile(chapterName) ?.let(Format.Companion::valueOf) @@ -292,29 +322,39 @@ actual class LocalSource( } } - private fun updateCover(chapter: SChapter, manga: SManga): UniFile? { - return try { + private fun updateCover( + chapter: SChapter, + manga: SManga, + ): UniFile? = + try { when (val format = getFormat(chapter)) { is Format.Directory -> { - val entry = format.file.listFiles() - ?.sortedWith { f1, f2 -> - f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder( - f2.name.orEmpty(), - ) - } - ?.find { - !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } - } + val entry = + format.file + .listFiles() + ?.sortedWith { f1, f2 -> + f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder( + f2.name.orEmpty(), + ) + }?.find { + !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } + } entry?.let { coverManager.update(manga, it.openInputStream()) } } is Format.Archive -> { format.file.archiveReader(context).use { reader -> - val entry = reader.useEntries { entries -> - entries - .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - .find { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } } - } + val entry = + reader.useEntries { entries -> + entries + .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + .find { + it.isFile && + ImageUtil.isImage( + it.name, + ) { reader.getInputStream(it.name)!! } + } + } entry?.let { coverManager.update(manga, reader.getInputStream(it.name)!!) } } @@ -331,7 +371,6 @@ actual class LocalSource( logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" } null } - } companion object { const val ID = 0L diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/filter/OrderBy.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/filter/OrderBy.kt index 956cceeeee..f32d4db455 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/filter/OrderBy.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/filter/OrderBy.kt @@ -5,11 +5,19 @@ import eu.kanade.tachiyomi.source.model.Filter import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR -sealed class OrderBy(context: Context, selection: Selection) : Filter.Sort( - context.stringResource(MR.strings.local_filter_order_by), - arrayOf(context.stringResource(MR.strings.title), context.stringResource(MR.strings.date)), - selection, -) { - class Popular(context: Context) : OrderBy(context, Selection(0, true)) - class Latest(context: Context) : OrderBy(context, Selection(1, false)) +sealed class OrderBy( + context: Context, + selection: Selection, +) : Filter.Sort( + context.stringResource(MR.strings.local_filter_order_by), + arrayOf(context.stringResource(MR.strings.title), context.stringResource(MR.strings.date)), + selection, + ) { + class Popular( + context: Context, + ) : OrderBy(context, Selection(0, true)) + + class Latest( + context: Context, + ) : OrderBy(context, Selection(1, false)) } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index a54e3e7e4e..e8f649edec 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt @@ -15,14 +15,13 @@ actual class LocalCoverManager( private val context: Context, private val fileSystem: LocalSourceFileSystem, ) { - - actual fun find(mangaUrl: String): UniFile? { - return fileSystem.getFilesInMangaDirectory(mangaUrl) + actual fun find(mangaUrl: String): UniFile? = + fileSystem + .getFilesInMangaDirectory(mangaUrl) // Get all file whose names start with "cover" .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) } // Get the first actual image .firstOrNull { ImageUtil.isImage(it.name) { it.openInputStream() } } - } actual fun update( manga: SManga, diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt index 69faaa2b77..034844d94b 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt @@ -6,22 +6,15 @@ import tachiyomi.domain.storage.service.StorageManager actual class LocalSourceFileSystem( private val storageManager: StorageManager, ) { + actual fun getBaseDirectory(): UniFile? = storageManager.getLocalSourceDirectory() - actual fun getBaseDirectory(): UniFile? { - return storageManager.getLocalSourceDirectory() - } + actual fun getFilesInBaseDirectory(): List = getBaseDirectory()?.listFiles().orEmpty().toList() - actual fun getFilesInBaseDirectory(): List { - return getBaseDirectory()?.listFiles().orEmpty().toList() - } - - actual fun getMangaDirectory(name: String): UniFile? { - return getBaseDirectory() + actual fun getMangaDirectory(name: String): UniFile? = + getBaseDirectory() ?.findFile(name) ?.takeIf { it.isDirectory } - } - actual fun getFilesInMangaDirectory(name: String): List { - return getMangaDirectory(name)?.listFiles().orEmpty().toList() - } + actual fun getFilesInMangaDirectory(name: String): List = + getMangaDirectory(name)?.listFiles().orEmpty().toList() } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt index 6bade530b2..9bc1476c04 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt @@ -10,7 +10,10 @@ import java.util.Locale /** * Fills manga and chapter metadata using this epub file's metadata. */ -fun EpubFile.fillMetadata(manga: SManga, chapter: SChapter) { +fun EpubFile.fillMetadata( + manga: SManga, + chapter: SChapter, +) { val ref = getPackageHref() val doc = getPackageDocument(ref) diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt index d592f1c961..20cb7f3a45 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -3,4 +3,6 @@ package tachiyomi.source.local import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.UnmeteredSource -expect class LocalSource : CatalogueSource, UnmeteredSource +expect class LocalSource : + CatalogueSource, + UnmeteredSource diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index 037d9f1dcb..32809591f8 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt @@ -5,8 +5,10 @@ import eu.kanade.tachiyomi.source.model.SManga import java.io.InputStream expect class LocalCoverManager { - fun find(mangaUrl: String): UniFile? - fun update(manga: SManga, inputStream: InputStream): UniFile? + fun update( + manga: SManga, + inputStream: InputStream, + ): UniFile? } diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt index ea18e9b53d..30d4cb2085 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt @@ -4,10 +4,7 @@ import com.hippo.unifile.UniFile import tachiyomi.core.common.storage.extension object Archive { - private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "7z", "cb7", "tar", "cbt") - fun isSupported(file: UniFile): Boolean { - return file.extension?.lowercase() in SUPPORTED_ARCHIVE_TYPES - } + fun isSupported(file: UniFile): Boolean = file.extension?.lowercase() in SUPPORTED_ARCHIVE_TYPES } diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt index ad53d407c7..d13662b41c 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt @@ -5,19 +5,27 @@ import tachiyomi.core.common.storage.extension import tachiyomi.source.local.io.Archive.isSupported as isArchiveSupported sealed interface Format { - data class Directory(val file: UniFile) : Format - data class Archive(val file: UniFile) : Format - data class Epub(val file: UniFile) : Format + data class Directory( + val file: UniFile, + ) : Format + + data class Archive( + val file: UniFile, + ) : Format + + data class Epub( + val file: UniFile, + ) : Format class UnknownFormatException : Exception() companion object { - - fun valueOf(file: UniFile) = when { - file.isDirectory -> Directory(file) - file.extension.equals("epub", true) -> Epub(file) - isArchiveSupported(file) -> Archive(file) - else -> throw UnknownFormatException() - } + fun valueOf(file: UniFile) = + when { + file.isDirectory -> Directory(file) + file.extension.equals("epub", true) -> Epub(file) + isArchiveSupported(file) -> Archive(file) + else -> throw UnknownFormatException() + } } } diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt index 5aa74d8517..aa9cdc2442 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt @@ -3,7 +3,6 @@ package tachiyomi.source.local.io import com.hippo.unifile.UniFile expect class LocalSourceFileSystem { - fun getBaseDirectory(): UniFile? fun getFilesInBaseDirectory(): List