diff --git a/.github/mergify.yml b/.github/mergify.yml deleted file mode 100644 index c9ddf7602..000000000 --- a/.github/mergify.yml +++ /dev/null @@ -1,10 +0,0 @@ -#pull_request_rules: -# - name: Automatically merge translations -# conditions: -# - "author = weblate" -# - "-conflict" -# - "current-day-of-week = Sat" -# - "created-at < 1 day ago" -# actions: -# merge: -# method: squash \ No newline at end of file diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 4cc36b133..c01f918f5 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,18 +1,14 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ], - "schedule": ["every friday"], + "extends": ["config:base"], "labels": ["Dependencies"], "packageRules": [ { - // Compiler plugins are tightly coupled to Kotlin version - "groupName": "Kotlin", - "matchPackagePrefixes": [ - "androidx.compose.compiler", - "org.jetbrains.kotlin", + "groupName": "Compose BOM (Alpha)", + "matchPackageNames": [ + "dev.chrisbanes.compose:compose-bom" ], + "ignoreUnstable": false } ] } diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index d58311c47..98907b52f 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -3,8 +3,8 @@ on: pull_request: paths-ignore: - '**.md' - - 'i18n/src/commonMain/resources/**/strings.xml' - - 'i18n/src/commonMain/resources/**/plurals.xml' + - 'i18n/src/commonMain/moko-resources/**/strings.xml' + - 'i18n/src/commonMain/moko-resources/**/plurals.xml' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} @@ -20,13 +20,13 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@b231772637bb498f11fdbc86052b6e8a8dc9fc92 # v2.1.2 + uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 - name: Dependency Review - uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # v4.2.5 + uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 - name: Set up JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 @@ -35,7 +35,19 @@ jobs: distribution: adopt - name: Set up gradle - uses: gradle/actions/setup-gradle@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0 + uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 - name: Build app and run unit tests - run: ./gradlew detekt assembleStandardRelease testReleaseUnitTest + run: ./gradlew detekt assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest + + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: arm64-v8a-${{ github.sha }} + path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk + + - name: Upload mapping + uses: actions/upload-artifact@v4 + with: + name: mapping-${{ github.sha }} + path: app/build/outputs/mapping/standardRelease diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 878382f17..1f04e4759 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@b231772637bb498f11fdbc86052b6e8a8dc9fc92 # v2.1.2 + uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 - name: Setup Android SDK run: | @@ -33,10 +33,22 @@ jobs: distribution: adopt - name: Set up gradle - uses: gradle/actions/setup-gradle@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0 + uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 - name: Build app and run unit tests - run: ./gradlew detekt assembleStandardRelease testReleaseUnitTest + run: ./gradlew detekt assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest + + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: arm64-v8a-${{ github.sha }} + path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk + + - name: Upload mapping + uses: actions/upload-artifact@v4 + with: + name: mapping-${{ github.sha }} + path: app/build/outputs/mapping/standardRelease # Sign APK and create release for tags @@ -83,7 +95,7 @@ jobs: - name: Create Release if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' - uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v2.0.4 + uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v2.0.6 with: tag_name: ${{ env.VERSION_TAG }} name: Mihon ${{ env.VERSION_TAG }} diff --git a/.github/workflows/issue_moderator.yml b/.github/workflows/issue_moderator.yml deleted file mode 100644 index 70292069d..000000000 --- a/.github/workflows/issue_moderator.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Issue moderator - -on: - issues: - types: [opened, edited, reopened] - issue_comment: - types: [created] - -jobs: - moderate: - runs-on: ubuntu-latest - steps: - - name: Moderate issues - uses: keiyoushi/issue-moderator-action@a017be83547db6e107431ce7575f53c1dfa3296a - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - duplicate-label: Duplicate - - auto-close-rules: | - [ - { - "type": "both", - "regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$", - "ignoreCase": true, - "message": "Mihon does not support anime, and has no plans to support anime. In addition Mihon is not affiliated with Aniyomi https://github.com/jmir1/aniyomi" - }, - { - "type": "both", - "regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(? { - kotlinOptions.freeCompilerArgs += listOf( + compilerOptions.freeCompilerArgs.addAll( "-Xcontext-receivers", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.material.ExperimentalMaterialApi", @@ -286,7 +286,6 @@ tasks { "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", "-opt-in=coil3.annotation.ExperimentalCoilApi", - "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi", diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index c6471bea7..b2971f1b7 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -44,6 +44,10 @@ -dontnote rx.internal.util.PlatformDependent ##---------------End: proguard configuration for RxJava 1.x ---------- +##---------------Begin: proguard configuration for okhttp ---------- +-keepclasseswithmembers class okhttp3.MultipartBody$Builder { *; } +##---------------End: proguard configuration for okhttp ---------- + ##---------------Begin: proguard configuration for kotlinx.serialization ---------- -keepattributes *Annotation*, InnerClasses -dontnote kotlinx.serialization.** # core serialization annotations @@ -73,9 +77,6 @@ # XmlUtil -keep public enum nl.adaptivity.xmlutil.EventType { *; } -# Apache Commons Compress --keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { (); } - # Firebase -keep class com.google.firebase.installations.** { *; } -keep interface com.google.firebase.installations.** { *; } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 48c183a92..4c769f703 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -179,7 +179,7 @@ class DomainModule : InjektModule { addFactory { ToggleLanguage(get()) } addFactory { ToggleSource(get()) } addFactory { ToggleSourcePin(get()) } - addFactory { TrustExtension(get()) } + addFactory { TrustExtension(get(), get()) } addSingletonFactory { ExtensionRepoRepositoryImpl(get()) } addFactory { ExtensionRepoService(get(), get()) } 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 dadbd35f6..489403407 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 @@ -20,7 +20,7 @@ class GetExtensionsByType( extensionManager.installedExtensionsFlow, extensionManager.untrustedExtensionsFlow, extensionManager.availableExtensionsFlow, - ) { _activeLanguages, _installed, _untrusted, _available -> + ) { enabledLanguages, _installed, _untrusted, _available -> val (updates, installed) = _installed .filter { (showNsfwSources || !it.isNsfw) } .sortedWith( @@ -40,9 +40,9 @@ class GetExtensionsByType( } .flatMap { ext -> if (ext.sources.isEmpty()) { - return@flatMap if (ext.lang in _activeLanguages) listOf(ext) else emptyList() + return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList() } - ext.sources.filter { it.lang in _activeLanguages } + ext.sources.filter { it.lang in enabledLanguages } .map { ext.copy( name = it.name, 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 c3daec5f8..b4259945d 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 @@ -3,15 +3,18 @@ package eu.kanade.domain.extension.interactor import android.content.pm.PackageInfo import androidx.core.content.pm.PackageInfoCompat import eu.kanade.domain.source.service.SourcePreferences +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository import tachiyomi.core.common.preference.getAndSet class TrustExtension( + private val extensionRepoRepository: ExtensionRepoRepository, private val preferences: SourcePreferences, ) { - fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean { - val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash" - return key in preferences.trustedExtensions().get() + 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) { @@ -19,9 +22,7 @@ class TrustExtension( // Remove previously trusted versions val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet() - removed.also { - it += "$pkgName:$versionCode:$signatureHash" - } + removed.also { it += "$pkgName:$versionCode:$signatureHash" } } } 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 d4d3989c0..72989891e 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 @@ -48,4 +48,9 @@ class SourcePreferences( Preference.appStateKey("trusted_extensions"), emptySet(), ) + + fun globalSearchFilterState() = preferenceStore.getBoolean( + Preference.appStateKey("has_filters_toggle_state"), + false, + ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 807b68b20..277dc5e65 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -5,7 +5,6 @@ import android.net.Uri import android.provider.Settings import android.util.DisplayMetrics import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -191,7 +190,7 @@ private fun ExtensionDetails( key = { it.source.id }, ) { source -> SourceSwitchPreference( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), source = source, onClickSourcePreferences = onClickSourcePreferences, onClickSource = onClickSource, @@ -354,10 +353,8 @@ private fun InfoText( primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge, onClick: (() -> Unit)? = null, ) { - val interactionSource = remember { MutableInteractionSource() } - val clickableModifier = if (onClick != null) { - Modifier.clickable(interactionSource, indication = null) { onClick() } + Modifier.clickable(interactionSource = null, indication = null, onClick = onClick) } else { Modifier } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt index c65f0d0b1..7f7e18baa 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt @@ -58,7 +58,7 @@ private fun ExtensionFilterContent( ) { items(state.languages) { language -> SwitchPreferenceWidget( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), title = LocaleHelper.getSourceDisplayName(language, context), checked = language in state.enabledLanguages, onCheckedChanged = { onClickLang(language) }, 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 d08a2d93b..5849e8c9e 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -90,7 +90,7 @@ fun ExtensionScreen( PullRefresh( refreshing = state.isRefreshing, onRefresh = onRefresh, - enabled = { !state.isLoading }, + enabled = !state.isLoading, ) { when { state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) @@ -187,14 +187,14 @@ private fun ExtensionContent( } ExtensionHeader( textRes = header.textRes, - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), action = action, ) } is ExtensionUiModel.Header.Text -> { ExtensionHeader( text = header.text, - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), ) } } @@ -212,7 +212,7 @@ private fun ExtensionContent( }, ) { item -> ExtensionItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), item = item, onClickItem = { when (it) { 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 7f79dd3ed..da4db5e98 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.ui.Modifier import eu.kanade.presentation.browse.components.GlobalSearchCardRow import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem @@ -79,6 +80,7 @@ internal fun GlobalSearchContent( } ?: source.name, subtitle = LocaleHelper.getLocalizedDisplayName(source.lang), onClick = { onClickSource(source) }, + modifier = Modifier.animateItem(), ) { when (result) { SearchItemResult.Loading -> { 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 404d11476..3f8a67c4c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -133,7 +133,7 @@ private fun MigrateSourceList( key = { (source, _) -> "migrate-${source.id}" }, ) { (source, count) -> MigrateSourceItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), source = source, count = count, onClickItem = { onClickItem(source) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt index e334a451e..9f74c3799 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt @@ -68,7 +68,7 @@ private fun SourcesFilterContent( contentType = "source-filter-header", ) { SourcesFilterHeader( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), language = language, enabled = enabled, onClickItem = onClickLanguage, @@ -81,7 +81,7 @@ private fun SourcesFilterContent( contentType = { "source-filter-item" }, ) { source -> SourcesFilterItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), source = source, enabled = "${source.id}" !in state.disabledSources, onClickItem = onClickSource, 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 d0411b7a8..56644b3d8 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -74,12 +74,12 @@ fun SourcesScreen( when (model) { is SourceUiModel.Header -> { SourceHeader( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), language = model.language, ) } is SourceUiModel.Item -> SourceItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), source = model.source, onClickItem = onClickItem, onLongClickItem = onLongClickItem, 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 29ef3b97b..5de9a2142 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 @@ -32,9 +32,10 @@ fun GlobalSearchResultItem( title: String, subtitle: String, onClick: () -> Unit, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { - Column { + Column(modifier = modifier) { Row( modifier = Modifier .padding( 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 34a6a1217..df553a203 100644 --- a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt @@ -107,7 +107,7 @@ private fun CategoryContent( key = { _, category -> "category-${category.id}" }, ) { index, category -> CategoryListItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), category = category, canMoveUp = index != 0, canMoveDown = index != categories.lastIndex, diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt index 58d7f163a..a151e9b2f 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt @@ -10,8 +10,7 @@ import androidx.compose.ui.Modifier import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrollingUp +import tachiyomi.presentation.core.util.shouldExpandFAB @Composable fun CategoryFloatingActionButton( @@ -23,7 +22,7 @@ fun CategoryFloatingActionButton( text = { Text(text = stringResource(MR.strings.action_add)) }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) }, onClick = onCreate, - expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(), + expanded = lazyListState.shouldExpandFAB(), modifier = modifier, ) } 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 2712d9a27..15d05a6ec 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt @@ -7,8 +7,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import cafe.adriel.voyager.core.annotation.InternalVoyagerApi @@ -23,7 +21,6 @@ import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl @Composable fun NavigatorAdaptiveSheet( screen: Screen, - tonalElevation: Dp = 1.dp, enableSwipeDismiss: (Navigator) -> Boolean = { true }, onDismissRequest: () -> Unit, ) { @@ -31,7 +28,6 @@ fun NavigatorAdaptiveSheet( screen = screen, content = { sheetNavigator -> AdaptiveSheet( - tonalElevation = tonalElevation, enableSwipeDismiss = enableSwipeDismiss(sheetNavigator), onDismissRequest = onDismissRequest, ) { @@ -73,7 +69,6 @@ fun NavigatorAdaptiveSheet( fun AdaptiveSheet( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, - tonalElevation: Dp = 1.dp, enableSwipeDismiss: Boolean = true, content: @Composable () -> Unit, ) { @@ -86,7 +81,6 @@ fun AdaptiveSheet( AdaptiveSheetImpl( modifier = modifier, isTabletUi = isTabletUi, - tonalElevation = tonalElevation, enableSwipeDismiss = enableSwipeDismiss, onDismissRequest = onDismissRequest, ) { 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 e7338c13e..2315e0050 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.outlined.Close @@ -21,6 +20,7 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.TopAppBar @@ -179,7 +179,7 @@ fun AppBarTitle( maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.basicMarquee( - delayMillis = 2_000, + repeatDelayMillis = 2_000, ), ) } @@ -312,7 +312,7 @@ fun SearchToolbar( visualTransformation = visualTransformation, interactionSource = interactionSource, decorationBox = { innerTextField -> - TextFieldDefaults.TextFieldDecorationBox( + TextFieldDefaults.DecorationBox( value = searchQuery, innerTextField = innerTextField, enabled = true, @@ -331,6 +331,7 @@ fun SearchToolbar( ), ) }, + container = {}, ) }, ) 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 4ef2e9771..b651060f7 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -58,6 +58,7 @@ fun TabbedDialog( PrimaryTabRow( modifier = Modifier.weight(1f), selectedTabIndex = pagerState.currentPage, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, divider = {}, ) { tabTitles.fastForEachIndexed { index, tab -> 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 fd8b27030..5be7a2ca5 100644 --- a/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt @@ -37,7 +37,7 @@ fun CrashScreen( acceptText = stringResource(MR.strings.pref_dump_crash_logs), onAcceptClick = { scope.launch { - CrashLogUtil(context).dumpLogs() + CrashLogUtil(context).dumpLogs(exception) } }, rejectText = stringResource(MR.strings.crash_screen_restart_application), 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 c15604829..dba526b54 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -113,14 +113,14 @@ private fun HistoryScreenContent( when (item) { is HistoryUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), text = relativeDateText(item.date), ) } is HistoryUiModel.Item -> { val value = item.item HistoryItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), history = value, onClickCover = { onClickCover(value) }, onClickResume = { onClickResume(value) }, 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 ad0733606..bf5daae99 100644 --- a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.FilterChip import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -125,7 +126,7 @@ private fun ColumnScope.FilterPage( ) } - val trackers = remember { screenModel.trackers } + val trackers by screenModel.trackersFlow.collectAsState() when (trackers.size) { 0 -> { // No trackers @@ -158,15 +159,15 @@ private fun ColumnScope.SortPage( category: Category?, screenModel: LibrarySettingsScreenModel, ) { + val trackers by screenModel.trackersFlow.collectAsState() val sortingMode = category.sort.type val sortDescending = !category.sort.isAscending - val trackerSortOption = - if (screenModel.trackers.isEmpty()) { - emptyList() - } else { - listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean) - } + val trackerSortOption = if (trackers.isEmpty()) { + emptyList() + } else { + listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean) + } listOf( MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical, 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 92957384a..b4a4c2cc0 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 @@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import eu.kanade.presentation.manga.components.MangaCover @@ -42,15 +43,22 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.BadgeGroup import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.selectedBackground +import tachiyomi.domain.manga.model.MangaCover as MangaCoverModel object CommonMangaItemDefaults { val GridHorizontalSpacer = 4.dp val GridVerticalSpacer = 4.dp + @Suppress("ConstPropertyName") const val BrowseFavoriteCoverAlpha = 0.34f } -private val ContinueReadingButtonSize = 28.dp +private val ContinueReadingButtonSizeSmall = 28.dp +private val ContinueReadingButtonSizeLarge = 32.dp + +private val ContinueReadingButtonIconSizeSmall = 16.dp +private val ContinueReadingButtonIconSizeLarge = 20.dp + private val ContinueReadingButtonGridPadding = 6.dp private val ContinueReadingButtonListSpacing = 8.dp @@ -62,7 +70,7 @@ private const val GridSelectedCoverAlpha = 0.76f */ @Composable fun MangaCompactGridItem( - coverData: tachiyomi.domain.manga.model.MangaCover, + coverData: MangaCoverModel, onClick: () -> Unit, onLongClick: () -> Unit, isSelected: Boolean = false, @@ -96,10 +104,12 @@ fun MangaCompactGridItem( ) } else if (onClickContinueReading != null) { ContinueReadingButton( + size = ContinueReadingButtonSizeLarge, + iconSize = ContinueReadingButtonIconSizeLarge, + onClick = onClickContinueReading, modifier = Modifier .padding(ContinueReadingButtonGridPadding) .align(Alignment.BottomEnd), - onClickContinueReading = onClickContinueReading, ) } }, @@ -148,11 +158,13 @@ private fun BoxScope.CoverTextOverlay( ) if (onClickContinueReading != null) { ContinueReadingButton( + size = ContinueReadingButtonSizeSmall, + iconSize = ContinueReadingButtonIconSizeSmall, + onClick = onClickContinueReading, modifier = Modifier.padding( end = ContinueReadingButtonGridPadding, bottom = ContinueReadingButtonGridPadding, ), - onClickContinueReading = onClickContinueReading, ) } } @@ -163,7 +175,7 @@ private fun BoxScope.CoverTextOverlay( */ @Composable fun MangaComfortableGridItem( - coverData: tachiyomi.domain.manga.model.MangaCover, + coverData: MangaCoverModel, title: String, onClick: () -> Unit, onLongClick: () -> Unit, @@ -194,10 +206,12 @@ fun MangaComfortableGridItem( content = { if (onClickContinueReading != null) { ContinueReadingButton( + size = ContinueReadingButtonSizeLarge, + iconSize = ContinueReadingButtonIconSizeLarge, + onClick = onClickContinueReading, modifier = Modifier .padding(ContinueReadingButtonGridPadding) .align(Alignment.BottomEnd), - onClickContinueReading = onClickContinueReading, ) } }, @@ -309,14 +323,14 @@ private fun GridItemSelectable( private fun Modifier.selectedOutline( isSelected: Boolean, color: Color, -) = this then drawBehind { if (isSelected) drawRect(color = color) } +) = drawBehind { if (isSelected) drawRect(color = color) } /** * Layout of list item. */ @Composable fun MangaListItem( - coverData: tachiyomi.domain.manga.model.MangaCover, + coverData: MangaCoverModel, title: String, onClick: () -> Unit, onLongClick: () -> Unit, @@ -354,8 +368,10 @@ fun MangaListItem( BadgeGroup(content = badge) if (onClickContinueReading != null) { ContinueReadingButton( - modifier = Modifier.padding(start = ContinueReadingButtonListSpacing), - onClickContinueReading = onClickContinueReading, + size = ContinueReadingButtonSizeSmall, + iconSize = ContinueReadingButtonIconSizeSmall, + onClick = onClickContinueReading, + modifier = Modifier.padding(start = ContinueReadingButtonListSpacing) ) } } @@ -363,23 +379,25 @@ fun MangaListItem( @Composable private fun ContinueReadingButton( + size: Dp, + iconSize: Dp, + onClick: () -> Unit, modifier: Modifier = Modifier, - onClickContinueReading: () -> Unit, ) { Box(modifier = modifier) { FilledIconButton( - onClick = onClickContinueReading, - modifier = Modifier.size(ContinueReadingButtonSize), + 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) ) { Icon( imageVector = Icons.Filled.PlayArrow, contentDescription = stringResource(MR.strings.action_resume), - modifier = Modifier.size(16.dp), + modifier = Modifier.size(iconSize), ) } } 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 f0a63f597..61da10345 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 @@ -93,7 +93,7 @@ fun LibraryContent( isRefreshing = false } }, - enabled = { notSelectionMode }, + enabled = notSelectionMode, ) { LibraryPager( state = pagerState, 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 9a6e3f6fd..91546f57f 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -75,8 +75,7 @@ import tachiyomi.presentation.core.components.material.ExtendedFloatingActionBut import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrollingUp +import tachiyomi.presentation.core.util.shouldExpandFAB import tachiyomi.source.local.isLocal import java.time.Instant @@ -346,7 +345,7 @@ private fun MangaScreenSmallImpl( }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, - expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), + expanded = chapterListState.shouldExpandFAB(), ) } }, @@ -356,7 +355,7 @@ private fun MangaScreenSmallImpl( PullRefresh( refreshing = state.isRefreshingData, onRefresh = onRefresh, - enabled = { !isAnySelected }, + enabled = !isAnySelected, indicatorPadding = PaddingValues(top = topPadding), ) { val layoutDirection = LocalLayoutDirection.current @@ -594,7 +593,7 @@ fun MangaScreenLargeImpl( }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, - expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), + expanded = chapterListState.shouldExpandFAB(), ) } }, @@ -602,7 +601,7 @@ fun MangaScreenLargeImpl( PullRefresh( refreshing = state.isRefreshingData, onRefresh = onRefresh, - enabled = { !isAnySelected }, + enabled = !isAnySelected, indicatorPadding = PaddingValues( start = insetPadding.calculateStartPadding(layoutDirection), top = with(density) { topBarHeight.toDp() }, 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 b1a1474ec..518f09d4b 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 @@ -9,13 +9,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.outlined.ArrowDownward import androidx.compose.material.icons.outlined.ErrorOutline -import androidx.compose.material.ripple import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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 d67a406e7..93b8c1843 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 @@ -8,7 +8,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -30,11 +29,11 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.RemoveDone -import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -82,7 +81,7 @@ fun MangaBottomActionMenu( Surface( modifier = modifier, shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize), - tonalElevation = 3.dp, + color = MaterialTheme.colorScheme.surfaceContainerHigh, ) { val haptic = LocalHapticFeedback.current val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) } @@ -191,7 +190,7 @@ private fun RowScope.Button( .size(48.dp) .weight(animatedWeight) .combinedClickable( - interactionSource = remember { MutableInteractionSource() }, + interactionSource = null, indication = ripple(bounded = false), onLongClick = onLongClick, onClick = onClick, @@ -238,7 +237,7 @@ fun LibraryBottomActionMenu( Surface( modifier = modifier, shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize), - tonalElevation = 3.dp, + color = MaterialTheme.colorScheme.surfaceContainerHigh, ) { val haptic = LocalHapticFeedback.current val confirm = remember { mutableStateListOf(false, false, false, false, false) } 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 12720957e..6f323993c 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 @@ -30,7 +30,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector @@ -66,9 +65,6 @@ fun MangaChapterListItem( onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, modifier: Modifier = Modifier, ) { - val textAlpha = if (read) ReadItemAlpha else 1f - val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha - val start = getSwipeAction( action = chapterSwipeStartAction, read = read, @@ -133,15 +129,20 @@ fun MangaChapterListItem( Text( text = title, style = MaterialTheme.typography.bodyMedium, - color = LocalContentColor.current.copy(alpha = textAlpha), maxLines = 1, overflow = TextOverflow.Ellipsis, onTextLayout = { textHeight = it.size.height }, + color = LocalContentColor.current.copy(alpha = if (read) ReadItemAlpha else 1f), ) } - Row(modifier = Modifier.alpha(textSubtitleAlpha)) { - ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { + Row { + val subtitleStyle = MaterialTheme.typography.bodySmall + .merge( + color = LocalContentColor.current + .copy(alpha = if (read) ReadItemAlpha else SecondaryItemAlpha) + ) + ProvideTextStyle(value = subtitleStyle) { if (date != null) { Text( text = date, 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 a70511326..87bad99c2 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 @@ -37,6 +37,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.core.view.updatePadding +import coil3.asDrawable import coil3.imageLoader import coil3.request.CachePolicy import coil3.request.ImageRequest 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 a6ef3f9a5..bee8a8844 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 @@ -102,9 +102,12 @@ fun SetIntervalDialog( ), ), ) - - Spacer(Modifier.height(MaterialTheme.padding.small)) + } else { + Text( + stringResource(MR.strings.manga_interval_expected_update_null), + ) } + Spacer(Modifier.height(MaterialTheme.padding.small)) if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) { Text(stringResource(MR.strings.manga_interval_custom_amount)) 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 ac4946c99..f1b660626 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 @@ -42,7 +42,7 @@ import androidx.compose.material.icons.outlined.Sync import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement +import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle @@ -649,7 +649,7 @@ private fun TagsChip( modifier: Modifier = Modifier, onClick: () -> Unit, ) { - CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { + CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) { SuggestionChip( modifier = modifier, onClick = onClick, 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 d7c46e90d..747ac09e0 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 @@ -31,8 +31,6 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrolledToStart @Composable fun ScanlatorFilterDialog( @@ -96,8 +94,8 @@ fun ScanlatorFilterDialog( } } } - if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } }, properties = DialogProperties( 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 79e45159f..8b3d9c07b 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 @@ -28,11 +28,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.core.content.getSystemService import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import tachiyomi.i18n.MR 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 b22e69323..5a3c9d53b 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 @@ -7,12 +7,12 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.ui.unit.dp -import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget import eu.kanade.presentation.more.settings.widget.InfoWidget import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget @@ -23,8 +23,6 @@ import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget import kotlinx.coroutines.launch import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.util.collectAsState -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false } val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) { 56.dp } @@ -156,16 +154,14 @@ internal fun PreferenceItem( ) } is Preference.PreferenceItem.TrackerPreference -> { - val uName by Injekt.get() - .trackUsername(item.tracker) - .collectAsState() - item.tracker.run { - TrackingPreferenceWidget( - tracker = this, - checked = uName.isNotEmpty(), - onClick = { if (isLoggedIn) item.logout() else item.login() }, - ) + val isLoggedIn by item.tracker.let { tracker -> + tracker.isLoggedInFlow.collectAsState(tracker.isLoggedIn) } + TrackingPreferenceWidget( + tracker = item.tracker, + checked = isLoggedIn, + onClick = { if (isLoggedIn) item.logout() else item.login() }, + ) } is Preference.PreferenceItem.InfoPreference -> { InfoWidget(text = item.title) 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 91de2993f..ecfd2ec75 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 @@ -111,7 +111,17 @@ object SettingsDataScreen : SearchableSettings { val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - context.contentResolver.takePersistableUriPermission(uri, flags) + // 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 + // revoked after the app is closed or the device is restarted. + // This also holds for some Samsung devices. Thus, we simply execute inside of a try-catch block and + // ignore the exception if it is thrown. + try { + context.contentResolver.takePersistableUriPermission(uri, flags) + } catch (e: SecurityException) { + logcat(LogPriority.ERROR, e) + context.toast(MR.strings.file_picker_uri_permission_unsupported) + } UniFile.fromUri(context, uri)?.let { storageDirPref.set(it.uri.toString()) 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 be685a780..7b5aa15f3 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 @@ -14,6 +14,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableMap import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt @@ -61,12 +62,8 @@ object SettingsReaderScreen : SearchableSettings { pref = readerPref.pageTransitions(), title = stringResource(MR.strings.pref_page_transitions), ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPref.flashOnPageChange(), - title = stringResource(MR.strings.pref_flash_page), - subtitle = stringResource(MR.strings.pref_flash_page_summ), - ), getDisplayGroup(readerPreferences = readerPref), + getEInkGroup(readerPreferences = readerPref), getReadingGroup(readerPreferences = readerPref), getPagedGroup(readerPreferences = readerPref), getWebtoonGroup(readerPreferences = readerPref), @@ -122,6 +119,65 @@ object SettingsReaderScreen : SearchableSettings { ) } + @Composable + private fun getEInkGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup { + val flashPageState by readerPreferences.flashOnPageChange().collectAsState() + + val flashMillisPref = readerPreferences.flashDurationMillis() + val flashMillis by flashMillisPref.collectAsState() + + val flashIntervalPref = readerPreferences.flashPageInterval() + val flashInterval by flashIntervalPref.collectAsState() + + val flashColorPref = readerPreferences.flashColor() + + 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, + ), + ), + ) + } + @Composable private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup { return Preference.PreferenceGroup( 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 20a924d11..e6b2a227f 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 @@ -46,7 +46,7 @@ fun ExtensionReposContent( repos.forEach { item { ExtensionRepoListItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), repo = it, onOpenWebsite = { onOpenWebsite(it) }, onDelete = { onClickDelete(it.baseUrl) }, 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 5022f44f0..239f917d0 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 @@ -1,6 +1,7 @@ package eu.kanade.presentation.more.settings.screen.browse.components import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialog import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -14,6 +15,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.KeyboardType import kotlinx.collections.immutable.ImmutableSet import kotlinx.coroutines.delay import mihon.domain.extensionrepo.model.ExtensionRepo @@ -74,6 +76,7 @@ fun ExtensionRepoCreateDialog( Text(text = stringResource(msgRes)) }, isError = name.isNotEmpty() && nameAlreadyExists, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), singleLine = true, ) } 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 db0842d08..5e3f76efe 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 @@ -223,13 +223,12 @@ fun AppThemePreviewItem( contentAlignment = Alignment.BottomCenter, ) { Surface( - tonalElevation = 3.dp, + color = MaterialTheme.colorScheme.surfaceContainer, ) { Row( modifier = Modifier .height(32.dp) .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceVariant) .padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { 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 c8e757491..7daf65168 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 @@ -26,8 +26,6 @@ import androidx.compose.ui.unit.dp import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrolledToStart @Composable fun ListPreferenceWidget( @@ -69,8 +67,8 @@ fun ListPreferenceWidget( } } } - if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } }, confirmButton = { 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 f6e195e4d..be5029ac3 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 @@ -30,8 +30,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrolledToStart private enum class State { CHECKED, INVERSED, UNCHECKED @@ -115,16 +113,8 @@ fun TriStateListDialog( } } - if (!listState.isScrolledToStart()) { - HorizontalDivider( - modifier = Modifier.align(Alignment.TopCenter), - ) - } - if (!listState.isScrolledToEnd()) { - HorizontalDivider( - modifier = Modifier.align(Alignment.BottomCenter), - ) - } + if (listState.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (listState.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } } }, 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 82d3cad0f..ecf26119d 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt @@ -7,19 +7,42 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import kotlinx.coroutines.delay -import kotlin.time.Duration.Companion.seconds +import tachiyomi.presentation.core.util.collectAsState +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import kotlin.time.Duration.Companion.milliseconds @Stable class DisplayRefreshHost { internal var currentDisplayRefresh by mutableStateOf(false) + private val readerPreferences = Injekt.get() + + internal val flashMillis = readerPreferences.flashDurationMillis() + internal val flashMode = readerPreferences.flashColor() + + internal val flashIntervalPref = readerPreferences.flashPageInterval() + + // Internal State for Flash + private var flashInterval = flashIntervalPref.get() + private var timesCalled = 0 fun flash() { - currentDisplayRefresh = true + if (timesCalled % flashInterval == 0) { + currentDisplayRefresh = true + } + timesCalled += 1 + } + + fun setInterval(interval: Int) { + flashInterval = interval + timesCalled = 0 } } @@ -29,18 +52,39 @@ fun DisplayRefreshHost( modifier: Modifier = Modifier, ) { val currentDisplayRefresh = hostState.currentDisplayRefresh + val refreshDuration by hostState.flashMillis.collectAsState() + val flashMode by hostState.flashMode.collectAsState() + val flashInterval by hostState.flashIntervalPref.collectAsState() + + var currentColor by remember { mutableStateOf(null) } + LaunchedEffect(currentDisplayRefresh) { - if (currentDisplayRefresh) { - delay(1.5.seconds) - hostState.currentDisplayRefresh = false + if (!currentDisplayRefresh) { + currentColor = null + return@LaunchedEffect } + + val refreshDurationHalf = refreshDuration.milliseconds / 2 + currentColor = if (flashMode == ReaderPreferences.FlashColor.BLACK) { + Color.Black + } else { + Color.White + } + delay(refreshDurationHalf) + if (flashMode == ReaderPreferences.FlashColor.WHITE_BLACK) { + currentColor = Color.Black + } + delay(refreshDurationHalf) + hostState.currentDisplayRefresh = false + } + + LaunchedEffect(flashInterval) { + hostState.setInterval(flashInterval) } Canvas( modifier = modifier.fillMaxSize(), ) { - if (currentDisplayRefresh) { - drawRect(Color.Black) - } + currentColor?.let { drawRect(it) } } } diff --git a/app/src/main/java/eu/kanade/presentation/reader/ReaderPageActionsDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/ReaderPageActionsDialog.kt index 70cc58f20..b83e8d6ad 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ReaderPageActionsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ReaderPageActionsDialog.kt @@ -33,9 +33,7 @@ fun ReaderPageActionsDialog( ) { var showSetCoverDialog by remember { mutableStateOf(false) } - AdaptiveSheet( - onDismissRequest = onDismissRequest, - ) { + AdaptiveSheet(onDismissRequest = onDismissRequest) { Row( modifier = Modifier.padding(vertical = 16.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), 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 2f2832feb..0bb2da6cc 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 @@ -5,10 +5,13 @@ import androidx.compose.material3.FilterChip import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.SettingsChipRow +import tachiyomi.presentation.core.components.SliderItem +import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState @@ -19,9 +22,27 @@ private val themes = listOf( 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) { val readerTheme by screenModel.preferences.readerTheme().collectAsState() + + val flashPageState by screenModel.preferences.flashOnPageChange().collectAsState() + + val flashMillisPref = screenModel.preferences.flashDurationMillis() + val flashMillis by flashMillisPref.collectAsState() + + val flashIntervalPref = screenModel.preferences.flashPageInterval() + val flashInterval by flashIntervalPref.collectAsState() + + val flashColorPref = screenModel.preferences.flashColor() + val flashColor by flashColorPref.collectAsState() + SettingsChipRow(MR.strings.pref_reader_theme) { themes.map { (labelRes, value) -> FilterChip( @@ -73,4 +94,33 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) { label = stringResource(MR.strings.pref_flash_page), pref = screenModel.preferences.flashOnPageChange(), ) + if (flashPageState) { + SliderItem( + value = flashMillis / ReaderPreferences.MILLI_CONVERSION, + label = stringResource(MR.strings.pref_flash_duration), + valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), + onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) }, + min = 1, + max = 15, + ) + SliderItem( + value = flashInterval, + label = stringResource(MR.strings.pref_flash_page_interval), + valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), + onChange = { + flashIntervalPref.set(it) + }, + min = 1, + max = 10, + ) + SettingsChipRow(MR.strings.pref_flash_with) { + flashColors.map { (labelRes, value) -> + FilterChip( + selected = flashColor == value, + onClick = { flashColorPref.set(value) }, + label = { Text(stringResource(labelRes)) }, + ) + } + } + } } 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 97455fd9e..22dd9a0a7 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 @@ -8,6 +8,12 @@ internal abstract class BaseColorScheme { abstract val darkScheme: ColorScheme abstract val lightScheme: ColorScheme + // Cannot be pure black as there's content scrolling behind it + // https://m3.material.io/components/navigation-bar/guidelines#90615a71-607e-485e-9e09-778bfc080563 + private val surfaceContainer = Color(0xFF0C0C0C) + private val surfaceContainerHigh = Color(0xFF131313) + private val surfaceContainerHighest = Color(0xFF1B1B1B) + fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme { if (!isDark) return lightScheme @@ -18,6 +24,12 @@ internal abstract class BaseColorScheme { onBackground = Color.White, surface = Color.Black, onSurface = Color.White, + surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget) + surfaceContainerLowest = surfaceContainer, + surfaceContainerLow = surfaceContainer, + surfaceContainer = surfaceContainer, // Navigation bar background + surfaceContainerHigh = surfaceContainerHigh, + surfaceContainerHighest = surfaceContainerHighest, ) } } 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 354faed5f..566cce91b 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 @@ -19,53 +19,77 @@ internal object GreenAppleColorScheme : BaseColorScheme() { override val darkScheme = darkColorScheme( primary = Color(0xFF7ADB8F), - onPrimary = Color(0xFF003915), - primaryContainer = Color(0xFF005322), - onPrimaryContainer = Color(0xFF96F8A9), - inversePrimary = Color(0xFF006D2F), - secondary = Color(0xFF7ADB8F), - onSecondary = Color(0xFF003915), - secondaryContainer = Color(0xFF005322), - onSecondaryContainer = Color(0xFF96F8A9), - tertiary = Color(0xFFFFB3AA), - onTertiary = Color(0xFF680006), - tertiaryContainer = Color(0xFF93000D), - onTertiaryContainer = Color(0xFFFFDAD5), - background = Color(0xFF1A1C19), - onBackground = Color(0xFFE1E3DD), - surface = Color(0xFF1A1C19), - onSurface = Color(0xFFE1E3DD), - surfaceVariant = Color(0xFF414941), - onSurfaceVariant = Color(0xFFC1C8BE), - surfaceTint = Color(0xFF7ADB8F), - inverseSurface = Color(0xFFE1E3DD), - inverseOnSurface = Color(0xFF1A1C19), - outline = Color(0xFF8B9389), + 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(0xFF006D2F), + primary = Color(0xFF005927), onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFF96F8A9), - onPrimaryContainer = Color(0xFF002109), + 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), - secondary = Color(0xFF006D2F), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFF96F8A9), - onSecondaryContainer = Color(0xFF002109), - tertiary = Color(0xFFB91D22), - onTertiary = Color(0xFFFFFFFF), - tertiaryContainer = Color(0xFFFFDAD5), - onTertiaryContainer = Color(0xFF410003), - background = Color(0xFFFBFDF7), - onBackground = Color(0xFF1A1C19), - surface = Color(0xFFFBFDF7), - onSurface = Color(0xFF1A1C19), - surfaceVariant = Color(0xFFDDE5DA), - onSurfaceVariant = Color(0xFF414941), - surfaceTint = Color(0xFF006D2F), - inverseSurface = Color(0xFF2F312E), - inverseOnSurface = Color(0xFFF0F2EC), - outline = Color(0xFF717970), + 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 70a9bd196..e1bb6b3ee 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 @@ -18,53 +18,77 @@ internal object LavenderColorScheme : BaseColorScheme() { override val darkScheme = darkColorScheme( primary = Color(0xFFA177FF), - onPrimary = Color(0xFF111129), + onPrimary = Color(0xFF3D0090), primaryContainer = Color(0xFFA177FF), - onPrimaryContainer = Color(0xFF111129), - inversePrimary = Color(0xFF006D2F), - secondary = Color(0xFFA177FF), - onSecondary = Color(0xFF111129), - secondaryContainer = Color(0xFFA177FF), - onSecondaryContainer = Color(0xFF111129), - tertiary = Color(0xFF5E25E1), - onTertiary = Color(0xFFE8E8E8), - tertiaryContainer = Color(0xFF111129), - onTertiaryContainer = Color(0xFFDEE8FF), + 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(0xFFDEE8FF), + onBackground = Color(0xFFE7E0EC), surface = Color(0xFF111129), - onSurface = Color(0xFFDEE8FF), - surfaceVariant = Color(0x2CB6B6B6), - onSurfaceVariant = Color(0xFFE8E8E8), - surfaceTint = Color(0xFFA177FF), - inverseSurface = Color(0xFF221247), - inverseOnSurface = Color(0xFFDEE8FF), - outline = Color(0xA8905FFF), + 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(0xFF7B46AF), - onPrimary = Color(0xFFEDE2FF), + primary = Color(0xFF6D41C8), + onPrimary = Color(0xFFFFFFFF), primaryContainer = Color(0xFF7B46AF), - onPrimaryContainer = Color(0xFFEDE2FF), - inversePrimary = Color(0xFFD6BAFF), - secondary = Color(0xFF7B46AF), - onSecondary = Color(0xFFEDE2FF), - secondaryContainer = Color(0xFF7B46AF), - onSecondaryContainer = Color(0xFFEDE2FF), - tertiary = Color(0xFFEDE2FF), - onTertiary = Color(0xFF7B46AF), - tertiaryContainer = Color(0xFFEDE2FF), - onTertiaryContainer = 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(0xFF1B1B22), + onBackground = Color(0xFF1D1A22), surface = Color(0xFFEDE2FF), - onSurface = Color(0xFF1B1B22), - surfaceVariant = Color(0xFFB9B0CC), - onSurfaceVariant = Color(0xD849454E), - surfaceTint = Color(0xFF7B46AF), - inverseSurface = Color(0xFF313033), - inverseOnSurface = Color(0xFFF3EFF4), - outline = Color(0xFF7B46AF), + 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 7feaae333..5ae86aa34 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 @@ -23,24 +23,29 @@ internal object MidnightDuskColorScheme : BaseColorScheme() { primaryContainer = Color(0xFFBD1C5C), onPrimaryContainer = Color(0xFFFFFFFF), inversePrimary = Color(0xFFF02475), - secondary = Color(0xFFF02475), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFF02475), - onSecondaryContainer = Color(0xFFFFFFFF), - tertiary = Color(0xFF55971C), - onTertiary = Color(0xFFFFFFFF), + 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(0xFF524346), + 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( @@ -49,23 +54,28 @@ internal object MidnightDuskColorScheme : BaseColorScheme() { primaryContainer = Color(0xFFFFD9E1), onPrimaryContainer = Color(0xFF3F0017), inversePrimary = Color(0xFFFFB1C4), - secondary = Color(0xFFBB0054), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFFFD9E1), - onSecondaryContainer = Color(0xFF3F0017), - tertiary = Color(0xFF006638), - onTertiary = Color(0xFFFFFFFF), + 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(0xFFF3DDE0), + 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/NordColorScheme.kt b/app/src/main/java/eu/kanade/presentation/theme/colorscheme/NordColorScheme.kt index d493e2d62..84fe7b49f 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 @@ -17,19 +17,19 @@ internal object NordColorScheme : BaseColorScheme() { primaryContainer = Color(0xFF88C0D0), onPrimaryContainer = Color(0xFF2E3440), inversePrimary = Color(0xFF397E91), - secondary = Color(0xFF81A1C1), - onSecondary = Color(0xFF2E3440), - secondaryContainer = Color(0xFF81A1C1), - onSecondaryContainer = Color(0xFF2E3440), - tertiary = Color(0xFF5E81AC), - onTertiary = Color(0xFF000000), + 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(0xFF3B4252), + surface = Color(0xFF2E3440), onSurface = Color(0xFFECEFF4), - surfaceVariant = Color(0xFF2E3440), + surfaceVariant = Color(0xFF414C5C), // Navigation bar background (ThemePrefWidget) onSurfaceVariant = Color(0xFFECEFF4), surfaceTint = Color(0xFF88C0D0), inverseSurface = Color(0xFFD8DEE9), @@ -39,6 +39,11 @@ internal object NordColorScheme : BaseColorScheme() { 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( @@ -47,19 +52,19 @@ internal object NordColorScheme : BaseColorScheme() { primaryContainer = Color(0xFF5E81AC), onPrimaryContainer = Color(0xFF000000), inversePrimary = Color(0xFF8CA8CD), - secondary = Color(0xFF81A1C1), - onSecondary = Color(0xFF2E3440), - secondaryContainer = Color(0xFF81A1C1), - onSecondaryContainer = Color(0xFF2E3440), - tertiary = Color(0xFF88C0D0), - onTertiary = Color(0xFF2E3440), + 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(0xFFffffff), + surfaceVariant = Color(0xFFDAE0EA), // Navigation bar background (ThemePrefWidget) onSurfaceVariant = Color(0xFF2E3440), surfaceTint = Color(0xFF5E81AC), inverseSurface = Color(0xFF3B4252), @@ -68,5 +73,10 @@ internal object NordColorScheme : BaseColorScheme() { 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 98417e336..e1b096e25 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 @@ -18,54 +18,78 @@ import androidx.compose.ui.graphics.Color internal object StrawberryColorScheme : BaseColorScheme() { override val darkScheme = darkColorScheme( - primary = Color(0xFFFFB2B9), - onPrimary = Color(0xFF67001B), - primaryContainer = Color(0xFF91002A), - onPrimaryContainer = Color(0xFFFFDADD), - inversePrimary = Color(0xFFB61E40), - secondary = Color(0xFFFFB2B9), - onSecondary = Color(0xFF67001B), - secondaryContainer = Color(0xFF91002A), - onSecondaryContainer = Color(0xFFFFDADD), - tertiary = Color(0xFFE8C08E), - onTertiary = Color(0xFF432C06), - tertiaryContainer = Color(0xFF5D421B), - onTertiaryContainer = Color(0xFFFFDDB1), + 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(0xFFECDFDF), + onBackground = Color(0xFFF7DCDD), surface = Color(0xFF201A1A), - onSurface = Color(0xFFECDFDF), - surfaceVariant = Color(0xFF534344), - onSurfaceVariant = Color(0xFFD7C1C2), - surfaceTint = Color(0xFFFFB2B9), - inverseSurface = Color(0xFFECDFDF), - inverseOnSurface = Color(0xFF201A1A), - outline = Color(0xFFA08C8D), + 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(0xFFB61E40), + primary = Color(0xFFA10833), onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFFFDADD), - onPrimaryContainer = Color(0xFF40000D), - inversePrimary = Color(0xFFFFB2B9), - secondary = Color(0xFFB61E40), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFFFDADD), - onSecondaryContainer = Color(0xFF40000D), - tertiary = Color(0xFF775930), - onTertiary = Color(0xFFFFFFFF), - tertiaryContainer = Color(0xFFFFDDB1), - onTertiaryContainer = Color(0xFF2A1800), - background = Color(0xFFFCFCFC), - onBackground = Color(0xFF201A1A), - surface = Color(0xFFFCFCFC), - onSurface = Color(0xFF201A1A), - surfaceVariant = Color(0xFFF4DDDD), - onSurfaceVariant = Color(0xFF534344), - surfaceTint = Color(0xFFB61E40), - inverseSurface = Color(0xFF362F2F), - inverseOnSurface = Color(0xFFFBEDED), - outline = Color(0xFF857374), + 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 974d5f22d..5faeb7df0 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 @@ -22,19 +22,19 @@ internal object TachiyomiColorScheme : BaseColorScheme() { primaryContainer = Color(0xFF00429B), onPrimaryContainer = Color(0xFFD9E2FF), inversePrimary = Color(0xFF0058CA), - secondary = Color(0xFFB0C6FF), - onSecondary = Color(0xFF002D6E), - secondaryContainer = Color(0xFF00429B), - onSecondaryContainer = Color(0xFFD9E2FF), - tertiary = Color(0xFF7ADC77), - onTertiary = Color(0xFF003909), + 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(0xFF44464F), + surfaceVariant = Color(0xFF211F26), // Navigation bar background (ThemePrefWidget) onSurfaceVariant = Color(0xFFC5C6D0), surfaceTint = Color(0xFFB0C6FF), inverseSurface = Color(0xFFE3E2E6), @@ -45,6 +45,11 @@ internal object TachiyomiColorScheme : BaseColorScheme() { 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( @@ -53,19 +58,19 @@ internal object TachiyomiColorScheme : BaseColorScheme() { primaryContainer = Color(0xFFD9E2FF), onPrimaryContainer = Color(0xFF001945), inversePrimary = Color(0xFFB0C6FF), - secondary = Color(0xFF0058CA), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFD9E2FF), - onSecondaryContainer = Color(0xFF001945), - tertiary = Color(0xFF006E1B), - onTertiary = Color(0xFFFFFFFF), + 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(0xFFE1E2EC), + surfaceVariant = Color(0xFFF3EDF7), // Navigation bar background (ThemePrefWidget) onSurfaceVariant = Color(0xFF44464F), surfaceTint = Color(0xFF0058CA), inverseSurface = Color(0xFF303034), @@ -76,5 +81,10 @@ internal object TachiyomiColorScheme : BaseColorScheme() { 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 244e769d4..798812963 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 @@ -23,24 +23,29 @@ internal object TakoColorScheme : BaseColorScheme() { primaryContainer = Color(0xFFF3B375), onPrimaryContainer = Color(0xFF38294E), inversePrimary = Color(0xFF84531E), - secondary = Color(0xFFF3B375), - onSecondary = Color(0xFF38294E), - secondaryContainer = Color(0xFFF3B375), - onSecondaryContainer = Color(0xFF38294E), - tertiary = Color(0xFF66577E), - onTertiary = Color(0xFFF3B375), + 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(0xFF49454E), + 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( @@ -49,23 +54,28 @@ internal object TakoColorScheme : BaseColorScheme() { primaryContainer = Color(0xFF66577E), onPrimaryContainer = Color(0xFFF3B375), inversePrimary = Color(0xFFD6BAFF), - secondary = Color(0xFF66577E), - onSecondary = Color(0xFFF3B375), - secondaryContainer = Color(0xFF66577E), - onSecondaryContainer = Color(0xFFF3B375), - tertiary = Color(0xFFF3B375), - onTertiary = Color(0xFF574360), + 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), + 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 e914b49fc..28811fa6a 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 @@ -15,24 +15,29 @@ internal object TealTurqoiseColorScheme : BaseColorScheme() { primaryContainer = Color(0xFF40E0D0), onPrimaryContainer = Color(0xFF000000), inversePrimary = Color(0xFF008080), - secondary = Color(0xFF40E0D0), - onSecondary = Color(0xFF000000), - secondaryContainer = Color(0xFF18544E), - onSecondaryContainer = Color(0xFF40E0D0), - tertiary = Color(0xFFBF1F2F), - onTertiary = Color(0xFFFFFFFF), + 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(0xFF3F4947), + 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( @@ -41,23 +46,28 @@ internal object TealTurqoiseColorScheme : BaseColorScheme() { primaryContainer = Color(0xFF008080), onPrimaryContainer = Color(0xFFFFFFFF), inversePrimary = Color(0xFF40E0D0), - secondary = Color(0xFF008080), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFBFDFDF), - onSecondaryContainer = Color(0xFF008080), - tertiary = Color(0xFFFF7F7F), - onTertiary = Color(0xFF000000), + 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(0xFFDAE5E2), + 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 c56a1fa57..09dc248c0 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 @@ -22,24 +22,29 @@ internal object TidalWaveColorScheme : BaseColorScheme() { primaryContainer = Color(0xFF004d61), onPrimaryContainer = Color(0xFFb8eaff), inversePrimary = Color(0xFFa12b03), - secondary = Color(0xFF5ed4fc), - onSecondary = Color(0xFF003544), - secondaryContainer = Color(0xFF004d61), - onSecondaryContainer = Color(0xFFb8eaff), - tertiary = Color(0xFF92f7bc), - onTertiary = Color(0xFF001c3b), + 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(0xFF40484c), + 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( @@ -48,23 +53,28 @@ internal object TidalWaveColorScheme : BaseColorScheme() { primaryContainer = Color(0xFFB4D4DF), onPrimaryContainer = Color(0xFF001f28), inversePrimary = Color(0xFFff987f), - secondary = Color(0xFF006780), - onSecondary = Color(0xFFffffff), - secondaryContainer = Color(0xFFb8eaff), - onSecondaryContainer = Color(0xFF001f28), - tertiary = Color(0xFF92f7bc), - onTertiary = Color(0xFF001c3b), + 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(0xFFdce4e8), + 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 da9dee424..1e9b12978 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 @@ -17,24 +17,29 @@ internal object YinYangColorScheme : BaseColorScheme() { primaryContainer = Color(0xFFFFFFFF), onPrimaryContainer = Color(0xFF000000), inversePrimary = Color(0xFFCECECE), - secondary = Color(0xFFFFFFFF), - onSecondary = Color(0xFF5A5A5A), - secondaryContainer = Color(0xFF717171), - onSecondaryContainer = Color(0xFFE4E4E4), - tertiary = Color(0xFF000000), - onTertiary = Color(0xFFFFFFFF), + 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(0xFF4E4E4E), + 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( @@ -43,23 +48,28 @@ internal object YinYangColorScheme : BaseColorScheme() { primaryContainer = Color(0xFF000000), onPrimaryContainer = Color(0xFFFFFFFF), inversePrimary = Color(0xFFA6A6A6), - secondary = Color(0xFF000000), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFDDDDDD), - onSecondaryContainer = Color(0xFF0C0C0C), - tertiary = Color(0xFFFFFFFF), - onTertiary = Color(0xFF000000), + 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(0xFFEDEDED), + 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 fdda6b7dc..58007a680 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 @@ -23,24 +23,29 @@ internal object YotsubaColorScheme : BaseColorScheme() { primaryContainer = Color(0xFF862200), onPrimaryContainer = Color(0xFFFFDBCF), inversePrimary = Color(0xFFAE3200), - secondary = Color(0xFFFFB59D), - onSecondary = Color(0xFF5F1600), - secondaryContainer = Color(0xFF862200), - onSecondaryContainer = Color(0xFFFFDBCF), - tertiary = Color(0xFFD7C68D), - onTertiary = Color(0xFF3A2F05), + 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(0xFF53433F), + 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( @@ -49,23 +54,28 @@ internal object YotsubaColorScheme : BaseColorScheme() { primaryContainer = Color(0xFFFFDBCF), onPrimaryContainer = Color(0xFF3B0A00), inversePrimary = Color(0xFFFFB59D), - secondary = Color(0xFFAE3200), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFFFDBCF), - onSecondaryContainer = Color(0xFF3B0A00), - tertiary = Color(0xFF6B5E2F), - onTertiary = Color(0xFFFFFFFF), + 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(0xFFF5DED8), + 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 993532180..7ed6b3cc0 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt @@ -186,7 +186,7 @@ private fun TrackInfoItem( modifier = Modifier .padding(top = 12.dp) .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.surface) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) .padding(8.dp) .clip(RoundedCornerShape(6.dp)), ) { 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 053ba7bbc..c6fda5cc5 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt @@ -43,8 +43,6 @@ import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrolledToStart @Composable fun TrackStatusSelector( @@ -86,8 +84,8 @@ fun TrackStatusSelector( } } } - if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) }, onConfirm = onConfirm, onDismissRequest = onDismissRequest, 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 fa2bd53fe..5693185c0 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -104,7 +104,7 @@ fun UpdateScreen( isRefreshing = false } }, - enabled = { !state.selectionMode }, + enabled = !state.selectionMode, indicatorPadding = contentPadding, ) { FastScrollLazyColumn( 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 a69252ffc..9f5c92479 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -54,7 +54,7 @@ internal fun LazyListScope.updatesLastUpdatedItem( item(key = "updates-lastUpdated") { Box( modifier = Modifier - .animateItemPlacement() + .animateItem() .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), ) { Text( @@ -91,14 +91,14 @@ internal fun LazyListScope.updatesUiItems( when (item) { is UpdatesUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), text = relativeDateText(item.date), ) } is UpdatesUiModel.Item -> { val updatesItem = item.item UpdatesUiItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), update = updatesItem.update, selected = updatesItem.selected, readProgress = updatesItem.update.lastPageRead 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 6bee9203e..80c2f90b9 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Permissions.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Permissions.kt @@ -7,9 +7,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner @Composable fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 12170d8a5..d0b77cec1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -27,6 +27,7 @@ import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.tachiyomi.crash.CrashActivity import eu.kanade.tachiyomi.crash.GlobalExceptionHandler +import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer import eu.kanade.tachiyomi.data.coil.MangaKeyer @@ -152,25 +153,33 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor ) } + @Suppress("MagicNumber") override fun newImageLoader(context: Context): ImageLoader { return ImageLoader.Builder(this).apply { val callFactoryLazy = lazy { Injekt.get().client } components { + // NetworkFetcher.Factory add(OkHttpNetworkFetcherFactory(callFactoryLazy::value)) + // Decoder.Factory add(TachiyomiImageDecoder.Factory()) - add(MangaCoverFetcher.MangaFactory(callFactoryLazy)) + // Fetcher.Factory + add(BufferedSourceFetcher.Factory()) add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy)) - add(MangaKeyer()) + add(MangaCoverFetcher.MangaFactory(callFactoryLazy)) + // Keyer add(MangaCoverKeyer()) + add(MangaKeyer()) } + 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() + fetcherCoroutineContext(Dispatchers.IO.limitedParallelism(8)) + decoderCoroutineContext(Dispatchers.IO.limitedParallelism(3)) + } + .build() } override fun onStart(owner: LifecycleOwner) { 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 e33572caf..34f862548 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 @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import eu.kanade.tachiyomi.data.backup.models.Backup -import eu.kanade.tachiyomi.data.backup.models.BackupSerializer import kotlinx.serialization.protobuf.ProtoBuf import okio.buffer import okio.gzip @@ -33,7 +32,7 @@ class BackupDecoder( source }.use { it.readByteArray() } - parser.decodeFromByteArray(BackupSerializer, backupString) + parser.decodeFromByteArray(Backup.serializer(), backupString) } } } 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 14d05e239..42c97ca9c 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 @@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference -import eu.kanade.tachiyomi.data.backup.models.BackupSerializer import eu.kanade.tachiyomi.data.backup.models.BackupSource import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences import kotlinx.serialization.protobuf.ProtoBuf @@ -84,7 +83,7 @@ class BackupCreator( backupSourcePreferences = backupSourcePreferences(options), ) - val byteArray = parser.encodeToByteArray(BackupSerializer, backup) + val byteArray = parser.encodeToByteArray(Backup.serializer(), backup) if (byteArray.isEmpty()) { throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt index cdc5c4ad2..dcf3d1174 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt @@ -1,12 +1,8 @@ package eu.kanade.tachiyomi.data.backup.models import kotlinx.serialization.Serializable -import kotlinx.serialization.Serializer import kotlinx.serialization.protobuf.ProtoNumber -@Serializer(forClass = Backup::class) -object BackupSerializer - @Serializable data class Backup( @ProtoNumber(1) val backupManga: List, 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 f98af1045..23a2d47fa 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 @@ -17,14 +17,20 @@ class CategoriesRestorer( 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.map { - dbCategoriesByName[it.name] - ?: handler.awaitOneExecutable { - categoriesQueries.insert(it.name, it.order, it.flags) + 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) } - } + } + .let { id -> it.toCategory(id).copy(order = order) } + } libraryPreferences.categorizedDisplaySettings().set( (dbCategories + categories) 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 new file mode 100644 index 000000000..4bee925ed --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/BufferedSourceFetcher.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.data.coil + +import coil3.ImageLoader +import coil3.decode.DataSource +import coil3.decode.ImageSource +import coil3.fetch.FetchResult +import coil3.fetch.Fetcher +import coil3.fetch.SourceFetchResult +import coil3.request.Options +import okio.BufferedSource + +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, + ), + mimeType = null, + dataSource = DataSource.MEMORY, + ) + } + + class Factory : Fetcher.Factory { + + override fun create( + data: BufferedSource, + options: Options, + imageLoader: ImageLoader, + ): Fetcher { + return 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 11ff6a277..65a142099 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 @@ -21,7 +21,6 @@ import okhttp3.CacheControl import okhttp3.Call import okhttp3.Request import okhttp3.Response -import okhttp3.internal.http.HTTP_NOT_MODIFIED import okio.FileSystem import okio.Path.Companion.toOkioPath import okio.Source @@ -348,5 +347,7 @@ class MangaCoverFetcher( private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().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/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt index 2f0c3df49..6403f760b 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 @@ -1,12 +1,16 @@ package eu.kanade.tachiyomi.data.coil +import android.graphics.Bitmap import coil3.ImageLoader -import coil3.asCoilImage +import coil3.asImage import coil3.decode.DecodeResult +import coil3.decode.DecodeUtils import coil3.decode.Decoder import coil3.decode.ImageSource import coil3.fetch.SourceFetchResult import coil3.request.Options +import coil3.request.bitmapConfig +import eu.kanade.tachiyomi.util.system.GLUtil import okio.BufferedSource import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.decoder.ImageDecoder @@ -18,27 +22,55 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti override suspend fun decode(): DecodeResult { val decoder = resources.sourceOrNull()?.use { - ImageDecoder.newInstance(it.inputStream()) + ImageDecoder.newInstance(it.inputStream(), options.cropBorders, displayProfile) } check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" } - val bitmap = decoder.decode() + val srcWidth = decoder.width + val srcHeight = decoder.height + + 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, + ) + + var bitmap = decoder.decode(sampleSize = sampleSize) decoder.recycle() check(bitmap != null) { "Failed to decode image" } + if ( + options.bitmapConfig == Bitmap.Config.HARDWARE && + maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize + ) { + val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false) + if (hwBitmap != null) { + bitmap.recycle() + bitmap = hwBitmap + } + } + return DecodeResult( - image = bitmap.asCoilImage(), - isSampled = false, + image = bitmap.asImage(), + isSampled = sampleSize > 1, ) } class Factory : Decoder.Factory { override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? { - if (!isApplicable(result.source.source())) return null - return TachiyomiImageDecoder(result.source, options) + return if (options.customDecoder || isApplicable(result.source.source())) { + TachiyomiImageDecoder(result.source, options) + } else { + null + } } private fun isApplicable(source: BufferedSource): Boolean { @@ -55,4 +87,8 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti override fun hashCode() = javaClass.hashCode() } + + companion object { + var displayProfile: ByteArray? = null + } } 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 new file mode 100644 index 000000000..7a920bf39 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt @@ -0,0 +1,44 @@ +package eu.kanade.tachiyomi.data.coil + +import coil3.Extras +import coil3.getExtra +import coil3.request.ImageRequest +import coil3.request.Options +import coil3.size.Dimension +import coil3.size.Scale +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 + } +} + +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 +} + +val Options.customDecoder: Boolean + get() = getExtra(customDecoderKey) + +private val customDecoderKey = Extras.Key(default = false) 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 01a342859..5fba899ca 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 @@ -17,6 +17,7 @@ import logcat.LogPriority import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.storage.extension import tachiyomi.core.common.util.lang.launchIO +import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.chapter.model.Chapter @@ -160,7 +161,7 @@ class DownloadManager( 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() } + .filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } } if (files.isEmpty()) { throw Exception(context.stringResource(MR.strings.page_list_empty_error)) 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 001395af1..2ab5e55d9 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 @@ -57,7 +57,7 @@ class DownloadProvider( * @param source the source to query. */ fun findSourceDir(source: Source): UniFile? { - return downloadsDir?.findFile(getSourceDirName(source), true) + return downloadsDir?.findFile(getSourceDirName(source)) } /** @@ -68,7 +68,7 @@ class DownloadProvider( */ fun findMangaDir(mangaTitle: String, source: Source): UniFile? { val sourceDir = findSourceDir(source) - return sourceDir?.findFile(getMangaDirName(mangaTitle), true) + return sourceDir?.findFile(getMangaDirName(mangaTitle)) } /** @@ -82,7 +82,7 @@ class DownloadProvider( fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? { val mangaDir = findMangaDir(mangaTitle, source) return getValidChapterDirNames(chapterName, chapterScanlator).asSequence() - .mapNotNull { mangaDir?.findFile(it, true) } + .mapNotNull { mangaDir?.findFile(it) } .firstOrNull() } @@ -97,7 +97,7 @@ class DownloadProvider( 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, true) } + .mapNotNull { mangaDir.findFile(it) } .firstOrNull() } } @@ -160,21 +160,12 @@ class DownloadProvider( */ fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?): List { val chapterDirName = getChapterDirName(chapterName, chapterScanlator) - return buildList(4) { + return buildList(2) { // Folder of images add(chapterDirName) // Archived chapters add("$chapterDirName.cbz") - - if (chapterScanlator.isNullOrBlank()) { - // Previously null scanlator fields were converted to "" due to a bug - add("_$chapterDirName") - add("_$chapterDirName.cbz") - } else { - // Legacy chapter directory name used in v0.9.2 and before - add(DiskUtil.buildValidFilename(chapterName)) - } } } } 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 7c8238d5a..57c3a9824 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 @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import logcat.LogPriority +import mihon.core.common.archive.ZipWriter import nl.adaptivity.xmlutil.serialization.XML import okhttp3.Response import tachiyomi.core.common.i18n.stringResource @@ -58,12 +59,8 @@ import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.BufferedOutputStream import java.io.File import java.util.Locale -import java.util.zip.CRC32 -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream /** * This class is the one in charge of downloading chapters. @@ -526,14 +523,8 @@ class Downloader( * @param file the file where the image is already downloaded. */ 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 - - return ImageUtil.getExtensionFromMimeType(mime) + return ImageUtil.getExtensionFromMimeType(mime) { file.openInputStream() } } private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile) { @@ -594,25 +585,9 @@ class Downloader( tmpDir: UniFile, ) { val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!! - ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut -> - zipOut.setMethod(ZipEntry.STORED) - - tmpDir.listFiles()?.forEach { img -> - img.openInputStream().use { input -> - val data = input.readBytes() - val size = img.length() - val entry = ZipEntry(img.name).apply { - val crc = CRC32().apply { - update(data) - } - setCrc(crc.value) - - compressedSize = size - setSize(size) - } - zipOut.putNextEntry(entry) - zipOut.write(data) - } + ZipWriter(context, zip).use { writer -> + tmpDir.listFiles()?.forEach { file -> + writer.write(file) } } zip.renameTo("$dirname.cbz") @@ -645,7 +620,7 @@ class Downloader( ) // Remove the old file - dir.findFile(COMIC_INFO_FILE, true)?.delete() + dir.findFile(COMIC_INFO_FILE)?.delete() dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use { val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo) it.write(comicInfoString.toByteArray()) 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 e18bf76c4..f7d6c5dbc 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 @@ -9,6 +9,7 @@ import android.graphics.BitmapFactory import android.net.Uri import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import coil3.asDrawable import coil3.imageLoader import coil3.request.ImageRequest import coil3.request.transformations 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 b7b53d835..0cc0ebc04 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 @@ -7,6 +7,7 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.MediaStore +import android.webkit.MimeTypeMap import androidx.annotation.RequiresApi import androidx.core.content.contentValuesOf import androidx.core.net.toUri @@ -65,21 +66,26 @@ class ImageSaver( filename: String, data: () -> InputStream, ): Uri { - val pictureDir = + val isMimeTypeSupported = MimeTypeMap.getSingleton().hasMimeType(type.mime) + + val pictureDir = if (isMimeTypeSupported) { MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + } else { + MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + } val imageLocation = (image.location as Location.Pictures).relativePath val relativePath = listOf( - Environment.DIRECTORY_PICTURES, + if (isMimeTypeSupported) Environment.DIRECTORY_PICTURES else Environment.DIRECTORY_DOCUMENTS, 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, + MediaStore.MediaColumns.RELATIVE_PATH to relativePath, + MediaStore.MediaColumns.DISPLAY_NAME to if (isMimeTypeSupported) image.name else filename, + MediaStore.MediaColumns.MIME_TYPE to type.mime, + MediaStore.MediaColumns.DATE_MODIFIED to Instant.now().epochSecond, ) val picture = findUriOrDefault(relativePath, filename) { 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 8f88f1051..33caf6555 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 @@ -8,6 +8,8 @@ import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import logcat.LogPriority import okhttp3.OkHttpClient import tachiyomi.core.common.util.lang.withIOContext @@ -53,6 +55,15 @@ abstract class BaseTracker( get() = getUsername().isNotEmpty() && getPassword().isNotEmpty() + override val isLoggedInFlow: Flow by lazy { + combine( + trackPreferences.trackUsername(this).changes(), + trackPreferences.trackPassword(this).changes(), + ) { username, password -> + username.isNotEmpty() && password.isNotEmpty() + } + } + override fun getUsername() = trackPreferences.trackUsername(this).get() override fun getPassword() = trackPreferences.trackPassword(this).get() 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 06644e932..fd3a9f45e 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 @@ -7,6 +7,7 @@ import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.Flow import okhttp3.OkHttpClient import tachiyomi.domain.track.model.Track as DomainTrack @@ -61,6 +62,8 @@ interface Tracker { val isLoggedIn: Boolean + val isLoggedInFlow: Flow + fun getUsername(): String fun getPassword(): String 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 598a0c06c..1071fa7ee 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 @@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.data.track.shikimori.Shikimori import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi +import kotlinx.coroutines.flow.combine class TrackerManager { @@ -32,5 +33,13 @@ class TrackerManager { fun loggedInTrackers() = trackers.filter { it.isLoggedIn } + fun loggedInTrackersFlow() = combine(trackers.map { it.isLoggedInFlow }) { + it.mapIndexedNotNull { index, isLoggedIn -> + if (isLoggedIn) trackers[index] else null + } + } + fun get(id: Long) = trackers.find { it.id == id } + + fun getAll(ids: Set) = trackers.filter { it.id in ids } } 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 15f386d38..968263b5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -13,14 +13,17 @@ import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver import eu.kanade.tachiyomi.extension.util.ExtensionInstaller import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.util.system.toast -import kotlinx.coroutines.async +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import logcat.LogPriority -import tachiyomi.core.common.util.lang.launchNow import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.source.model.StubSource @@ -42,6 +45,8 @@ class ExtensionManager( private val trustExtension: TrustExtension = Injekt.get(), ) { + val scope = CoroutineScope(SupervisorJob()) + private val _isInitialized = MutableStateFlow(false) val isInitialized: StateFlow = _isInitialized.asStateFlow() @@ -57,24 +62,35 @@ class ExtensionManager( private val iconMap = mutableMapOf() - private val _installedExtensionsFlow = MutableStateFlow(emptyList()) - val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow() + private val _installedExtensionsMapFlow = MutableStateFlow(emptyMap()) + val installedExtensionsFlow = _installedExtensionsMapFlow.mapExtensions(scope) + + private val _availableExtensionsMapFlow = MutableStateFlow(emptyMap()) + val availableExtensionsFlow = _availableExtensionsMapFlow.mapExtensions(scope) + + private val _untrustedExtensionsMapFlow = MutableStateFlow(emptyMap()) + val untrustedExtensionsFlow = _untrustedExtensionsMapFlow.mapExtensions(scope) + + init { + initExtensions() + ExtensionInstallReceiver(InstallationListener()).register(context) + } private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet() fun getAppIconForSource(sourceId: Long): Drawable? { - val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName - if (pkgName != null) { - return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { - ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo - .loadIcon(context.packageManager) + val pkgName = _installedExtensionsMapFlow.value.values + .find { ext -> + ext.sources.any { it.id == sourceId } } - } - return null - } + ?.pkgName + ?: return null - private val _availableExtensionsFlow = MutableStateFlow(emptyList()) - val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow() + return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { + ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo + .loadIcon(context.packageManager) + } + } private var availableExtensionsSourcesData: Map = emptyMap() @@ -87,33 +103,25 @@ class ExtensionManager( fun getSourceData(id: Long) = availableExtensionsSourcesData[id] - private val _untrustedExtensionsFlow = MutableStateFlow(emptyList()) - val untrustedExtensionsFlow = _untrustedExtensionsFlow.asStateFlow() - - init { - initExtensions() - ExtensionInstallReceiver(InstallationListener()).register(context) - } - /** * Loads and registers the installed extensions. */ private fun initExtensions() { val extensions = ExtensionLoader.loadExtensions(context) - _installedExtensionsFlow.value = extensions + _installedExtensionsMapFlow.value = extensions .filterIsInstance() - .map { it.extension } + .associate { it.extension.pkgName to it.extension } - _untrustedExtensionsFlow.value = extensions + _untrustedExtensionsMapFlow.value = extensions .filterIsInstance() - .map { it.extension } + .associate { it.extension.pkgName to it.extension } _isInitialized.value = true } /** - * Finds the available extensions in the [api] and updates [availableExtensions]. + * Finds the available extensions in the [api] and updates [_availableExtensionsMapFlow]. */ suspend fun findAvailableExtensions() { val extensions: List = try { @@ -126,7 +134,7 @@ class ExtensionManager( enableAdditionalSubLanguages(extensions) - _availableExtensionsFlow.value = extensions + _availableExtensionsMapFlow.value = extensions.associateBy { it.pkgName } updatedInstalledExtensionsStatuses(extensions) setupAvailableExtensionsSourcesDataMap(extensions) } @@ -172,35 +180,31 @@ class ExtensionManager( return } - val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList() + val installedExtensionsMap = _installedExtensionsMapFlow.value.toMutableMap() var changed = false - - for ((index, installedExt) in mutInstalledExtensions.withIndex()) { - val pkgName = installedExt.pkgName + for ((pkgName, extension) in installedExtensionsMap) { val availableExt = availableExtensions.find { it.pkgName == pkgName } - if (availableExt == null && !installedExt.isObsolete) { - mutInstalledExtensions[index] = installedExt.copy(isObsolete = true) + if (availableExt == null && !extension.isObsolete) { + installedExtensionsMap[pkgName] = extension.copy(isObsolete = true) changed = true } else if (availableExt != null) { - val hasUpdate = installedExt.updateExists(availableExt) - - if (installedExt.hasUpdate != hasUpdate) { - mutInstalledExtensions[index] = installedExt.copy( + val hasUpdate = extension.updateExists(availableExt) + if (extension.hasUpdate != hasUpdate) { + installedExtensionsMap[pkgName] = extension.copy( hasUpdate = hasUpdate, repoUrl = availableExt.repoUrl, ) - changed = true } else { - mutInstalledExtensions[index] = installedExt.copy( + installedExtensionsMap[pkgName] = extension.copy( repoUrl = availableExt.repoUrl, ) - changed = true } + changed = true } } if (changed) { - _installedExtensionsFlow.value = mutInstalledExtensions + _installedExtensionsMapFlow.value = installedExtensionsMap } updatePendingUpdatesCount() } @@ -224,8 +228,7 @@ class ExtensionManager( * @param extension The extension to be updated. */ fun updateExtension(extension: Extension.Installed): Flow { - val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName } - ?: return emptyFlow() + val availableExt = _availableExtensionsMapFlow.value[extension.pkgName] ?: return emptyFlow() return installExtension(availableExt) } @@ -261,24 +264,16 @@ class ExtensionManager( * * @param extension the extension to trust */ - fun trust(extension: Extension.Untrusted) { - val untrustedPkgNames = _untrustedExtensionsFlow.value.map { it.pkgName }.toSet() - if (extension.pkgName !in untrustedPkgNames) return + suspend fun trust(extension: Extension.Untrusted) { + _untrustedExtensionsMapFlow.value[extension.pkgName] ?: return trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash) - val nowTrustedExtensions = _untrustedExtensionsFlow.value - .filter { it.pkgName == extension.pkgName && it.versionCode == extension.versionCode } - _untrustedExtensionsFlow.value -= nowTrustedExtensions + _untrustedExtensionsMapFlow.value -= extension.pkgName - launchNow { - nowTrustedExtensions - .map { extension -> - async { ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) }.await() - } - .filterIsInstance() - .forEach { registerNewExtension(it.extension) } - } + ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) + .let { it as? LoadResult.Success } + ?.let { registerNewExtension(it.extension) } } /** @@ -287,7 +282,7 @@ class ExtensionManager( * @param extension The extension to be registered. */ private fun registerNewExtension(extension: Extension.Installed) { - _installedExtensionsFlow.value += extension + _installedExtensionsMapFlow.value += extension } /** @@ -297,13 +292,7 @@ class ExtensionManager( * @param extension The extension to be registered. */ private fun registerUpdatedExtension(extension: Extension.Installed) { - val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList() - val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName } - if (oldExtension != null) { - mutInstalledExtensions -= oldExtension - } - mutInstalledExtensions += extension - _installedExtensionsFlow.value = mutInstalledExtensions + _installedExtensionsMapFlow.value += extension } /** @@ -313,14 +302,8 @@ class ExtensionManager( * @param pkgName The package name of the uninstalled application. */ private fun unregisterExtension(pkgName: String) { - val installedExtension = _installedExtensionsFlow.value.find { it.pkgName == pkgName } - if (installedExtension != null) { - _installedExtensionsFlow.value -= installedExtension - } - val untrustedExtension = _untrustedExtensionsFlow.value.find { it.pkgName == pkgName } - if (untrustedExtension != null) { - _untrustedExtensionsFlow.value -= untrustedExtension - } + _installedExtensionsMapFlow.value -= pkgName + _untrustedExtensionsMapFlow.value -= pkgName } /** @@ -339,14 +322,9 @@ class ExtensionManager( } override fun onExtensionUntrusted(extension: Extension.Untrusted) { - val installedExtension = _installedExtensionsFlow.value - .find { it.pkgName == extension.pkgName } - - if (installedExtension != null) { - _installedExtensionsFlow.value -= installedExtension - } else { - _untrustedExtensionsFlow.value += extension - } + _installedExtensionsMapFlow.value -= extension.pkgName + _untrustedExtensionsMapFlow.value += extension + updatePendingUpdatesCount() } override fun onPackageUninstalled(pkgName: String) { @@ -368,17 +346,24 @@ class ExtensionManager( } private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean { - val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName } + val availableExt = availableExtension + ?: _availableExtensionsMapFlow.value[pkgName] ?: return false return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) } private fun updatePendingUpdatesCount() { - val pendingUpdateCount = _installedExtensionsFlow.value.count { it.hasUpdate } + val pendingUpdateCount = _installedExtensionsMapFlow.value.values.count { it.hasUpdate } preferences.extensionUpdatesCount().set(pendingUpdateCount) if (pendingUpdateCount == 0) { ExtensionUpdateNotifier(context).dismiss() } } + + 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()) + } } 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 e0a008e1c..a0ccb23fb 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 @@ -9,12 +9,10 @@ import androidx.core.content.ContextCompat import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import logcat.LogPriority -import tachiyomi.core.common.util.lang.launchNow import tachiyomi.core.common.util.system.logcat /** @@ -23,29 +21,23 @@ 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()) - /** - * Registers this broadcast receiver - */ fun register(context: Context) { ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED) } - /** - * Returns the intent filter this receiver should subscribe to. - */ - private val filter - get() = 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, @@ -58,7 +50,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Intent.ACTION_PACKAGE_ADDED, ACTION_EXTENSION_ADDED -> { if (isReplacing(intent)) return - launchNow { + scope.launch { when (val result = getExtensionFromIntent(context, intent)) { is LoadResult.Success -> listener.onExtensionInstalled(result.extension) is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension) @@ -67,7 +59,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : } } Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> { - launchNow { + scope.launch { when (val result = getExtensionFromIntent(context, intent)) { is LoadResult.Success -> listener.onExtensionUpdated(result.extension) is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension) @@ -107,9 +99,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : logcat(LogPriority.WARN) { "Package name not found" } return LoadResult.Error } - return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { - ExtensionLoader.loadExtensionFromPkgName(context, pkgName) - }.await() + return ExtensionLoader.loadExtensionFromPkgName(context, pkgName) } /** 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 0468c45e5..50ab94279 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 @@ -172,7 +172,7 @@ 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. */ - 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)" } @@ -223,7 +223,8 @@ internal object ExtensionLoader { * @param context The application context. * @param extensionInfo The extension to load. */ - private fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult { + @Suppress("LongMethod", "CyclomaticComplexMethod", "ReturnCount") + private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult { val pkgManager = context.packageManager val pkgInfo = extensionInfo.packageInfo val appInfo = pkgInfo.applicationInfo @@ -252,7 +253,7 @@ internal object ExtensionLoader { if (signatures.isNullOrEmpty()) { logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } return LoadResult.Error - } else if (!trustExtension.isTrusted(pkgInfo, signatures.last())) { + } else if (!trustExtension.isTrusted(pkgInfo, signatures)) { val extension = Extension.Untrusted( extName, pkgName, 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 e71d65997..feb69c5ce 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 @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import tachiyomi.core.common.util.lang.launchIO import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt @@ -196,7 +197,9 @@ class ExtensionsScreenModel( } fun trustExtension(extension: Extension.Untrusted) { - extensionManager.trust(extension) + screenModelScope.launch { + extensionManager.trust(extension) + } } @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 017af7479..42355c504 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 @@ -40,9 +40,7 @@ fun SourceFilterDialog( ) { val updateFilters = { onUpdate(filters) } - AdaptiveSheet( - onDismissRequest = onDismissRequest, - ) { + AdaptiveSheet(onDismissRequest = onDismissRequest) { LazyColumn { stickyHeader { Row( 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 3f7438b7c..1cb9ba3ff 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 @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.produceState import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.presentation.util.ioCoroutineScope @@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import tachiyomi.core.common.preference.toggle import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.model.Manga @@ -38,6 +40,7 @@ abstract class SearchScreenModel( private val extensionManager: ExtensionManager = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val getManga: GetManga = Injekt.get(), + private val preferences: SourcePreferences = Injekt.get(), ) : StateScreenModel(initialState) { private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher() @@ -60,6 +63,14 @@ abstract class SearchScreenModel( ) } + init { + screenModelScope.launch { + preferences.globalSearchFilterState().changes().collectLatest { state -> + mutableState.update { it.copy(onlyShowHasResults = state) } + } + } + } + @Composable fun getManga(initialManga: Manga): androidx.compose.runtime.State { return produceState(initialValue = initialManga) { @@ -107,7 +118,7 @@ abstract class SearchScreenModel( } fun toggleFilterResults() { - mutableState.update { it.copy(onlyShowHasResults = !it.onlyShowHasResults) } + preferences.globalSearchFilterState().toggle() } fun search() { 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 b2771d4e2..e866c98c9 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 @@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -106,10 +107,10 @@ class LibraryScreenModel( getTracksPerManga.subscribe(), getTrackingFilterFlow(), downloadCache.changes, - ) { searchQuery, library, tracks, loggedInTrackers, _ -> + ) { searchQuery, library, tracks, trackingFiler, _ -> library - .applyFilters(tracks, loggedInTrackers) - .applySort(tracks) + .applyFilters(tracks, trackingFiler) + .applySort(tracks, trackingFiler.keys) .mapValues { (_, value) -> if (searchQuery != null) { // Filter query @@ -173,9 +174,10 @@ class LibraryScreenModel( /** * Applies library filters to the given map of manga. */ + @Suppress("LongMethod", "CyclomaticComplexMethod") private suspend fun LibraryMap.applyFilters( trackMap: Map>, - loggedInTrackers: Map, + trackingFiler: Map, ): LibraryMap { val prefs = getLibraryItemPreferencesFlow().first() val downloadedOnly = prefs.globalFilterDownloaded @@ -187,10 +189,10 @@ class LibraryScreenModel( val filterCompleted = prefs.filterCompleted val filterIntervalCustom = prefs.filterIntervalCustom - val isNotLoggedInAnyTrack = loggedInTrackers.isEmpty() + val isNotLoggedInAnyTrack = trackingFiler.isEmpty() - val excludedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null } - val includedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null } + val excludedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null } + val includedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null } val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() val filterFnDownloaded: (LibraryItem) -> Boolean = { @@ -254,9 +256,11 @@ class LibraryScreenModel( /** * Applies library sorting to the given map of manga. */ + @Suppress("LongMethod", "CyclomaticComplexMethod") private fun LibraryMap.applySort( // Map> trackMap: Map>, + loggedInTrackerIds: Set ): LibraryMap { val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase()) @@ -264,7 +268,7 @@ class LibraryScreenModel( val defaultTrackerScoreSortValue = -1.0 val trackerScores by lazy { - val trackerMap = trackerManager.loggedInTrackers().associateBy { e -> e.id } + val trackerMap = trackerManager.getAll(loggedInTrackerIds).associateBy { e -> e.id } trackMap.mapValues { entry -> when { entry.value.isEmpty() -> null @@ -405,18 +409,17 @@ class LibraryScreenModel( * @return map of track id with the filter value */ private fun getTrackingFilterFlow(): Flow> { - val loggedInTrackers = trackerManager.loggedInTrackers() - return if (loggedInTrackers.isNotEmpty()) { - val prefFlows = loggedInTrackers - .map { libraryPreferences.filterTracking(it.id.toInt()).changes() } - .toTypedArray() - combine(*prefFlows) { + return trackerManager.loggedInTrackersFlow().flatMapLatest { loggedInTrackers -> + if (loggedInTrackers.isEmpty()) return@flatMapLatest flowOf(emptyMap()) + + val prefFlows = loggedInTrackers.map { tracker -> + libraryPreferences.filterTracking(tracker.id.toInt()).changes() + } + combine(prefFlows) { loggedInTrackers .mapIndexed { index, tracker -> tracker.id to it[index] } .toMap() } - } else { - flowOf(emptyMap()) } } 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 f0856f830..bd5867797 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 @@ -4,6 +4,8 @@ import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.data.track.TrackerManager +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.getAndSet @@ -16,17 +18,22 @@ import tachiyomi.domain.library.model.LibrarySort import tachiyomi.domain.library.service.LibraryPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import kotlin.time.Duration.Companion.seconds class LibrarySettingsScreenModel( val preferences: BasePreferences = Injekt.get(), val libraryPreferences: LibraryPreferences = Injekt.get(), private val setDisplayMode: SetDisplayMode = Injekt.get(), private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(), - private val trackerManager: TrackerManager = Injekt.get(), + trackerManager: TrackerManager = Injekt.get(), ) : ScreenModel { - val trackers - get() = trackerManager.trackers.filter { it.isLoggedIn } + val trackersFlow = trackerManager.loggedInTrackersFlow() + .stateIn( + scope = screenModelScope, + started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), + initialValue = trackerManager.loggedInTrackers() + ) fun toggleFilter(preference: (LibraryPreferences) -> Preference) { preference(libraryPreferences).getAndSet { 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 75a98c5df..de79d4d0d 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 @@ -5,6 +5,7 @@ import android.net.Uri import androidx.compose.material3.SnackbarHostState import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope +import coil3.asDrawable import coil3.imageLoader import coil3.request.ImageRequest import coil3.size.Size 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 c2466ffd4..e500a04d3 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 @@ -138,7 +138,7 @@ class MangaScreen( ) }.takeIf { isHttpSource }, onTrackingClicked = { - if (screenModel.loggedInTrackers.isEmpty()) { + if (!successState.hasLoggedInTrackers) { navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking)) } else { screenModel.showTrackDialog() 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 14cb037df..99a1b1612 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 @@ -31,7 +31,6 @@ import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.chapter.getNextUnread import eu.kanade.tachiyomi.util.removeCovers @@ -45,7 +44,6 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -117,8 +115,6 @@ class MangaScreenModel( private val successState: State.Success? get() = state.value as? State.Success - val loggedInTrackers by lazy { trackerManager.trackers.filter { it.isLoggedIn } } - val manga: Manga? get() = successState?.manga @@ -978,18 +974,24 @@ class MangaScreenModel( val manga = successState?.manga ?: return screenModelScope.launchIO { - getTracks.subscribe(manga.id) - .catch { logcat(LogPriority.ERROR, it) } - .map { tracks -> - loggedInTrackers - // Map to TrackItem - .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 } - } + combine( + getTracks.subscribe(manga.id).catch { logcat(LogPriority.ERROR, it) }, + trackerManager.loggedInTrackersFlow(), + ) { mangaTracks, loggedInTrackers -> + // Show only if the service supports this manga's source + val supportedTrackers = loggedInTrackers.filter { (it as? EnhancedTracker)?.accept(source!!) ?: true } + val supportedTrackerIds = supportedTrackers.map { it.id }.toHashSet() + val supportedTrackerTracks = mangaTracks.filter { it.trackerId in supportedTrackerIds } + supportedTrackerTracks.size to supportedTrackers.isNotEmpty() + } .distinctUntilChanged() - .collectLatest { trackItems -> - updateSuccessState { it.copy(trackItems = trackItems) } + .collectLatest { (trackingCount, hasLoggedInTrackers) -> + updateSuccessState { + it.copy( + trackingCount = trackingCount, + hasLoggedInTrackers = hasLoggedInTrackers, + ) + } } } } @@ -1053,7 +1055,8 @@ class MangaScreenModel( val chapters: List, val availableScanlators: Set, val excludedScanlators: Set, - val trackItems: List = emptyList(), + val trackingCount: Int = 0, + val hasLoggedInTrackers: Boolean = false, val isRefreshingData: Boolean = false, val dialog: Dialog? = null, val hasPromptedToAddBefore: Boolean = false, @@ -1099,9 +1102,6 @@ class MangaScreenModel( val filterActive: Boolean get() = scanlatorFilterActive || manga.chaptersFiltered() - val trackingCount: Int - get() = trackItems.count { it.track != null } - /** * Applies the view filters to the list of chapters obtained from the database. * @return an observable of the list of chapters filtered and sorted. 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 1ba697f24..4c66940c9 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 @@ -239,7 +239,7 @@ data class TrackInfoDialogHomeScreen( } private fun List.mapToTrackItem(): List { - val loggedInTrackers = Injekt.get().trackers.filter { it.isLoggedIn } + val loggedInTrackers = Injekt.get().loggedInTrackers() val source = Injekt.get().getOrStub(sourceId) return loggedInTrackers // Map to TrackItem 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 62bf23646..ae471a025 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 @@ -51,6 +51,7 @@ import eu.kanade.presentation.reader.ReadingModeSelectDialog import eu.kanade.presentation.reader.appbars.ReaderAppBars import eu.kanade.presentation.reader.settings.ReaderSettingsDialog import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.databinding.ReaderActivityBinding @@ -872,7 +873,9 @@ class ReaderActivity : BaseActivity() { input.copyTo(output) } } - SubsamplingScaleImageView.setDisplayProfile(outputStream.toByteArray()) + val data = outputStream.toByteArray() + SubsamplingScaleImageView.setDisplayProfile(data) + TachiyomiImageDecoder.displayProfile = data } } 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 373ed97a8..6b08dfeb3 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 @@ -4,6 +4,7 @@ import android.content.Context import android.graphics.Bitmap import android.net.Uri import androidx.core.app.NotificationCompat +import coil3.asDrawable import coil3.imageLoader import coil3.request.CachePolicy import coil3.request.ImageRequest diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt similarity index 57% rename from app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt index 89856bf22..397ac51bc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt @@ -3,26 +3,22 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import mihon.core.common.extensions.toZipFile +import mihon.core.common.archive.ArchiveReader import tachiyomi.core.common.util.system.ImageUtil -import java.nio.channels.SeekableByteChannel /** - * Loader used to load a chapter from a .zip or .cbz file. + * Loader used to load a chapter from an archive file. */ -internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() { - - private val zip = channel.toZipFile() - +internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() { override var isLocal: Boolean = true - override suspend fun getPages(): List { - return zip.entries.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } + 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 = { zip.getInputStream(entry) } + stream = { reader.getInputStream(entry.name)!! } status = Page.State.READY } } @@ -35,6 +31,6 @@ internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() { override fun recycle() { super.recycle() - zip.close() + reader.close() } } 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 1cd18bceb..3c1b34d6c 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 @@ -1,14 +1,13 @@ package eu.kanade.tachiyomi.ui.reader.loader import android.content.Context -import com.github.junrar.exception.UnsupportedRarV5Exception import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter +import mihon.core.common.archive.archiveReader import tachiyomi.core.common.i18n.stringResource -import tachiyomi.core.common.storage.openReadOnlyChannel import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.manga.model.Manga @@ -95,13 +94,8 @@ class ChapterLoader( source is LocalSource -> source.getFormat(chapter.chapter).let { format -> when (format) { is Format.Directory -> DirectoryPageLoader(format.file) - is Format.Zip -> ZipPageLoader(format.file.openReadOnlyChannel(context)) - is Format.Rar -> try { - RarPageLoader(format.file.openInputStream()) - } catch (e: UnsupportedRarV5Exception) { - error(context.stringResource(MR.strings.loader_rar5_error)) - } - is Format.Epub -> EpubPageLoader(format.file.openReadOnlyChannel(context)) + is Format.Archive -> ArchivePageLoader(format.file.archiveReader(context)) + is Format.Epub -> EpubPageLoader(format.file.archiveReader(context)) } } source is HttpSource -> HttpPageLoader(chapter, source) 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 abef28540..7b0f5c36c 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 @@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import tachiyomi.core.common.storage.openReadOnlyChannel +import mihon.core.common.archive.archiveReader import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.injectLazy @@ -27,7 +27,7 @@ internal class DownloadPageLoader( private val context: Application by injectLazy() - private var zipPageLoader: ZipPageLoader? = null + private var archivePageLoader: ArchivePageLoader? = null override var isLocal: Boolean = true @@ -43,11 +43,11 @@ internal class DownloadPageLoader( override fun recycle() { super.recycle() - zipPageLoader?.recycle() + archivePageLoader?.recycle() } private suspend fun getPagesFromArchive(file: UniFile): List { - val loader = ZipPageLoader(file.openReadOnlyChannel(context)).also { zipPageLoader = it } + val loader = ArchivePageLoader(file.archiveReader(context)).also { archivePageLoader = it } return loader.getPages() } @@ -63,6 +63,6 @@ internal class DownloadPageLoader( } override suspend fun loadPage(page: ReaderPage) { - zipPageLoader?.loadPage(page) + archivePageLoader?.loadPage(page) } } 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 baf65324b..8ace2fdee 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 @@ -3,21 +3,21 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.storage.EpubFile -import java.nio.channels.SeekableByteChannel +import mihon.core.common.archive.ArchiveReader /** * Loader used to load a chapter from a .epub file. */ -internal class EpubPageLoader(channel: SeekableByteChannel) : PageLoader() { +internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() { - private val epub = EpubFile(channel) + private val epub = EpubFile(reader) override var isLocal: Boolean = true override suspend fun getPages(): List { return epub.getImagesFromPages() .mapIndexed { i, path -> - val streamFn = { epub.getInputStream(epub.getEntry(path)!!) } + val streamFn = { epub.getInputStream(path)!! } ReaderPage(i).apply { stream = streamFn status = Page.State.READY diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt deleted file mode 100644 index b1db3300d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ /dev/null @@ -1,67 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.loader - -import com.github.junrar.Archive -import com.github.junrar.rarfile.FileHeader -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import tachiyomi.core.common.util.system.ImageUtil -import java.io.InputStream -import java.io.PipedInputStream -import java.io.PipedOutputStream -import java.util.concurrent.Executors - -/** - * Loader used to load a chapter from a .rar or .cbr file. - */ -internal class RarPageLoader(inputStream: InputStream) : PageLoader() { - - private val rar = Archive(inputStream) - - override var isLocal: Boolean = true - - /** - * Pool for copying compressed files to an input stream. - */ - private val pool = Executors.newFixedThreadPool(1) - - override suspend fun getPages(): List { - return rar.fileHeaders.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } } - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .mapIndexed { i, header -> - ReaderPage(i).apply { - stream = { getStream(header) } - status = Page.State.READY - } - } - .toList() - } - - override suspend fun loadPage(page: ReaderPage) { - check(!isRecycled) - } - - override fun recycle() { - super.recycle() - rar.close() - pool.shutdown() - } - - /** - * Returns an input stream for the given [header]. - */ - private fun getStream(header: FileHeader): InputStream { - val pipeIn = PipedInputStream() - val pipeOut = PipedOutputStream(pipeIn) - pool.execute { - try { - pipeOut.use { - rar.extractFile(header, it) - } - } catch (e: Exception) { - } - } - return pipeIn - } -} 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 63162788c..57def3c12 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 @@ -17,6 +17,12 @@ class ReaderPreferences( fun flashOnPageChange() = preferenceStore.getBoolean("pref_reader_flash", false) + fun flashDurationMillis() = preferenceStore.getInt("pref_reader_flash_duration", MILLI_CONVERSION) + + fun flashPageInterval() = preferenceStore.getInt("pref_reader_flash_interval", 1) + + fun flashColor() = preferenceStore.getEnum("pref_reader_flash_mode", FlashColor.BLACK) + fun doubleTapAnimSpeed() = preferenceStore.getInt("pref_double_tap_anim_speed", 500) fun showPageNumber() = preferenceStore.getBoolean("pref_show_page_number_key", true) @@ -133,6 +139,12 @@ class ReaderPreferences( // endregion + enum class FlashColor { + BLACK, + WHITE, + WHITE_BLACK + } + enum class TappingInvertMode( val titleRes: StringResource, val shouldInvertHorizontal: Boolean = false, @@ -155,6 +167,8 @@ class ReaderPreferences( const val WEBTOON_PADDING_MIN = 0 const val WEBTOON_PADDING_MAX = 25 + const val MILLI_CONVERSION = 100 + val TapZones = listOf( MR.strings.label_default, MR.strings.l_nav, 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 2cd0f2840..61e78c161 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 @@ -18,23 +18,28 @@ import androidx.annotation.StyleRes import androidx.appcompat.widget.AppCompatImageView import androidx.core.os.postDelayed import androidx.core.view.isVisible +import coil3.BitmapImage +import coil3.asDrawable import coil3.dispose import coil3.imageLoader import coil3.request.CachePolicy import coil3.request.ImageRequest import coil3.request.crossfade +import coil3.size.Precision +import coil3.size.ViewSizeResolver import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE import com.github.chrisbanes.photoview.PhotoView +import eu.kanade.tachiyomi.data.coil.cropBorders +import eu.kanade.tachiyomi.data.coil.customDecoder import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.view.isVisibleOnScreen -import java.io.InputStream -import java.nio.ByteBuffer +import okio.BufferedSource /** * A wrapper view for showing page image. @@ -146,14 +151,14 @@ open class ReaderPageImageView @JvmOverloads constructor( } } - fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) { + fun setImage(source: BufferedSource, isAnimated: Boolean, config: Config) { this.config = config if (isAnimated) { prepareAnimatedImageView() - setAnimatedImage(inputStream, config) + setAnimatedImage(source, config) } else { prepareNonAnimatedImageView() - setNonAnimatedImage(inputStream, config) + setNonAnimatedImage(source, config) } } @@ -262,7 +267,7 @@ open class ReaderPageImageView @JvmOverloads constructor( } private fun setNonAnimatedImage( - image: Any, + data: Any, config: Config, ) = (pageView as? SubsamplingScaleImageView)?.apply { setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration()) @@ -283,12 +288,36 @@ open class ReaderPageImageView @JvmOverloads constructor( }, ) - when (image) { - is BitmapDrawable -> setImage(ImageSource.bitmap(image.bitmap)) - is InputStream -> setImage(ImageSource.inputStream(image)) - else -> throw IllegalArgumentException("Not implemented for class ${image::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() { @@ -331,18 +360,13 @@ open class ReaderPageImageView @JvmOverloads constructor( } private fun setAnimatedImage( - image: Any, + data: Any, config: Config, ) = (pageView as? AppCompatImageView)?.apply { if (this is PhotoView) { setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration()) } - val data = when (image) { - is Drawable -> image - is InputStream -> ByteBuffer.wrap(image.readBytes()) - else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}") - } val request = ImageRequest.Builder(context) .data(data) .memoryCachePolicy(CachePolicy.DISABLED) 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 7b6d1dd6a..71d499c38 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 @@ -18,14 +18,13 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import logcat.LogPriority +import okio.Buffer +import okio.BufferedSource import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.logcat -import java.io.BufferedInputStream -import java.io.ByteArrayInputStream -import java.io.InputStream /** * View of the ViewPager that contains a page of a chapter. @@ -139,38 +138,30 @@ class PagerPageHolder( val streamFn = page.stream ?: return try { - val (bais, isAnimated, background) = withIOContext { - streamFn().buffered(16).use { stream -> - process(item, stream).use { itemStream -> - val bais = ByteArrayInputStream(itemStream.readBytes()) - val isAnimated = ImageUtil.isAnimatedAndSupported(bais) - bais.reset() - val background = if (!isAnimated && viewer.config.automaticBackground) { - ImageUtil.chooseBackground(context, bais) - } else { - null - } - bais.reset() - Triple(bais, isAnimated, background) - } + 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) } withUIContext { - bais.use { - setImage( - it, - isAnimated, - Config( - zoomDuration = viewer.config.doubleTapAnimDuration, - minimumScaleType = viewer.config.imageScaleType, - cropBorders = viewer.config.imageCropBorders, - zoomStartPosition = viewer.config.imageZoomType, - landscapeZoom = viewer.config.landscapeZoom, - ), - ) - if (!isAnimated) { - pageBackground = background - } + setImage( + source, + isAnimated, + Config( + zoomDuration = viewer.config.doubleTapAnimDuration, + minimumScaleType = viewer.config.imageScaleType, + cropBorders = viewer.config.imageCropBorders, + zoomStartPosition = viewer.config.imageZoomType, + landscapeZoom = viewer.config.landscapeZoom, + ), + ) + if (!isAnimated) { + pageBackground = background } removeErrorLayout() } @@ -182,40 +173,40 @@ class PagerPageHolder( } } - private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream { + private fun process(page: ReaderPage, imageSource: BufferedSource): BufferedSource { if (viewer.config.dualPageRotateToFit) { - return rotateDualPage(imageStream) + return rotateDualPage(imageSource) } if (!viewer.config.dualPageSplit) { - return imageStream + return imageSource } if (page is InsertPage) { - return splitInHalf(imageStream) + return splitInHalf(imageSource) } - val isDoublePage = ImageUtil.isWideImage(imageStream) + val isDoublePage = ImageUtil.isWideImage(imageSource) if (!isDoublePage) { - return imageStream + return imageSource } onPageSplit(page) - return splitInHalf(imageStream) + return splitInHalf(imageSource) } - private fun rotateDualPage(imageStream: BufferedInputStream): InputStream { - val isDoublePage = ImageUtil.isWideImage(imageStream) + private fun rotateDualPage(imageSource: BufferedSource): BufferedSource { + val isDoublePage = ImageUtil.isWideImage(imageSource) return if (isDoublePage) { val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f - ImageUtil.rotateImage(imageStream, rotation) + ImageUtil.rotateImage(imageSource, rotation) } else { - imageStream + imageSource } } - private fun splitInHalf(imageStream: InputStream): InputStream { + 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 @@ -231,7 +222,7 @@ class PagerPageHolder( } } - return ImageUtil.splitInHalf(imageStream, side) + return ImageUtil.splitInHalf(imageSource, side) } private fun onPageSplit(page: ReaderPage) { 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 b501725f7..488db7bb6 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 @@ -22,15 +22,14 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope -import kotlinx.coroutines.suspendCancellableCoroutine import logcat.LogPriority +import okio.Buffer +import okio.BufferedSource import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.logcat -import java.io.BufferedInputStream -import java.io.InputStream /** * Holder of the webtoon reader for a single page of a chapter. @@ -188,16 +187,14 @@ class WebtoonPageHolder( val streamFn = page?.stream ?: return try { - val (openStream, isAnimated) = withIOContext { - val stream = streamFn().buffered(16) - val openStream = process(stream) - - val isAnimated = ImageUtil.isAnimatedAndSupported(stream) - Pair(openStream, isAnimated) + val (source, isAnimated) = withIOContext { + val source = streamFn().use { process(Buffer().readFrom(it)) } + val isAnimated = ImageUtil.isAnimatedAndSupported(source) + Pair(source, isAnimated) } withUIContext { frame.setImage( - openStream, + source, isAnimated, ReaderPageImageView.Config( zoomDuration = viewer.config.doubleTapAnimDuration, @@ -207,10 +204,6 @@ class WebtoonPageHolder( ) removeErrorLayout() } - // Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled - suspendCancellableCoroutine { continuation -> - continuation.invokeOnCancellation { openStream.close() } - } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) withUIContext { @@ -219,29 +212,29 @@ class WebtoonPageHolder( } } - private fun process(imageStream: BufferedInputStream): InputStream { + private fun process(imageSource: BufferedSource): BufferedSource { if (viewer.config.dualPageRotateToFit) { - return rotateDualPage(imageStream) + return rotateDualPage(imageSource) } if (viewer.config.dualPageSplit) { - val isDoublePage = ImageUtil.isWideImage(imageStream) + val isDoublePage = ImageUtil.isWideImage(imageSource) if (isDoublePage) { val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT - return ImageUtil.splitAndMerge(imageStream, upperSide) + return ImageUtil.splitAndMerge(imageSource, upperSide) } } - return imageStream + return imageSource } - private fun rotateDualPage(imageStream: BufferedInputStream): InputStream { - val isDoublePage = ImageUtil.isWideImage(imageStream) + private fun rotateDualPage(imageSource: BufferedSource): BufferedSource { + val isDoublePage = ImageUtil.isWideImage(imageSource) return if (isDoublePage) { val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f - ImageUtil.rotateImage(imageStream, rotation) + ImageUtil.rotateImage(imageSource, rotation) } else { - imageStream + imageSource } } 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 20c18c622..fe232fe87 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 @@ -28,7 +28,8 @@ class WebtoonRecyclerView @JvmOverloads constructor( private var atFirstPosition = false private var halfWidth = 0 private var halfHeight = 0 - private var originalHeight = 0 + var originalHeight = 0 + private set private var heightSet = false private var firstVisibleItemPosition = 0 private var lastVisibleItemPosition = 0 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 1e370a8c7..a965631fc 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 @@ -117,7 +117,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr recycler.getLocationInWindow(viewPositionRelativeToWindow) val pos = PointF( (event.rawX - viewPosition[0] + viewPositionRelativeToWindow[0]) / recycler.width, - (event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / recycler.height, + (event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / recycler.originalHeight, ) when (config.navigator.getAction(pos)) { NavigationRegion.MENU -> activity.toggleMenu() 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 0d553ab4b..174ac26dc 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 @@ -36,7 +36,7 @@ class StatsScreenModel( private val trackerManager: TrackerManager = Injekt.get(), ) : StateScreenModel(StatsScreenState.Loading) { - private val loggedInTrackers by lazy { trackerManager.trackers.fastFilter { it.isLoggedIn } } + private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() } init { screenModelScope.launchIO { 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 56e6c278b..ed27314ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -19,12 +19,13 @@ class CrashLogUtil( private val extensionManager: ExtensionManager = Injekt.get(), ) { - suspend fun dumpLogs() = withNonCancellableContext { + 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") } Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor() 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 2877768af..872552ef9 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 @@ -4,7 +4,7 @@ import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import androidx.core.graphics.drawable.toBitmap -import coil3.gif.ScaleDrawable +import coil3.size.ScaleDrawable fun Drawable.getBitmapOrNull(): Bitmap? = when (this) { is BitmapDrawable -> bitmap diff --git a/app/src/main/java/eu/kanade/test/DummyTracker.kt b/app/src/main/java/eu/kanade/test/DummyTracker.kt index 56092b440..4c77e660d 100644 --- a/app/src/main/java/eu/kanade/test/DummyTracker.kt +++ b/app/src/main/java/eu/kanade/test/DummyTracker.kt @@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import okhttp3.OkHttpClient import tachiyomi.domain.track.model.Track import tachiyomi.i18n.MR @@ -16,6 +18,7 @@ data class DummyTracker( override val name: String, override val supportsReadingDates: Boolean = false, override val isLoggedIn: Boolean = false, + override val isLoggedInFlow: Flow = flowOf(false), val valLogoColor: Int = Color.rgb(18, 25, 35), val valLogo: Int = R.drawable.ic_tracker_anilist, val valStatuses: List = (1L..6L).toList(), diff --git a/app/src/test/java/mihon/core/migration/MigratorTest.kt b/app/src/test/java/mihon/core/migration/MigratorTest.kt index e0feff3fd..a805b5630 100644 --- a/app/src/test/java/mihon/core/migration/MigratorTest.kt +++ b/app/src/test/java/mihon/core/migration/MigratorTest.kt @@ -1,6 +1,5 @@ package mihon.core.migration -import io.mockk.Called import io.mockk.slot import io.mockk.spyk import io.mockk.verify @@ -30,7 +29,7 @@ class MigratorTest { fun initilize() { migrationContext = MigrationContext(false) migrationJobFactory = spyk(MigrationJobFactory(migrationContext, CoroutineScope(Dispatchers.Main + Job()))) - migrationCompletedListener = spyk<() -> Unit>({}) + migrationCompletedListener = spyk(block = {}) migrationStrategyFactory = spyk(MigrationStrategyFactory(migrationJobFactory, migrationCompletedListener)) } @@ -59,7 +58,7 @@ class MigratorTest { val result = execute.await() assertFalse(result) - verify { migrationJobFactory.create(any()) wasNot Called } + verify(exactly = 0) { migrationJobFactory.create(any()) } } @Test @@ -72,7 +71,7 @@ class MigratorTest { val result = execute.await() assertFalse(result) - verify { migrationJobFactory.create(any()) wasNot Called } + verify(exactly = 0) { migrationJobFactory.create(any()) } } @Test diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 0ce5d68de..b72b8a98b 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -3,14 +3,16 @@ plugins { } dependencies { + implementation(androidx.gradle) + implementation(kotlinx.gradle) + implementation(kotlinx.compose.compiler.gradle) + implementation(libs.detekt.gradlePlugin) + implementation(gradleApi()) + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) implementation(files(androidx.javaClass.superclass.protectionDomain.codeSource.location)) implementation(files(compose.javaClass.superclass.protectionDomain.codeSource.location)) implementation(files(kotlinx.javaClass.superclass.protectionDomain.codeSource.location)) - implementation(androidx.gradle) - implementation(kotlinx.gradle) - implementation(libs.detekt.gradlePlugin) - implementation(gradleApi()) } repositories { diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index 1b058e0d8..e45d92988 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -14,3 +14,5 @@ dependencyResolutionManagement { } } } + +rootProject.name = "mihon-buildSrc" diff --git a/buildSrc/src/main/kotlin/mihon.android.application.compose.gradle.kts b/buildSrc/src/main/kotlin/mihon.android.application.compose.gradle.kts index eda5fb22e..6b330b1d9 100644 --- a/buildSrc/src/main/kotlin/mihon.android.application.compose.gradle.kts +++ b/buildSrc/src/main/kotlin/mihon.android.application.compose.gradle.kts @@ -1,4 +1,3 @@ -import mihon.buildlogic.AndroidConfig import mihon.buildlogic.configureCompose plugins { diff --git a/buildSrc/src/main/kotlin/mihon/buildlogic/AndroidConfig.kt b/buildSrc/src/main/kotlin/mihon/buildlogic/AndroidConfig.kt index afea120de..23f0d2b9a 100644 --- a/buildSrc/src/main/kotlin/mihon/buildlogic/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/mihon/buildlogic/AndroidConfig.kt @@ -1,11 +1,15 @@ package mihon.buildlogic import org.gradle.api.JavaVersion as GradleJavaVersion +import org.jetbrains.kotlin.gradle.dsl.JvmTarget as KotlinJvmTarget object AndroidConfig { const val COMPILE_SDK = 34 const val TARGET_SDK = 34 const val MIN_SDK = 26 const val NDK = "26.1.10909125" + + // https://youtrack.jetbrains.com/issue/KT-66995/JvmTarget-and-JavaVersion-compatibility-for-easier-JVM-version-setup val JavaVersion = GradleJavaVersion.VERSION_17 + val JvmTarget = KotlinJvmTarget.JVM_17 } diff --git a/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt b/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt index 874e556a2..4b9e762b9 100644 --- a/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt +++ b/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt @@ -8,9 +8,12 @@ import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.Project import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.the import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile val Project.androidx get() = the() @@ -37,15 +40,18 @@ internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *, } tasks.withType().configureEach { - kotlinOptions { - jvmTarget = AndroidConfig.JavaVersion.toString() - // freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" - // freeCompilerArgs += "-Xcontext-receivers" + compilerOptions { + jvmTarget.set(AndroidConfig.JvmTarget) + freeCompilerArgs.addAll( + "-opt-in=kotlin.RequiresOptIn", + "-Xcontext-receivers", + ) // Treat all Kotlin warnings as errors (disabled by default) // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties - // val warningsAsErrors: String? by project - // allWarningsAsErrors = warningsAsErrors.toBoolean() + val warningsAsErrors: String? by project + allWarningsAsErrors.set(warningsAsErrors.toBoolean()) + } } @@ -55,51 +61,41 @@ internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *, } internal fun Project.configureCompose(commonExtension: CommonExtension<*, *, *, *, *, *>) { + pluginManager.apply(kotlinx.plugins.compose.compiler.get().pluginId) + commonExtension.apply { buildFeatures { compose = true } - composeOptions { - kotlinCompilerExtensionVersion = compose.versions.compiler.get() - } - dependencies { "implementation"(platform(compose.bom)) } } - tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs += buildComposeMetricsParameters() + extensions.configure { + // Enable strong skipping mode + enableStrongSkippingMode.set(true) - // Enable experimental compiler opts - // https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.9 - freeCompilerArgs += listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:nonSkippingGroupOptimization=true", - ) + // Enable experimental compiler opts + // 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 relativePath = projectDir.relativeTo(rootDir) + if (enableMetrics) { + val buildDirPath = rootProjectDir.resolve("compose-metrics").resolve(relativePath) + metricsDestination.set(buildDirPath) + } + if (enableReports) { + val buildDirPath = rootProjectDir.resolve("compose-reports").resolve(relativePath) + reportsDestination.set(buildDirPath) } } -} -private fun Project.buildComposeMetricsParameters(): List { - val rootProjectDir = rootProject.layout.buildDirectory.asFile.get() - val relativePath = projectDir.relativeTo(rootDir) - - val enableMetrics = project.providers.gradleProperty("enableComposeCompilerMetrics").orNull.toBoolean() - val enableReports = project.providers.gradleProperty("enableComposeCompilerReports").orNull.toBoolean() - - return listOfNotNull( - ("metricsDestination" to "compose-metrics").takeIf { enableMetrics }, - ("reportsDestination" to "compose-reports").takeIf { enableReports }, - ).flatMap { (flag, dirName) -> - val buildDirPath = rootProjectDir.resolve(dirName).resolve(relativePath).absolutePath - listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:$flag=$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 961a3b751..d84d2cb64 100644 --- a/buildSrc/src/main/kotlin/mihon/buildlogic/tasks/LocalesConfigPlugin.kt +++ b/buildSrc/src/main/kotlin/mihon/buildlogic/tasks/LocalesConfigPlugin.kt @@ -8,7 +8,7 @@ private val emptyResourcesElement = "\\s*|".t fun Project.getLocalesConfigTask(): TaskProvider { return tasks.register("generateLocalesConfig") { - val locales = fileTree("$projectDir/src/commonMain/resources/MR/") + val locales = fileTree("$projectDir/src/commonMain/moko-resources/") .matching { include("**/strings.xml") } .filterNot { it.readText().contains(emptyResourcesElement) } .map { diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index e31015dc4..d00fec682 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { implementation(libs.image.decoder) implementation(libs.unifile) - implementation(libs.bundles.archive) + implementation(libs.libarchive) api(kotlinx.coroutines.core) api(kotlinx.serialization.json) 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 29cea5824..b194c5ee3 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 @@ -1,48 +1,27 @@ package eu.kanade.tachiyomi.util.storage -import mihon.core.common.extensions.toZipFile -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import mihon.core.common.archive.ArchiveReader import org.jsoup.Jsoup import org.jsoup.nodes.Document import java.io.Closeable import java.io.File import java.io.InputStream -import java.nio.channels.SeekableByteChannel /** * Wrapper over ZipFile to load files in epub format. */ -class EpubFile(channel: SeekableByteChannel) : Closeable { - - /** - * Zip file of this epub. - */ - private val zip = channel.toZipFile() +class EpubFile(private val reader: ArchiveReader) : Closeable by reader { /** * Path separator used by this epub. */ private val pathSeparator = getPathSeparator() - /** - * Closes the underlying zip file. - */ - override fun close() { - zip.close() - } - /** * Returns an input stream for reading the contents of the specified zip file entry. */ - fun getInputStream(entry: ZipArchiveEntry): InputStream { - return zip.getInputStream(entry) - } - - /** - * Returns the zip file entry for the specified name, or null if not found. - */ - fun getEntry(name: String): ZipArchiveEntry? { - return zip.getEntry(name) + fun getInputStream(entryName: String): InputStream? { + return reader.getInputStream(entryName) } /** @@ -59,9 +38,9 @@ class EpubFile(channel: SeekableByteChannel) : Closeable { * Returns the path to the package document. */ fun getPackageHref(): String { - val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml")) + val meta = getInputStream(resolveZipPath("META-INF", "container.xml")) if (meta != null) { - val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") } + val metaDoc = meta.use { Jsoup.parse(it, null, "") } val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path") if (path != null) { return path @@ -74,8 +53,7 @@ class EpubFile(channel: SeekableByteChannel) : Closeable { * Returns the package document where all the files are listed. */ fun getPackageDocument(ref: String): Document { - val entry = zip.getEntry(ref) - return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") } + return getInputStream(ref)!!.use { Jsoup.parse(it, null, "") } } /** @@ -98,8 +76,7 @@ class EpubFile(channel: SeekableByteChannel) : Closeable { val basePath = getParentDirectory(packageHref) pages.forEach { page -> val entryPath = resolveZipPath(basePath, page) - val entry = zip.getEntry(entryPath) - val document = zip.getInputStream(entry).use { Jsoup.parse(it, null, "") } + val document = getInputStream(entryPath)!!.use { Jsoup.parse(it, null, "") } val imageBasePath = getParentDirectory(entryPath) document.allElements.forEach { @@ -117,8 +94,9 @@ class EpubFile(channel: SeekableByteChannel) : Closeable { * Returns the path separator used by the epub file. */ private fun getPathSeparator(): String { - val meta = zip.getEntry("META-INF\\container.xml") + val meta = getInputStream("META-INF\\container.xml") return if (meta != null) { + meta.close() "\\" } else { "/" diff --git a/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveEntry.kt b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveEntry.kt new file mode 100644 index 000000000..26240de00 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveEntry.kt @@ -0,0 +1,6 @@ +package mihon.core.common.archive + +class ArchiveEntry( + val name: String, + val isFile: Boolean, +) 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 new file mode 100644 index 000000000..1499867c8 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveInputStream.kt @@ -0,0 +1,63 @@ +package mihon.core.common.archive + +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +import me.zhanghai.android.libarchive.ArchiveException +import java.io.InputStream +import java.nio.ByteBuffer +import kotlin.concurrent.Volatile + +class ArchiveInputStream(buffer: Long, size: Long) : InputStream() { + private val lock = Any() + + @Volatile + private var isClosed = false + + private val archive = Archive.readNew() + + init { + try { + Archive.setCharset(archive, Charsets.UTF_8.name().toByteArray()) + Archive.readSupportFilterAll(archive) + Archive.readSupportFormatAll(archive) + Archive.readOpenMemoryUnsafe(archive, buffer, size) + } catch (e: ArchiveException) { + close() + throw e + } + } + + private val oneByteBuffer = ByteBuffer.allocateDirect(1) + + override fun read(): Int { + read(oneByteBuffer) + return if (oneByteBuffer.hasRemaining()) oneByteBuffer.get().toUByte().toInt() else -1 + } + + 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 + } + + private fun read(buffer: ByteBuffer) { + buffer.clear() + Archive.readData(archive, buffer) + buffer.flip() + } + + override fun close() { + synchronized(lock) { + if (isClosed) return + isClosed = true + } + + 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) + } +} 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 new file mode 100644 index 000000000..28467d0fe --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveReader.kt @@ -0,0 +1,42 @@ +package mihon.core.common.archive + +import android.content.Context +import android.os.ParcelFileDescriptor +import android.system.Os +import android.system.OsConstants +import com.hippo.unifile.UniFile +import me.zhanghai.android.libarchive.ArchiveException +import tachiyomi.core.common.storage.openFileDescriptor +import java.io.Closeable +import java.io.InputStream + +class ArchiveReader(pfd: ParcelFileDescriptor) : Closeable { + val size = pfd.statSize + val address = Os.mmap(0, size, OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, pfd.fileDescriptor, 0) + + inline fun useEntries(block: (Sequence) -> T): T = + ArchiveInputStream(address, size).use { block(generateSequence { it.getNextEntry() }) } + + fun getInputStream(entryName: String): InputStream? { + val archive = ArchiveInputStream(address, size) + try { + while (true) { + val entry = archive.getNextEntry() ?: break + if (entry.name == entryName) { + return archive + } + } + } catch (e: ArchiveException) { + archive.close() + throw e + } + archive.close() + return null + } + + override fun close() { + Os.munmap(address, size) + } +} + +fun UniFile.archiveReader(context: Context) = openFileDescriptor(context, "r").use { ArchiveReader(it) } 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 new file mode 100644 index 000000000..b5d201516 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ZipWriter.kt @@ -0,0 +1,74 @@ +package mihon.core.common.archive + +import android.content.Context +import android.system.Os +import android.system.StructStat +import com.hippo.unifile.UniFile +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +import me.zhanghai.android.libarchive.ArchiveException +import tachiyomi.core.common.storage.openFileDescriptor +import java.io.Closeable +import java.nio.ByteBuffer + +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) + private val buffer = ByteBuffer.allocateDirect(8192) + + init { + try { + Archive.setCharset(archive, Charsets.UTF_8.name().toByteArray()) + Archive.writeSetFormatZip(archive) + Archive.writeZipSetCompressionStore(archive) + Archive.writeOpenFd(archive, pfd.fd) + } catch (e: ArchiveException) { + close() + throw e + } + } + + fun write(file: UniFile) { + file.openFileDescriptor(context, "r").use { + val fd = it.fileDescriptor + ArchiveEntry.clear(entry) + ArchiveEntry.setPathnameUtf8(entry, file.name) + val stat = Os.fstat(fd) + ArchiveEntry.setStat(entry, stat.toArchiveStat()) + Archive.writeHeader(archive, entry) + while (true) { + buffer.clear() + Os.read(fd, buffer) + if (buffer.position() == 0) break + buffer.flip() + Archive.writeData(archive, buffer) + } + Archive.writeFinishEntry(archive) + } + } + + override fun close() { + ArchiveEntry.free(entry) + Archive.writeFree(archive) + pfd.close() + } +} + +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/mihon/core/common/extensions/SeekableByteChannel.kt b/core/common/src/main/kotlin/mihon/core/common/extensions/SeekableByteChannel.kt deleted file mode 100644 index 69e2d7201..000000000 --- a/core/common/src/main/kotlin/mihon/core/common/extensions/SeekableByteChannel.kt +++ /dev/null @@ -1,8 +0,0 @@ -package mihon.core.common.extensions - -import org.apache.commons.compress.archivers.zip.ZipFile -import java.nio.channels.SeekableByteChannel - -fun SeekableByteChannel.toZipFile(): ZipFile { - return ZipFile.Builder().setSeekableByteChannel(this).get() -} 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 257fe210d..4b04ff405 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 @@ -3,7 +3,6 @@ package tachiyomi.core.common.storage import android.content.Context import android.os.ParcelFileDescriptor import com.hippo.unifile.UniFile -import java.nio.channels.FileChannel val UniFile.extension: String? get() = name?.substringAfterLast('.') @@ -14,6 +13,5 @@ val UniFile.nameWithoutExtension: String? val UniFile.displayablePath: String get() = filePath ?: uri.toString() -fun UniFile.openReadOnlyChannel(context: Context): FileChannel { - return ParcelFileDescriptor.AutoCloseInputStream(context.contentResolver.openFileDescriptor(uri, "r")).channel -} +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/system/ImageUtil.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt index 03a2f2d60..2cdd2a306 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 @@ -13,7 +13,6 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.os.Build -import android.webkit.MimeTypeMap import androidx.annotation.ColorInt import androidx.core.graphics.alpha import androidx.core.graphics.applyCanvas @@ -24,13 +23,11 @@ import androidx.core.graphics.green import androidx.core.graphics.red import com.hippo.unifile.UniFile import logcat.LogPriority +import okio.Buffer +import okio.BufferedSource import tachiyomi.decoder.Format import tachiyomi.decoder.ImageDecoder -import java.io.BufferedInputStream -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream import java.io.InputStream -import java.net.URLConnection import java.util.Locale import kotlin.math.abs import kotlin.math.max @@ -41,12 +38,8 @@ object ImageUtil { 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 } - return contentType?.startsWith("image/") ?: false + val extension = name.substringAfterLast('.') + return ImageType.entries.any { it.extension == extension } || openStream?.let { findImageType(it) } != null } fun findImageType(openStream: () -> InputStream): ImageType? { @@ -70,15 +63,14 @@ object ImageUtil { } } - fun getExtensionFromMimeType(mime: String?): String { - return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) - ?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime] - ?: "jpg" + fun getExtensionFromMimeType(mime: String?, openStream: () -> InputStream): String { + val type = mime?.let { ImageType.entries.find { it.mime == mime } } ?: findImageType(openStream) + return type?.extension ?: "jpg" } - fun isAnimatedAndSupported(stream: InputStream): Boolean { + fun isAnimatedAndSupported(source: BufferedSource): Boolean { return try { - val type = getImageType(stream) ?: return false + val type = getImageType(source.peek().inputStream()) ?: return false // https://coil-kt.github.io/coil/getting_started/#supported-image-formats when (type.format) { Format.Gif -> true @@ -125,18 +117,16 @@ object ImageUtil { * * @return true if the width is greater than the height */ - fun isWideImage(imageStream: BufferedInputStream): Boolean { - val options = extractImageOptions(imageStream) + fun isWideImage(imageSource: BufferedSource): Boolean { + val options = extractImageOptions(imageSource) return options.outWidth > options.outHeight } /** - * Extract the 'side' part from imageStream and return it as InputStream. + * Extract the 'side' part from [BufferedSource] and return it as [BufferedSource]. */ - fun splitInHalf(imageStream: InputStream, side: Side): InputStream { - val imageBytes = imageStream.readBytes() - - val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + fun splitInHalf(imageSource: BufferedSource, side: Side): BufferedSource { + val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream()) val height = imageBitmap.height val width = imageBitmap.width @@ -150,22 +140,20 @@ object ImageUtil { half.applyCanvas { drawBitmap(imageBitmap, part, singlePage, null) } - val output = ByteArrayOutputStream() - half.compress(Bitmap.CompressFormat.JPEG, 100, output) + val output = Buffer() + half.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream()) - return ByteArrayInputStream(output.toByteArray()) + return output } - fun rotateImage(imageStream: InputStream, degrees: Float): InputStream { - val imageBytes = imageStream.readBytes() - - val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + fun rotateImage(imageSource: BufferedSource, degrees: Float): BufferedSource { + val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream()) val rotated = rotateBitMap(imageBitmap, degrees) - val output = ByteArrayOutputStream() - rotated.compress(Bitmap.CompressFormat.JPEG, 100, output) + val output = Buffer() + rotated.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream()) - return ByteArrayInputStream(output.toByteArray()) + return output } private fun rotateBitMap(bitmap: Bitmap, degrees: Float): Bitmap { @@ -176,10 +164,8 @@ object ImageUtil { /** * Split the image into left and right parts, then merge them into a new image. */ - fun splitAndMerge(imageStream: InputStream, upperSide: Side): InputStream { - val imageBytes = imageStream.readBytes() - - val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + fun splitAndMerge(imageSource: BufferedSource, upperSide: Side): BufferedSource { + val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream()) val height = imageBitmap.height val width = imageBitmap.width @@ -201,9 +187,9 @@ object ImageUtil { drawBitmap(imageBitmap, leftPart, bottomPart, null) } - val output = ByteArrayOutputStream() - result.compress(Bitmap.CompressFormat.JPEG, 100, output) - return ByteArrayInputStream(output.toByteArray()) + val output = Buffer() + result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream()) + return output } enum class Side { @@ -216,8 +202,8 @@ object ImageUtil { * * @return true if the height:width ratio is greater than 3. */ - private fun isTallImage(imageStream: InputStream): Boolean { - val options = extractImageOptions(imageStream, resetAfterExtraction = false) + private fun isTallImage(imageSource: BufferedSource): Boolean { + val options = extractImageOptions(imageSource) return (options.outHeight / options.outWidth) > 3 } @@ -225,17 +211,18 @@ object ImageUtil { * Splits tall images to improve performance of reader */ fun splitTallImage(tmpDir: UniFile, imageFile: UniFile, filenamePrefix: String): Boolean { - if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) { + val imageSource = imageFile.openInputStream().use { Buffer().readFrom(it) } + if (isAnimatedAndSupported(imageSource) || !isTallImage(imageSource)) { return true } - val bitmapRegionDecoder = getBitmapRegionDecoder(imageFile.openInputStream()) + val bitmapRegionDecoder = getBitmapRegionDecoder(imageSource.peek().inputStream()) if (bitmapRegionDecoder == null) { logcat { "Failed to create new instance of BitmapRegionDecoder" } return false } - val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { + val options = extractImageOptions(imageSource).apply { inJustDecodeBounds = false } @@ -548,16 +535,9 @@ object ImageUtil { /** * Used to check an image's dimensions without loading it in the memory. */ - private fun extractImageOptions( - imageStream: InputStream, - resetAfterExtraction: Boolean = true, - ): BitmapFactory.Options { - imageStream.mark(Int.MAX_VALUE) - - val imageBytes = imageStream.readBytes() + private fun extractImageOptions(imageSource: BufferedSource): BitmapFactory.Options { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) - if (resetAfterExtraction) imageStream.reset() + BitmapFactory.decodeStream(imageSource.peek().inputStream(), null, options) return options } @@ -571,12 +551,6 @@ object ImageUtil { } 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", - ) } val getDisplayMaxHeightInPx: Int diff --git a/data/build.gradle.kts b/data/build.gradle.kts index e3f9d9004..2fded2043 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { tasks { withType { - kotlinOptions.freeCompilerArgs += listOf( + compilerOptions.freeCompilerArgs.addAll( "-Xcontext-receivers", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", ) diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index c49c5bc51..5e2357181 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -31,7 +31,7 @@ dependencies { tasks { withType { - kotlinOptions.freeCompilerArgs += listOf( + compilerOptions.freeCompilerArgs.addAll( "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-Xcontext-receivers", ) 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 b2c3f6b44..3190e0456 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt @@ -41,27 +41,35 @@ object ChapterRecognition { } // Get chapter title with lower case - var name = chapterName.lowercase() + 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, "") - // Remove manga title from chapter title. - name = name.replace(mangaTitle.lowercase(), "").trim() + val numberMatch = number.findAll(cleanChapterName) - // Remove comma's or hyphens. - name = name.replace(',', '.').replace('-', '.') + when { + numberMatch.none() -> { + return chapterNumber ?: -1.0 + } + numberMatch.count() > 1 -> { + // Remove unwanted tags. + unwanted.replace(cleanChapterName, "").let { name -> + // Check base case ch.xx + basic.find(name)?.let { return getChapterNumberFromMatch(it) } - // Remove unwanted white spaces. - name = unwantedWhiteSpace.replace(name, "") + // need to find again first number might already removed + number.find(name)?.let { return getChapterNumberFromMatch(it) } + } + } + } - // Remove unwanted tags. - name = unwanted.replace(name, "") - - // Check base case ch.xx - basic.find(name)?.let { return getChapterNumberFromMatch(it) } - - // Take the first number encountered. - number.find(name)?.let { return getChapterNumberFromMatch(it) } - - return chapterNumber ?: -1.0 + // return the first number encountered + return getChapterNumberFromMatch(numberMatch.first()) } /** 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 90ee3a486..8369bfa5c 100644 --- a/domain/src/test/java/tachiyomi/domain/chapter/service/ChapterRecognitionTest.kt +++ b/domain/src/test/java/tachiyomi/domain/chapter/service/ChapterRecognitionTest.kt @@ -171,6 +171,17 @@ class ChapterRecognitionTest { assertChapter(mangaTitle, "Tokyo ESP 027: Part 002: Chapter 001", 027.0) } + /** + * Case where the chapter title contains the unwanted tag + * But follow by chapter number. + */ + @Test + fun `Number after unwanted tag`() { + val mangaTitle = "One-punch Man" + + assertChapter(mangaTitle, "Mag Version 195.5", 195.5) + } + @Test fun `Unparseable chapter`() { val mangaTitle = "random" diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 2357dfc0b..6cb627960 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,16 +1,17 @@ [versions] -agp_version = "8.3.1" -lifecycle_version = "2.7.0" -paging_version = "3.2.1" +agp_version = "8.5.1" +lifecycle_version = "2.8.3" +paging_version = "3.3.0" +interpolator_version = "1.0.0" [libraries] gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" } -annotation = "androidx.annotation:annotation:1.7.1" -appcompat = "androidx.appcompat:appcompat:1.6.1" +annotation = "androidx.annotation:annotation:1.8.0" +appcompat = "androidx.appcompat:appcompat:1.7.0" biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05" constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" -corektx = "androidx.core:core-ktx:1.12.0" +corektx = "androidx.core:core-ktx:1.13.1" splashscreen = "androidx.core:core-splashscreen:1.0.1" recyclerview = "androidx.recyclerview:recyclerview:1.3.2" viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01" @@ -25,9 +26,11 @@ workmanager = "androidx.work:work-runtime:2.9.0" paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" } paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" } -benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.3" -test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha03" -test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha03" +interpolator = { group = "androidx.interpolator", name = "interpolator", version.ref = "interpolator_version" } + +benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.4" +test-ext = "androidx.test.ext:junit-ktx:1.2.1" +test-espresso-core = "androidx.test.espresso:espresso-core:3.6.1" test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0" [bundles] diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index b37960e0a..ea9e52efd 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,16 +1,14 @@ [versions] -compiler = "1.5.11" -compose-bom = "2024.02.00-alpha02" -accompanist = "0.35.0-alpha" +compose-bom = "2024.07.00-alpha01" +accompanist = "0.35.1-alpha" [libraries] -compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compiler" } - -activity = "androidx.activity:activity-compose:1.8.2" +activity = "androidx.activity:activity-compose:1.9.0" bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" } foundation = { module = "androidx.compose.foundation:foundation" } animation = { module = "androidx.compose.animation:animation" } animation-graphics = { module = "androidx.compose.animation:animation-graphics" } +runtime = { module = "androidx.compose.runtime:runtime" } ui-tooling = { module = "androidx.compose.ui:ui-tooling" } ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } ui-util = { module = "androidx.compose.ui:ui-util" } @@ -18,9 +16,6 @@ ui-util = { module = "androidx.compose.ui:ui-util" } material3-core = { module = "androidx.compose.material3:material3" } material-icons = { module = "androidx.compose.material:material-icons-extended" } -# Some components aren't available in Material3 -material-core = { module = "androidx.compose.material:material" } - -glance = "androidx.glance:glance-appwidget:1.0.0" +glance = "androidx.glance:glance-appwidget:1.1.0" accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 4c8871cd9..03b23177d 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,15 +1,16 @@ [versions] -kotlin_version = "1.9.23" -serialization_version = "1.6.3" +kotlin_version = "2.0.0" +serialization_version = "1.7.1" xml_serialization_version = "0.86.3" [libraries] reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" } gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" } +compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin_version" } immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.7" } -coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.8.0" } +coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.8.1" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" } @@ -28,4 +29,5 @@ serialization = ["serialization-json", "serialization-json-okio", "serialization [plugins] android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin_version" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin_version" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin_version" } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a5e8555b..398a2f89b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -aboutlib_version = "11.1.1" -leakcanary = "2.13" -moko = "0.23.0" -okhttp_version = "5.0.0-alpha.12" +aboutlib_version = "11.2.2" +leakcanary = "2.14" +moko = "0.24.1" +okhttp_version = "5.0.0-alpha.14" richtext = "0.20.0" shizuku_version = "12.2.0" sqldelight = "2.0.2" @@ -14,7 +14,7 @@ detektCompose = "0.3.12" [libraries] desugar = "com.android.tools:desugar_jdk_libs:2.0.4" android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2" -google-services-gradle = "com.google.gms:google-services:4.4.1" +google-services-gradle = "com.google.gms:google-services:4.4.2" rxjava = "io.reactivex:rxjava:1.3.8" @@ -28,12 +28,11 @@ conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2" quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2" -jsoup = "org.jsoup:jsoup:1.17.2" +jsoup = "org.jsoup:jsoup:1.18.1" disklrucache = "com.jakewharton:disklrucache:2.0.2" -unifile = "com.github.tachiyomiorg:unifile:7c257e1c64" -common-compress = "org.apache.commons:commons-compress:1.26.1" -junrar = "com.github.junrar:junrar:7.5.5" +unifile = "com.github.tachiyomiorg:unifile:e0def6b3dc" +libarchive = "me.zhanghai.android.libarchive:library:1.1.0" sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" } @@ -43,26 +42,26 @@ preferencektx = "androidx.preference:preference-ktx:1.2.1" injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440" -coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.0-alpha06" } +coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.0-alpha08" } coil-core = { module = "io.coil-kt.coil3:coil" } coil-gif = { module = "io.coil-kt.coil3:coil-gif" } coil-compose = { module = "io.coil-kt.coil3:coil-compose" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" } -subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:aeaa170036" -image-decoder = "com.github.tachiyomiorg:image-decoder:e08e9be535" +subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:b8e1b0ed2b" +image-decoder = "com.github.tachiyomiorg:image-decoder:41c059e540" natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" } richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" } -material = "com.google.android.material:material:1.11.0" +material = "com.google.android.material:material:1.12.0" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" photoview = "com.github.chrisbanes:PhotoView:2.3.0" directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0" insetter = "dev.chrisbanes.insetter:insetter:0.6.1" -compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.2.0" +compose-materialmotion = "io.github.fornewid:material-motion-compose-core:2.0.1" compose-webview = "io.github.kevinnzou:compose-webview:0.33.6" compose-grid = "io.woong.compose.grid:grid:1.2.2" @@ -73,7 +72,7 @@ moko-gradle = { module = "dev.icerock.moko:resources-generator", version.ref = " logcat = "com.squareup.logcat:logcat:0.1" -firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.6.1" +firebase-analytics = "com.google.firebase:firebase-analytics:22.0.2" aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" } aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" } @@ -90,9 +89,9 @@ sqldelight-android-paging = { module = "app.cash.sqldelight:androidx-paging3-ext sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" } sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "sqldelight" } -junit = "org.junit.jupiter:junit-jupiter:5.10.2" -kotest-assertions = "io.kotest:kotest-assertions-core:5.8.1" -mockk = "io.mockk:mockk:1.13.10" +junit = "org.junit.jupiter:junit-jupiter:5.10.3" +kotest-assertions = "io.kotest:kotest-assertions-core:5.9.1" +mockk = "io.mockk:mockk:1.13.12" voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } @@ -104,7 +103,6 @@ detekt-rules-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatt detekt-rules-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } [bundles] -archive = ["common-compress", "junrar"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] js-engine = ["quickjs-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4..09523c0e5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426..b740cf133 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/i18n/build.gradle.kts b/i18n/build.gradle.kts index c782eac0f..7c7256acc 100644 --- a/i18n/build.gradle.kts +++ b/i18n/build.gradle.kts @@ -13,15 +13,11 @@ kotlin { applyDefaultHierarchyTemplate() sourceSets { - val commonMain by getting { + commonMain { dependencies { api(libs.moko.core) } } - - androidMain { - dependsOn(commonMain) // https://github.com/icerockdev/moko-resources/issues/562 - } } } @@ -40,7 +36,7 @@ android { } multiplatformResources { - multiplatformResourcesPackage = "tachiyomi.i18n" + resourcesPackage.set("tachiyomi.i18n") } tasks { @@ -50,7 +46,7 @@ tasks { } withType { - kotlinOptions.freeCompilerArgs += listOf( + compilerOptions.freeCompilerArgs.addAll( "-Xexpect-actual-classes", ) } diff --git a/i18n/src/commonMain/resources/MR/am/plurals.xml b/i18n/src/commonMain/moko-resources/am/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/am/plurals.xml rename to i18n/src/commonMain/moko-resources/am/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/am/strings.xml b/i18n/src/commonMain/moko-resources/am/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/am/strings.xml rename to i18n/src/commonMain/moko-resources/am/strings.xml diff --git a/i18n/src/commonMain/resources/MR/ar/plurals.xml b/i18n/src/commonMain/moko-resources/ar/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ar/plurals.xml rename to i18n/src/commonMain/moko-resources/ar/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/ar/strings.xml b/i18n/src/commonMain/moko-resources/ar/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/ar/strings.xml rename to i18n/src/commonMain/moko-resources/ar/strings.xml index 3f5ca8fd2..7ebedaeee 100644 --- a/i18n/src/commonMain/resources/MR/ar/strings.xml +++ b/i18n/src/commonMain/moko-resources/ar/strings.xml @@ -796,4 +796,13 @@ إضافة مستودع من المتوقع أن يتم إصدار فصول جديدة في حوالي 1%1$s، والتحقق من كل 2%2$s . متاح: %1$s / الكل: %2$s + الشهر السابق + الدليل القادم + الشهر القادم + القادم + عرض الثحديثات القادمة + أضف على أي حال + استبدل + بصمة مفتاح الانخراط موجودة من قبل + تعطيل التصغير \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/base/plurals.xml b/i18n/src/commonMain/moko-resources/base/plurals.xml similarity index 95% rename from i18n/src/commonMain/resources/MR/base/plurals.xml rename to i18n/src/commonMain/moko-resources/base/plurals.xml index d9c958afc..e436a8cb5 100644 --- a/i18n/src/commonMain/resources/MR/base/plurals.xml +++ b/i18n/src/commonMain/moko-resources/base/plurals.xml @@ -40,6 +40,11 @@ %d days + + 1 page + %1$s pages + + Missing %1$s chapter diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/base/strings.xml rename to i18n/src/commonMain/moko-resources/base/strings.xml index e4faa79d3..8a1fe6df8 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -366,6 +366,13 @@ Animate page transitions Flash on page change Reduces ghosting on e-ink displays + Flash duration + %1$s ms + Flash every + Flash with + Black + White + White and Black Double tap animation speed Show page number Show reading mode @@ -688,6 +695,7 @@ Set to update every New chapters predicted to be released in around %1$s, checking around every %2$s. + This manga is either completed, or there is no predicted release date. Soon Custom update frequency: Downloading (%1$d/%2$d) @@ -783,7 +791,6 @@ Failed to load pages: %1$s No pages found Source not found - RARv5 format is not supported Updating library @@ -871,6 +878,7 @@ Select cover image Select backup file No file picker app found + Failed to acquire persistent folder access. The app may behave unexpectedly. No file selected diff --git a/i18n/src/commonMain/resources/MR/be/plurals.xml b/i18n/src/commonMain/moko-resources/be/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/be/plurals.xml rename to i18n/src/commonMain/moko-resources/be/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/be/strings.xml b/i18n/src/commonMain/moko-resources/be/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/be/strings.xml rename to i18n/src/commonMain/moko-resources/be/strings.xml diff --git a/i18n/src/commonMain/resources/MR/bg/plurals.xml b/i18n/src/commonMain/moko-resources/bg/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/bg/plurals.xml rename to i18n/src/commonMain/moko-resources/bg/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/bg/strings.xml b/i18n/src/commonMain/moko-resources/bg/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/bg/strings.xml rename to i18n/src/commonMain/moko-resources/bg/strings.xml diff --git a/i18n/src/commonMain/resources/MR/bn/plurals.xml b/i18n/src/commonMain/moko-resources/bn/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/bn/plurals.xml rename to i18n/src/commonMain/moko-resources/bn/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/bn/strings.xml b/i18n/src/commonMain/moko-resources/bn/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/bn/strings.xml rename to i18n/src/commonMain/moko-resources/bn/strings.xml diff --git a/i18n/src/commonMain/resources/MR/ca/plurals.xml b/i18n/src/commonMain/moko-resources/ca/plurals.xml similarity index 95% rename from i18n/src/commonMain/resources/MR/ca/plurals.xml rename to i18n/src/commonMain/moko-resources/ca/plurals.xml index 89fd5c350..78d4572c3 100644 --- a/i18n/src/commonMain/resources/MR/ca/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ca/plurals.xml @@ -85,4 +85,9 @@ %d repositoris %d repositoris + + Demà + D’aquí a %1$d dies + D’aquí a %1$d dies + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ca/strings.xml b/i18n/src/commonMain/moko-resources/ca/strings.xml similarity index 97% rename from i18n/src/commonMain/resources/MR/ca/strings.xml rename to i18n/src/commonMain/moko-resources/ca/strings.xml index bc63d1260..d54f800aa 100644 --- a/i18n/src/commonMain/resources/MR/ca/strings.xml +++ b/i18n/src/commonMain/moko-resources/ca/strings.xml @@ -796,4 +796,18 @@ Aviat Freqüència d’actualització personalitzada: Obre el repositori d’origen + No s’ha pogut obtenir accés persistent a la carpeta. És possible que l’aplicació es comporti de manera inesperada. + Pròxim + Guia de pròximes actualitzacions + Mes següent + Afegeix igualment + Migra l’element existent + Desactiva l’allunyament + Substitueix + L’empremta digital de la clau de xifratge ja existeix + La clau de xifratge del repositori %1$s té la mateixa empremta digital que %2$s. +\nSi espereu que sigui així, se substituirà %2$s. En cas contrari, contacteu amb el mantenidor del repositori. + Perfil de visualització personalitzat + Mostra les pròximes actualitzacions + Mes anterior \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ceb/plurals.xml b/i18n/src/commonMain/moko-resources/ceb/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ceb/plurals.xml rename to i18n/src/commonMain/moko-resources/ceb/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/ceb/strings.xml b/i18n/src/commonMain/moko-resources/ceb/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ceb/strings.xml rename to i18n/src/commonMain/moko-resources/ceb/strings.xml diff --git a/i18n/src/commonMain/resources/MR/cs/plurals.xml b/i18n/src/commonMain/moko-resources/cs/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/cs/plurals.xml rename to i18n/src/commonMain/moko-resources/cs/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/cs/strings.xml b/i18n/src/commonMain/moko-resources/cs/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/cs/strings.xml rename to i18n/src/commonMain/moko-resources/cs/strings.xml diff --git a/i18n/src/commonMain/resources/MR/cv/plurals.xml b/i18n/src/commonMain/moko-resources/cv/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/cv/plurals.xml rename to i18n/src/commonMain/moko-resources/cv/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/cv/strings.xml b/i18n/src/commonMain/moko-resources/cv/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/cv/strings.xml rename to i18n/src/commonMain/moko-resources/cv/strings.xml diff --git a/i18n/src/commonMain/resources/MR/da/plurals.xml b/i18n/src/commonMain/moko-resources/da/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/da/plurals.xml rename to i18n/src/commonMain/moko-resources/da/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/da/strings.xml b/i18n/src/commonMain/moko-resources/da/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/da/strings.xml rename to i18n/src/commonMain/moko-resources/da/strings.xml diff --git a/i18n/src/commonMain/resources/MR/de/plurals.xml b/i18n/src/commonMain/moko-resources/de/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/de/plurals.xml rename to i18n/src/commonMain/moko-resources/de/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/de/strings.xml b/i18n/src/commonMain/moko-resources/de/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/de/strings.xml rename to i18n/src/commonMain/moko-resources/de/strings.xml index 7978b5a8c..25893556d 100644 --- a/i18n/src/commonMain/resources/MR/de/strings.xml +++ b/i18n/src/commonMain/moko-resources/de/strings.xml @@ -809,4 +809,5 @@ Bevorstehend Bevorstehende Aktualisierungen ansehen Leitfaden für Bevorstehendes + Dauerhafter Ordnerzugriff konnte nicht erlangt werden. Die App kann sich unerwartet verhalten. \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/el/plurals.xml b/i18n/src/commonMain/moko-resources/el/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/el/plurals.xml rename to i18n/src/commonMain/moko-resources/el/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/el/strings.xml b/i18n/src/commonMain/moko-resources/el/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/el/strings.xml rename to i18n/src/commonMain/moko-resources/el/strings.xml index 00fb56e96..54691d40c 100644 --- a/i18n/src/commonMain/resources/MR/el/strings.xml +++ b/i18n/src/commonMain/moko-resources/el/strings.xml @@ -797,4 +797,16 @@ Ανάκληση αξιόπιστων άγνωστων επεκτάσεων Αποθετήριο ανοικτού κώδικα Απενεργοποίηση σμίκρυνσης εικόνας + Προσθέστε ούτως ή άλλως + Το αποθετήριο %1$s έχει το ίδιο δακτυλικό αποτύπωμα κλειδιού υπογραφής με το %2$s. +\nΕάν αυτό είναι αναμενόμενο, το %2$s θα αντικατασταθεί, διαφορετικά επικοινωνήστε με τον συντηρητή του αποθετηρίου σας. + Ανερχόμενο + Μεταφορά υπάρχουσας καταχώρησης + Προσαρμοσμένο προφίλ εμφάνισης + Δείτε τις επερχόμενες ενημερώσεις + Αντικατάσταση + Το δακτυλικό αποτύπωμα κλειδιού υπογραφής υπάρχει ήδη + Οδηγός ανερχόμενων + Επόμενο μήνα + Προηγούμενο Μήνα \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/eo/plurals.xml b/i18n/src/commonMain/moko-resources/eo/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/eo/plurals.xml rename to i18n/src/commonMain/moko-resources/eo/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/eo/strings.xml b/i18n/src/commonMain/moko-resources/eo/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/eo/strings.xml rename to i18n/src/commonMain/moko-resources/eo/strings.xml diff --git a/i18n/src/commonMain/resources/MR/es/plurals.xml b/i18n/src/commonMain/moko-resources/es/plurals.xml similarity index 92% rename from i18n/src/commonMain/resources/MR/es/plurals.xml rename to i18n/src/commonMain/moko-resources/es/plurals.xml index 08f01feea..21cff773b 100644 --- a/i18n/src/commonMain/resources/MR/es/plurals.xml +++ b/i18n/src/commonMain/moko-resources/es/plurals.xml @@ -1,7 +1,7 @@ - Tras %1$s minuto + Tras %1$s minutos Tras %1$s minutos Tras %1$s minutos @@ -26,9 +26,9 @@ %d actualizaciones de extensiones disponibles - Queda %1$s - Quedan %1$s - Quedan %1$s + Restante %1$s + Restantes %1$s + Restantes %1$s %d categoría @@ -61,8 +61,8 @@ Hace %1$d días - El siguiente capítulo sin leer - Los siguientes %d capítulos sin leer + Siguiente capítulo sin leer + Siguientes %d capítulos sin leer Los siguientes %d capítulos sin leer diff --git a/i18n/src/commonMain/resources/MR/es/strings.xml b/i18n/src/commonMain/moko-resources/es/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/es/strings.xml rename to i18n/src/commonMain/moko-resources/es/strings.xml index 7c5a558c1..4e6501b9b 100644 --- a/i18n/src/commonMain/resources/MR/es/strings.xml +++ b/i18n/src/commonMain/moko-resources/es/strings.xml @@ -158,7 +158,7 @@ No se ha podido descargar el capítulo debido a un error inesperado No estás conectado a ninguna red Wi-Fi Categorías - Títulos en la biblioteca + Entradas de biblioteca Seguimiento Historial Favoritos @@ -508,8 +508,8 @@ Servicios de seguimiento mejorados Guía de seguimiento Automático - No - + Apagado + Encendido Establecer el tipo de orden para cada categoría Actividad en segundo plano Seguir @@ -679,7 +679,7 @@ Ver número de capítulos por leer en el icono de actualizaciones Se ha copiado al portapapeles Saltarse los capítulos repetidos - Están disponibles, pero las fuentes todavía no se han instalado: %s + Disponible, pero fuente no instalada: %s Ya tienes un elemento en la biblioteca con este mismo nombre. \n \n¿Seguro que quieres continuar? @@ -710,7 +710,7 @@ ¿Quieres desvincular %s? Esto eliminará el seguimiento localmente. Quitar también de %s - Borrar los ya descargados + Eliminar descargados Con resultados La biblioteca se ha sincronizado correctamente Sincronizando la biblioteca @@ -740,15 +740,15 @@ Almacenamiento utilizado Puntuación del rastreador Aplicar - Restablecer vista + Restablecer valor predeterminado Crear No se ha encontrado ningún equipo de traducción Equipo de traducción Excluir equipo de traducción Seleccionado - Sin seleccionar + No seleccionado Más opciones - Subir un nivel + Navegar hacia arriba Ubicación del almacenamiento Se utiliza para las copias de seguridad automáticas, poder descargar capítulos y abrir los que ya tengas en tu dispositivo. Elige una carpeta @@ -809,4 +809,5 @@ Próxima guía Próximo mes Mes anterior + No se ha podido obtener acceso a la carpeta persistente. La aplicación puede comportarse de forma inesperada. \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/eu/plurals.xml b/i18n/src/commonMain/moko-resources/eu/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/eu/plurals.xml rename to i18n/src/commonMain/moko-resources/eu/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/eu/strings.xml b/i18n/src/commonMain/moko-resources/eu/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/eu/strings.xml rename to i18n/src/commonMain/moko-resources/eu/strings.xml diff --git a/i18n/src/commonMain/resources/MR/fa/plurals.xml b/i18n/src/commonMain/moko-resources/fa/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/fa/plurals.xml rename to i18n/src/commonMain/moko-resources/fa/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/fa/strings.xml b/i18n/src/commonMain/moko-resources/fa/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/fa/strings.xml rename to i18n/src/commonMain/moko-resources/fa/strings.xml diff --git a/i18n/src/commonMain/resources/MR/fi/plurals.xml b/i18n/src/commonMain/moko-resources/fi/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/fi/plurals.xml rename to i18n/src/commonMain/moko-resources/fi/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/fi/strings.xml b/i18n/src/commonMain/moko-resources/fi/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/fi/strings.xml rename to i18n/src/commonMain/moko-resources/fi/strings.xml diff --git a/i18n/src/commonMain/resources/MR/fil/plurals.xml b/i18n/src/commonMain/moko-resources/fil/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/fil/plurals.xml rename to i18n/src/commonMain/moko-resources/fil/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/fil/strings.xml b/i18n/src/commonMain/moko-resources/fil/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/fil/strings.xml rename to i18n/src/commonMain/moko-resources/fil/strings.xml index 58c86182b..df8b126ad 100644 --- a/i18n/src/commonMain/resources/MR/fil/strings.xml +++ b/i18n/src/commonMain/moko-resources/fil/strings.xml @@ -809,4 +809,5 @@ Susunod na Buwan Nakaraang Buwan Tingnan ang mga Paparating na Update + Nabigong makakuha ng patuloy na pag-access ng folder. Ang app ay magkaroon ng di-inaasahang pagkilos. \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/fr/plurals.xml b/i18n/src/commonMain/moko-resources/fr/plurals.xml similarity index 95% rename from i18n/src/commonMain/resources/MR/fr/plurals.xml rename to i18n/src/commonMain/moko-resources/fr/plurals.xml index 21e4e5eab..8d007cf27 100644 --- a/i18n/src/commonMain/resources/MR/fr/plurals.xml +++ b/i18n/src/commonMain/moko-resources/fr/plurals.xml @@ -85,4 +85,9 @@ %d dépôts %d dépôts + + Demain + Dans %1$d jours + Dans %1$d jours + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/fr/strings.xml b/i18n/src/commonMain/moko-resources/fr/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/fr/strings.xml rename to i18n/src/commonMain/moko-resources/fr/strings.xml index 06b70876d..2764a75ef 100644 --- a/i18n/src/commonMain/resources/MR/fr/strings.xml +++ b/i18n/src/commonMain/moko-resources/fr/strings.xml @@ -796,4 +796,14 @@ Impossible de créer un fichier de sauvegarde Dernière sauvegarde automatique : %s Paramètres sources + Ajouter tout de même + Migrer l’entrée existante + À venir + Remplacer + Profil d\'affichage personnalisé + Désactiver le zoom arrière + Afficher les mises à jour à venir + Guide à venir + Le mois prochain + Le mois précédent \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/gl/plurals.xml b/i18n/src/commonMain/moko-resources/gl/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/gl/plurals.xml rename to i18n/src/commonMain/moko-resources/gl/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/gl/strings.xml b/i18n/src/commonMain/moko-resources/gl/strings.xml similarity index 94% rename from i18n/src/commonMain/resources/MR/gl/strings.xml rename to i18n/src/commonMain/moko-resources/gl/strings.xml index 3c0ee8dfe..81f69686e 100644 --- a/i18n/src/commonMain/resources/MR/gl/strings.xml +++ b/i18n/src/commonMain/moko-resources/gl/strings.xml @@ -78,9 +78,9 @@ As fontes desta extensión poden ter contido NSFW (+18) +18 Esta extensión xa non está dispoñible. Pode que non funcione ben e cause problemas na aplicación. Recoméndase desinstalala. - Unha extensión maliciosa podería ler calquera credencial de inicio de sesión gardado en Mihon ou executar código arbitrario. + Unha extensión maliciosa podería ler calquera credencial de inicio de sesión gardada en Mihon ou executar calquera tipo de código. \n -\nConfiando neste certificado aceptas estes riscos. +\nSe confías nesta extensión, aceptas estes riscos. Extensión non confiable Non confiable Desinstalar @@ -754,7 +754,7 @@ «%1$s» en lugar de «%2$s» Predicir cando sae o seguinte capítulo Preme aquí para conceder os permisos para instalar extensións. - Revogar as extensións de confianza de orixe descoñecido + Revogar as extensións de confianza de orixe descoñecida Repositorios de extensións Aínda non engadiches ningún repositorio. Dirección URL do repositorio @@ -770,4 +770,43 @@ Axustes da fonte Crear Erro completo: + Non se puido crear unha copia de seguridade + Almacenamento utilizado + Eliminar tamén de %s + Invalidouse o índice de descargas + Ten resultados + Engadir de todos modos + Migrar a entrada existente + Dispoñible: %1$s / Total: %2$s + Sincronizando a biblioteca + Nunca + Proximamente + Substituír + A impresión dixital da chave da firma xa existe + O repositorio %1$s ten a misma impresión dixital da chave da firma que %2$s. +\nSe isto é o comportamento agardado, %2$s substituirase. Se no é así, contacta con quen mantén o repositorio. + Perfil de visualización personalizado + Incluír configuracións sensibles, como as chaves de inicio de sesión en plataformas de seguemento + Navegar arriba + A biblioteca sincronizouse correctamente + Intervalos + Estimar cada + Prevese que o seguinte capítulo saia en %1$s, compróbase cada %2$s. + Frecuencia de actualización personalizada: + Queres deixar de seguir %s? + Actualizando a biblioteca… (%s) + Omitiuse porque hoxe non se esperaba ningunha publicación + Non se elixiu ningún arquivo + Última copia de seguridade automática: %s + Licenciado: sen capítulos que mostrar + Actualizar cada + Pronto + Excluír equipos de tradución + Non se atopou ningún equipo de tradución + Esto eliminará o seguimento local. + Ver as próximas actualizacións + Guía dos próximos lanzamenos + Mes seguinte + Mes anterior + Preme aquí para obter axuda con Cloudflare \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/he/plurals.xml b/i18n/src/commonMain/moko-resources/he/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/he/plurals.xml rename to i18n/src/commonMain/moko-resources/he/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/he/strings.xml b/i18n/src/commonMain/moko-resources/he/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/he/strings.xml rename to i18n/src/commonMain/moko-resources/he/strings.xml diff --git a/i18n/src/commonMain/resources/MR/hi/plurals.xml b/i18n/src/commonMain/moko-resources/hi/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/hi/plurals.xml rename to i18n/src/commonMain/moko-resources/hi/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/hi/strings.xml b/i18n/src/commonMain/moko-resources/hi/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/hi/strings.xml rename to i18n/src/commonMain/moko-resources/hi/strings.xml diff --git a/i18n/src/commonMain/resources/MR/hr/plurals.xml b/i18n/src/commonMain/moko-resources/hr/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/hr/plurals.xml rename to i18n/src/commonMain/moko-resources/hr/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/hr/strings.xml b/i18n/src/commonMain/moko-resources/hr/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/hr/strings.xml rename to i18n/src/commonMain/moko-resources/hr/strings.xml index ea77daeec..aafcf3da6 100644 --- a/i18n/src/commonMain/resources/MR/hr/strings.xml +++ b/i18n/src/commonMain/moko-resources/hr/strings.xml @@ -798,4 +798,16 @@ Dostupno: %1$s / Ukupno: %2$s Deaktiviraj smanjivanje zumiranja Prilagođeni profil prikaza + Predstojeći + Migriraj postojeći unos + Svejedno dodaj + Prethodni mjesec + Zamijeni + Sljedeći mjesec + Pogledaj nadolazeća aktualizirane verzije + Vodič za nadolazeće verzije + Digitalni otisak prsta za potpisivanje već postoji + Repozitorij %1$s ima isti digitalni otisak ključa za potpisivanje kao %2$s. +\nAko se to očekuje, %2$s će se zamijeniti, u suprotnom se obrati svom održavatelju repozitorija. + Neuspjelo dobivanje trajnog pristupa mapi. Aplikacija se može ponašati neočekivano. \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/hu/plurals.xml b/i18n/src/commonMain/moko-resources/hu/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/hu/plurals.xml rename to i18n/src/commonMain/moko-resources/hu/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/hu/strings.xml b/i18n/src/commonMain/moko-resources/hu/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/hu/strings.xml rename to i18n/src/commonMain/moko-resources/hu/strings.xml index 370d7bb77..51c00c3eb 100644 --- a/i18n/src/commonMain/resources/MR/hu/strings.xml +++ b/i18n/src/commonMain/moko-resources/hu/strings.xml @@ -69,7 +69,7 @@ Olvasatlan Szűrők eltávolítása Betűrendben - Összes fejezet + Fejezetek száma Legutóbb olvasott Globális keresés Olvasottnak jelölés @@ -431,11 +431,11 @@ Mit tartalmazzon a biztonsági mentés? Biztonsági mentés/helyreállítás nem biztos,hogy működik ha a MIUI Optimalizáció ki van kapcsolva. Helyreállítás folyamatban van - Felhasznált gyorsítótár: %1$s + Felhasznált: %1$s Adatbázis törlése Nem könyvtári elemek előzményeinek törlése %1$d nem könyvtári manga az adatbázisban - Fejezet gyorsítótár törlése kilépéskor + Fejezet gyorsítótárának törlése kilépéskor Helytelen fejezet formátum Hálózat Nem lehetett a fejezeteket letölteni. Próbálja újra a letöltések menüpontban @@ -458,7 +458,7 @@ Hibaüzenetetek törlése Sorozat beállításainak visszaállítása Néhány gyártónak extra korlátozása van arra, hogy kikapcsolja a háttér folyamatokat. Ezen a web oldalon több információt találsz, hogy hogyan oldható meg. - Olvasási előzmények megállítása + Olvasási előzmények rögzítésének szüneteltetése Fedlap Szüneteltetve Kategóriák törölve @@ -663,7 +663,7 @@ Biztos vagy benne? Kategória üres Ez eltávolítja az eddig kiválasztott befejezési dátumot a(z) %s szolgáltatásból - Olvasási idő + Olvasási időtartam Nem található elem ebben a kategóriában A/az \"%s\"-t elfogod távolítani a könyvtáradból Éppen most @@ -674,7 +674,7 @@ %s hibába ütközött. A hiba üzenetet kérünk oszd meg velünk a Discord szerverünkön. Dátum eltávolítása? Applikáció újrainditása - Globális frissités + Globális frissítésben *kötelező Fejezet csúsztatás Balra csúsztatási cselekmény @@ -721,7 +721,7 @@ Könyvtár frissítése ... (%s) Elemek Felnavigálás - Adat és tárolás + Adatok és tárolás Szeretnéd a kategóriákat betűrendbe rendezni? Átlépve, mert ma nem várható kiadás Nincs fájl kiválasztva diff --git a/i18n/src/commonMain/resources/MR/in/plurals.xml b/i18n/src/commonMain/moko-resources/in/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/in/plurals.xml rename to i18n/src/commonMain/moko-resources/in/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/in/strings.xml b/i18n/src/commonMain/moko-resources/in/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/in/strings.xml rename to i18n/src/commonMain/moko-resources/in/strings.xml index 286fe0752..fa8ba612c 100644 --- a/i18n/src/commonMain/resources/MR/in/strings.xml +++ b/i18n/src/commonMain/moko-resources/in/strings.xml @@ -797,4 +797,16 @@ Cabut izin ekstensi tidak dikenal yang tepercaya Repo sumber terbuka Menonaktifkan zoom out + Yang akan datang + Migrasikan entri yang ada + Repositori %1$s memiliki Signing Key Fingerprint yang sama dengan %2$s. +\nJika hal ini diharapkan, %2$s akan diganti, jika tidak, hubungi pengelola repo Anda. + Tambahkan saja + Profil tampilan khusus + Ganti + Signing Key Fingerprint sudah ada + Lihat Pembaruan Mendatang + Panduan Mendatang + Bulan Depan + Bulan Kemarin \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/it/plurals.xml b/i18n/src/commonMain/moko-resources/it/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/it/plurals.xml rename to i18n/src/commonMain/moko-resources/it/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/it/strings.xml b/i18n/src/commonMain/moko-resources/it/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/it/strings.xml rename to i18n/src/commonMain/moko-resources/it/strings.xml index b147c58b4..32b150fa5 100644 --- a/i18n/src/commonMain/resources/MR/it/strings.xml +++ b/i18n/src/commonMain/moko-resources/it/strings.xml @@ -806,4 +806,9 @@ \nSe è quello che si desidera, %2$s verrà sostituita, altrimenti contatta il manutentore della repository. Sostituisci Disattiva zoom indietro + Prossimamente + Controlla i Prossimi Aggiornamenti + Guida in arrivo + Mese prossimo + Mese scorso \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ja/plurals.xml b/i18n/src/commonMain/moko-resources/ja/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ja/plurals.xml rename to i18n/src/commonMain/moko-resources/ja/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/ja/strings.xml b/i18n/src/commonMain/moko-resources/ja/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/ja/strings.xml rename to i18n/src/commonMain/moko-resources/ja/strings.xml index 9add17edd..09d5268ca 100644 --- a/i18n/src/commonMain/resources/MR/ja/strings.xml +++ b/i18n/src/commonMain/moko-resources/ja/strings.xml @@ -799,7 +799,7 @@ ズームアウトを無効にする 追加 移行 - 置き換える + 交換 カスタムディスプレイプロファイル 前月 翌月 @@ -807,6 +807,6 @@ 今後のアップデート 署名キーのフィンガープリントはすでに存在します 今後のご案内 - リポジトリ %1$s は %2$s と同じ署名キーフィンガープリントを持っています。 -\nこれが予想される場合は %2$s が置き換えされますが、そうでない場合はリポジトリのメンテナーに連絡してください。 + リポジトリ %1$s は %2$s と同じ署名キー指紋を持っています。 +\nこれが予想される場合は %2$s が置換されますが、そうでない場合はリポジトリのメンテナーに連絡してください。 \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/jv/plurals.xml b/i18n/src/commonMain/moko-resources/jv/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/jv/plurals.xml rename to i18n/src/commonMain/moko-resources/jv/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/jv/strings.xml b/i18n/src/commonMain/moko-resources/jv/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/jv/strings.xml rename to i18n/src/commonMain/moko-resources/jv/strings.xml index 852bedcf8..5eadb71b9 100644 --- a/i18n/src/commonMain/resources/MR/jv/strings.xml +++ b/i18n/src/commonMain/moko-resources/jv/strings.xml @@ -307,4 +307,7 @@ Ngehapus downloadan Dipilih Gak dipilih + Pilihan liane + Data lan penyimpanan + Bakal muncul \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ka-rGE/plurals.xml b/i18n/src/commonMain/moko-resources/ka-rGE/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ka-rGE/plurals.xml rename to i18n/src/commonMain/moko-resources/ka-rGE/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/ka-rGE/strings.xml b/i18n/src/commonMain/moko-resources/ka-rGE/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ka-rGE/strings.xml rename to i18n/src/commonMain/moko-resources/ka-rGE/strings.xml diff --git a/i18n/src/commonMain/resources/MR/kk/plurals.xml b/i18n/src/commonMain/moko-resources/kk/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/kk/plurals.xml rename to i18n/src/commonMain/moko-resources/kk/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/kk/strings.xml b/i18n/src/commonMain/moko-resources/kk/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/kk/strings.xml rename to i18n/src/commonMain/moko-resources/kk/strings.xml diff --git a/i18n/src/commonMain/resources/MR/km/plurals.xml b/i18n/src/commonMain/moko-resources/km/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/km/plurals.xml rename to i18n/src/commonMain/moko-resources/km/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/km/strings.xml b/i18n/src/commonMain/moko-resources/km/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/km/strings.xml rename to i18n/src/commonMain/moko-resources/km/strings.xml diff --git a/i18n/src/commonMain/resources/MR/kn/plurals.xml b/i18n/src/commonMain/moko-resources/kn/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/kn/plurals.xml rename to i18n/src/commonMain/moko-resources/kn/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/kn/strings.xml b/i18n/src/commonMain/moko-resources/kn/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/kn/strings.xml rename to i18n/src/commonMain/moko-resources/kn/strings.xml diff --git a/i18n/src/commonMain/resources/MR/ko/plurals.xml b/i18n/src/commonMain/moko-resources/ko/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ko/plurals.xml rename to i18n/src/commonMain/moko-resources/ko/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/ko/strings.xml b/i18n/src/commonMain/moko-resources/ko/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ko/strings.xml rename to i18n/src/commonMain/moko-resources/ko/strings.xml diff --git a/i18n/src/commonMain/resources/MR/lt/plurals.xml b/i18n/src/commonMain/moko-resources/lt/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/lt/plurals.xml rename to i18n/src/commonMain/moko-resources/lt/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/lt/strings.xml b/i18n/src/commonMain/moko-resources/lt/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/lt/strings.xml rename to i18n/src/commonMain/moko-resources/lt/strings.xml diff --git a/i18n/src/commonMain/resources/MR/lv/plurals.xml b/i18n/src/commonMain/moko-resources/lv/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/lv/plurals.xml rename to i18n/src/commonMain/moko-resources/lv/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/lv/strings.xml b/i18n/src/commonMain/moko-resources/lv/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/lv/strings.xml rename to i18n/src/commonMain/moko-resources/lv/strings.xml diff --git a/i18n/src/commonMain/moko-resources/ml/plurals.xml b/i18n/src/commonMain/moko-resources/ml/plurals.xml new file mode 100644 index 000000000..589be4b61 --- /dev/null +++ b/i18n/src/commonMain/moko-resources/ml/plurals.xml @@ -0,0 +1,71 @@ + + + + ഇന്നലെ + %1$d ദിവസം മുമ്പ് + + + നാളെ + %1$d ദിവസത്തിനുള്ളിൽ + + + %2$s പിശകോടെ %1$s-നുള്ളിൽ ചെയ്തു + %2$s പിശകുകളോടെ %1$s-നുള്ളിൽ ചെയ്തു + + + %d അധ്യായം ഒഴിവാക്കുന്നു, ഒന്നുകിൽ ഉറവിടം അത് കാണുന്നില്ല അല്ലെങ്കിൽ അത് ഫിൽട്ടർ ചെയ്തിരിക്കുന്നു + %d അധ്യായങ്ങൾ ഒഴിവാക്കുന്നു, ഒന്നുകിൽ ഉറവിടം കാണുന്നില്ല അല്ലെങ്കിൽ അവ ഫിൽട്ടർ ചെയ്തിരിക്കുന്നു + + + വിപുലീകരണ അപ്ഡേറ്റ് ലഭ്യമാണ് + %d വിപുലീകരണ അപ്‌ഡേറ്റുകൾ ലഭ്യമാണ് + + + അടുത്ത അധ്യായം + അടുത്ത %d അധ്യായങ്ങൾ + + + %1$s മിനിറ്റിന് ശേഷം + %1$s മിനിറ്റിന് ശേഷം + + + %d വിഭാഗം + %d വിഭാഗങ്ങൾ + + + അടുത്ത വായിക്കാത്ത അധ്യായം + അടുത്ത %d വായിക്കാത്ത അധ്യായങ്ങൾ + + + %1$s ശേഷിക്കുന്നു + %1$s ശേഷിക്കുന്നു + + + 1 ദിവസം + %d ദിവസം + + + %1$s അധ്യായം കാണുന്നില്ല + %1$s അധ്യായങ്ങൾ കാണുന്നില്ല + + + %1$s അധ്യായം + %1$s അധ്യായങ്ങൾ + + + %d ട്രാക്കർ + %d ട്രാക്കറുകൾ + + + %1$d പുതിയ അധ്യായം + %1$d പുതിയ അധ്യായങ്ങൾ + + + അധ്യായങ്ങൾ %1$s-ഉം പിന്നെ 1-ഉം + %1$s-ഉം %2$d-ഉം അധ്യായങ്ങൾ + + + %d റിപ്പോ + %d റിപ്പോകൾ + + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ml/strings.xml b/i18n/src/commonMain/moko-resources/ml/strings.xml new file mode 100644 index 000000000..7e6ad53a5 --- /dev/null +++ b/i18n/src/commonMain/moko-resources/ml/strings.xml @@ -0,0 +1,134 @@ + + + ഡാറ്റാ ആന്റ് സ്റ്റോറേജ് + ചരിത്രം + തിരയുക + ആഗോള തിരയൽ + ബുക്ക്മാർക്ക് അധ്യായം + എല്ലാം പ്രവർത്തനക്ഷമമാക്കുക + വായിച്ചിട്ടില്ലെന്ന് അടയാളപ്പെടുത്തുക + എല്ലാം പ്രവർത്തനരഹിതമാക്കുക + തിരുത്തുക + ചേർക്കുക + വിഭാഗം അപ്ഡേറ്റ് ചെയ്യുക + ക്രമരഹിതമായ എൻട്രി തുറക്കുക + വിഭാഗം ചേർക്കുക + വിഭാഗങ്ങൾ അക്ഷരമാലാക്രമത്തിൽ അടുക്കാൻ നിങ്ങൾ ആഗ്രഹിക്കുന്നുണ്ടോ? + കവർ എഡിറ്റ് ചെയ്യുക + അധ്യായങ്ങൾ കാണുക + വിരാമം + വീണ്ടും ശ്രമിക്കുക + മുൻ അധ്യായം + അടുത്ത അധ്യായം + ഏറ്റവും പുതിയ അധ്യായം + ഓഫ് + തിരഞ്ഞെടുക്കപ്പെട്ട + മുകളിലേക്ക് നയിക്കുക + പേര് + വിഭാഗങ്ങൾ + അദ്ധ്യായങ്ങൾ + ട്രാക്കിംഗ് + ഡൗൺലോഡ് ചെയ്‌തത് ഇല്ലാതാക്കുക + ഇനിയും + ഡൗൺലോഡ് ക്യൂ + ലൈബ്രറി + അപ്ഡേറ്റുകൾ + അടുത്തതായി + ചരിത്രം + ഉൽഭവം + ബാക്കപ്പും റിസ്റ്റോറും + ലൈബ്രറി എൻട്രികൾ + സ്ഥിതിവിവരക്കണക്കുകൾ + മൈഗ്രേറ്റ് ചെയ്യുക + വിപുലീകരണങ്ങൾ + വിപുലീകരണ വിവരം + സഹായം + സ്ഥിരസ്ഥിതി + മുന്നറിയിപ്പ് + ആരംഭിച്ചു + പ്രാദേശികമായ + ഡൗൺലോഡ് ചെയ്തു + %s അൺലോക്ക് ചെയ്യുക + മാറ്റം സ്ഥിരീകരിക്കാൻ പ്രാമാണീകരിക്കുക + ക്രമീകരണങ്ങൾ + പട്ടിക + ഫിൽട്ടർ ചെയ്യുക + ഇടവേള സജ്ജമാക്കുക + ബുക്ക്‌മാർക്ക് ചെയ്തു + ട്രാക്ക് ചെയ്തു + വായിക്കാത്തത് + ഇഷ്‌ടാനുസൃതമാക്കിയ അപ്‌ഡേറ്റ് ആവൃത്തി + ഫിൽട്ടർ നീക്കം ചെയ്യുക + അക്ഷരമാലാക്രമത്തിൽ + ആകെ എൻട്രികൾ + ആകെ അധ്യായങ്ങൾ + അവസാനം വായിച്ചത് + അവസാന അപ്ഡേറ്റ് പരിശോധന + വായിക്കാത്ത എണ്ണം + അടുത്തതായി പ്രതീക്ഷിക്കുന്ന അപ്ഡേറ്റ് + അധ്യായം ലഭിച്ച തീയതി + തീയതി ചേർത്തു + ട്രാക്കർ സ്കോർ + തിരയുക… + തിരയൽ ക്രമീകരണങ്ങൾ + എല്ലാം തിരഞ്ഞെടുക്കുക + വിപരീതം തിരഞ്ഞെടുക്കുക + വായിച്ചതായി അടയാളപ്പെടുത്തുക + മുമ്പത്തേത് വായിച്ചതായി അടയാളപ്പെടുത്തുക + ഡൗൺലോഡ് + അൺബുക്ക്മാർക്ക് അധ്യായം + ഇല്ലാതാക്കുക + ലൈബ്രറി അപ്ഡേറ്റ് ചെയ്യുക + വിഭാഗങ്ങൾ എഡിറ്റ് ചെയ്യുക + വിഭാഗത്തിൻ്റെ പേരുമാറ്റുക + വിഭാഗങ്ങൾ സജ്ജമാക്കുക + \"%s\" എന്ന വിഭാഗം ഇല്ലാതാക്കാൻ നിങ്ങൾ ആഗ്രഹിക്കുന്നുണ്ടോ? + വിഭാഗം ഇല്ലാതാക്കുക + വിഭാഗങ്ങൾ അടുക്കുക + ഓൺ + കൂടുതൽ ഓപ്ഷനുകൾ + തിരഞ്ഞെടുത്തില്ല + സ്കാൻലേറ്റർ + ക്രമീകരണങ്ങൾ + നീക്കം ചെയ്യുക + എല്ലാം നീക്കം ചെയ്യുക + ആരംഭിക്കുക + പുനരാരംഭിക്കുക + ബ്രൗസറിൽ തുറക്കുക + ക്ലിപ്പ്ബോർഡിലേയ്ക്ക് പകർത്തുക + വെബ്‌വ്യൂവിൽ തുറക്കുക + മൈഗ്രേറ്റ് ചെയ്യുക + ഡിസ്പ്ലേ മോഡ് + ഏറ്റവും പഴയത് + റദ്ദാക്കുക + വായന തുടരുക ബട്ടൺ + ഭാഷ + എല്ലാം റദ്ദാക്കുക + ശരി + ഈ സീരീസിനായി എല്ലാം റദ്ദാക്കുക + മുകളിലേക്ക് നീങ്ങുക + അധ്യായ നമ്പർ പ്രകാരം + ഏറ്റവും പുതിയത് + എൻട്രി കാണിക്കുക + കോംപാക്റ്റ് ഗ്രിഡ് + സുഖപ്രദമായ ഗ്രിഡ് + ഡൌൺലോഡ് ചെയ്ത അധ്യായങ്ങൾ + ലോക്കൽ സോഴ്സ് + ഡിസേബിൾ + പിൻ ചെയ്യുക + അപ്ലൈ + സോർട് + ലിസ്റ്റ് + ചിത്രം മാത്രമുള്ള ഗ്രിഡ് + പുനഃസജ്ജമാക്കുക + പരമ്പര മുകളിലേക്ക് നീക്കുക + വിഭാഗം ടാബുകൾ കാണിക്കുക + അൺപിൻ ചെയ്യുക + അപ്‌ലോഡ് തീയതി പ്രകാരം + ആരോഹണം + അവരോഹണം + ഇൻസ്റ്റാൾ + താഴേക്ക് നീക്കുക + പങ്കിടുക + പ്രദർശിപ്പിക്കുക + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/mr/plurals.xml b/i18n/src/commonMain/moko-resources/mr/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/mr/plurals.xml rename to i18n/src/commonMain/moko-resources/mr/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/mr/strings.xml b/i18n/src/commonMain/moko-resources/mr/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/mr/strings.xml rename to i18n/src/commonMain/moko-resources/mr/strings.xml diff --git a/i18n/src/commonMain/resources/MR/ms/plurals.xml b/i18n/src/commonMain/moko-resources/ms/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ms/plurals.xml rename to i18n/src/commonMain/moko-resources/ms/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/ms/strings.xml b/i18n/src/commonMain/moko-resources/ms/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/ms/strings.xml rename to i18n/src/commonMain/moko-resources/ms/strings.xml index eb4ffe5be..539d2d449 100644 --- a/i18n/src/commonMain/resources/MR/ms/strings.xml +++ b/i18n/src/commonMain/moko-resources/ms/strings.xml @@ -796,4 +796,5 @@ Mengurangkan kesan \'ghosting\' pada skrin e-ink Sandaran automatik terakhir:%s Mengemaskini pustaka… (%s) + Ganti \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/nb-rNO/plurals.xml b/i18n/src/commonMain/moko-resources/nb-rNO/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/nb-rNO/plurals.xml rename to i18n/src/commonMain/moko-resources/nb-rNO/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/nb-rNO/strings.xml b/i18n/src/commonMain/moko-resources/nb-rNO/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/nb-rNO/strings.xml rename to i18n/src/commonMain/moko-resources/nb-rNO/strings.xml diff --git a/i18n/src/commonMain/resources/MR/ne/plurals.xml b/i18n/src/commonMain/moko-resources/ne/plurals.xml similarity index 95% rename from i18n/src/commonMain/resources/MR/ne/plurals.xml rename to i18n/src/commonMain/moko-resources/ne/plurals.xml index dfe3035be..3f499cb5c 100644 --- a/i18n/src/commonMain/resources/MR/ne/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ne/plurals.xml @@ -68,4 +68,8 @@ %d रेपो %d रेपोहरु + + भोलि + %1$d दिनमा + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ne/strings.xml b/i18n/src/commonMain/moko-resources/ne/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/ne/strings.xml rename to i18n/src/commonMain/moko-resources/ne/strings.xml index 7b110f49c..62f4f896e 100644 --- a/i18n/src/commonMain/resources/MR/ne/strings.xml +++ b/i18n/src/commonMain/moko-resources/ne/strings.xml @@ -92,7 +92,7 @@ तपाईंको पुस्तकालय खाली छ हालै केहि पढेको छैन हालैका कुनै अपडेट छैन - डाउनलोड लाम + डाउनलोड सूची कुनै डाउनलोडहरू छैन मद्दत एक्सटेन्शनको जानकारी @@ -351,7 +351,7 @@ कुनै Wi-Fi जडान उपलब्ध छैन डाउनलोडहरू रोकियो कुनै नेटवर्क जडान उपलब्ध छैन - बाह्य ट्र्याकर सेवाहरूमा अध्याय प्रगति अपडेट गर्न एक-तर्फी सिङ्क। तिनीहरूको ट्र्याकिङ बटनबाट व्यक्तिगत इन्ट्रीहरूको लागि ट्र्याकिङ सेट अप गर्नुहोस्। + बाह्य ट्र्याकर सेवाहरूमा अध्याय प्रगति अपडेट गर्न एकतर्फी सिंक। तिनीहरूको ट्र्याकिङ बटनबाट व्यक्तिगत इन्ट्रीहरूको लागि ट्र्याकिङ सेट अप गर्नुहोस्। ट्र्याकरहरूमा लगइन छैनन्: छुटेको स्रोतहरू: अध्याय क्यास खाली गर्नुहोस् @@ -540,7 +540,7 @@ डाउनलोड अघिल्लो अध्याय छैन थोरै जम्मा गर्ने ठाउँ भएको कारणले अध्याय डाउनलोड हुन सकिँदएन - सावधान: ठूलो हिस्सामा डाउनलोड गर्नाले स्रोत ढिलो चल्न अनि/वा ताचियोमीलाई अवरुद्घ गर्न सक्नेछ। थप जान्न ट्याप गर्नुहोस्। + सावधान: ठूलो हिस्सामा डाउनलोड गर्नाले स्रोत ढिलो चल्न अनि/वा Mihon लाई अवरुद्घ गर्न सक्नेछ। थप जान्न ट्याप गर्नुहोस्। ट्र्याकिङ ट्र्याक गर्नुहोस् पढ्दै @@ -616,7 +616,7 @@ %s एक अप्रत्याशित त्रुटिमा पर्यो। समर्थन को लागि हामी तपाईंलाई हाम्रो Discord को #support च्यानलमा क्र्यास लगहरू साझेदारी गर्न सुझाव दिन्छौं। GitHub मा खोल्नुहोस् डाउनलोड गरिएका अध्यायहरू पुन: जाँच गर्न एपलाई फोर्स गर्नुहोस् - एकतर्फी प्रगति सिङ्क, परिष्कृत सिङ्क + एकतर्फी प्रगति सिंक, परिष्कृत सिंक क्र्यास लग डम्प, ब्याट्री अप्टिमाइजेसन तस्वीर बचत गर्न त्रुटि भयो भर्खरै @@ -716,8 +716,8 @@ ठिक छ डाउनलोड गरिएको हटाउनुहोस् परिणामहरू भएको - पुस्तकालय सिङ्क सम्पन्न भयो - पुस्तकालय सिङ्क गर्दै + पुस्तकालय सिंक सम्पन्न भयो + पुस्तकालय सिंक गर्दै डाउनलोड इन्डेक्स अवैध भयो Cloudflare सम्बन्धित मद्दतको लागि यहाँ ट्याप गर्नुहोस् ट्र्याकर लगइन @@ -808,4 +808,10 @@ साइन गर्ने की फिंगरप्रिन्ट पहिले नै अवस्थित छ रिपो %1$s सँग %2$s जस्तै साइन गर्ने की फिंगरप्रिन्ट छ। \nयदि यो अपेक्षित छ भने, %2$s प्रतिस्थापन गरिनेछ, अन्यथा आफ्नो रिपो प्रबन्धकलाई सम्पर्क गर्नुहोस्। + आगामी + आगामी अपडेटहरू हेर्नुहोस् + आगामी गाइड + अर्को महिना + अघिल्लो महिना + लगातार फोल्डर पहुँच प्राप्त गर्न असफल। एपले अप्रत्याशित रूपमा व्यवहार गर्न सक्छ। \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/nl/plurals.xml b/i18n/src/commonMain/moko-resources/nl/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/nl/plurals.xml rename to i18n/src/commonMain/moko-resources/nl/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/nl/strings.xml b/i18n/src/commonMain/moko-resources/nl/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/nl/strings.xml rename to i18n/src/commonMain/moko-resources/nl/strings.xml diff --git a/i18n/src/commonMain/resources/MR/nn/plurals.xml b/i18n/src/commonMain/moko-resources/nn/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/nn/plurals.xml rename to i18n/src/commonMain/moko-resources/nn/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/nn/strings.xml b/i18n/src/commonMain/moko-resources/nn/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/nn/strings.xml rename to i18n/src/commonMain/moko-resources/nn/strings.xml diff --git a/i18n/src/commonMain/resources/MR/pl/plurals.xml b/i18n/src/commonMain/moko-resources/pl/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/pl/plurals.xml rename to i18n/src/commonMain/moko-resources/pl/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/pl/strings.xml b/i18n/src/commonMain/moko-resources/pl/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/pl/strings.xml rename to i18n/src/commonMain/moko-resources/pl/strings.xml index 214d732c6..8fdff8642 100644 --- a/i18n/src/commonMain/resources/MR/pl/strings.xml +++ b/i18n/src/commonMain/moko-resources/pl/strings.xml @@ -801,4 +801,11 @@ Pominięto, ponieważ nie spodziewano się dzisiaj żadnej publikacji Wyklucz skanlatorów Wyłącz oddalenie + Dodaj mimo to + Zmigruj istniejący wpis + Zamień + Pokaż nadchodzące aktualizacje + Następny miesiąc + Poprzedni miesiąc + Nadchodzący \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/pt-rBR/plurals.xml b/i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/pt-rBR/plurals.xml rename to i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml b/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/pt-rBR/strings.xml rename to i18n/src/commonMain/moko-resources/pt-rBR/strings.xml index 4a03ce56d..5a3ea0d86 100644 --- a/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml @@ -797,4 +797,16 @@ Revogar a confiabilidade de extensões desconhecidas Abrir repositório da fonte Desativar redução de zoom + Adicionar de qualquer maneira + Substituir + Próximo + Migrar entrada existente + A impressão digital da chave de assinatura já existe + O repositório %1$s tem a mesma impressão digital da chave de assinatura que %2$s. +\nSe isso for esperado, %2$s será substituído, caso contrário, entre em contato com o mantenedor do repositório. + Perfil de exibição personalizado + Exibir as próximas atualizações + Guia de próximos lançamentos + Próximo mês + Mês anterior \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/pt/plurals.xml b/i18n/src/commonMain/moko-resources/pt/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/pt/plurals.xml rename to i18n/src/commonMain/moko-resources/pt/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/pt/strings.xml b/i18n/src/commonMain/moko-resources/pt/strings.xml similarity index 86% rename from i18n/src/commonMain/resources/MR/pt/strings.xml rename to i18n/src/commonMain/moko-resources/pt/strings.xml index e447f8a33..70ddb44b2 100644 --- a/i18n/src/commonMain/resources/MR/pt/strings.xml +++ b/i18n/src/commonMain/moko-resources/pt/strings.xml @@ -717,4 +717,99 @@ Navegar para cima Desbloquear %s Dados e armazenamento + Uso de armazenamento + Disponível: %1$s / Total: %2$s + Índice de descargas invalidado + Não foi possível resolver %s + Frequência de atualização personalizada: + Remover o monitoramento do %s? + Nenhum ficheiro selecionado + Toque aqui para obter ajuda com o Cloudflare + Deletar repositório + Adicionar de qualquer maneira + Migrar entrada existente + Seja notificado para atualizações da biblioteca e mais. + A reinstalar o %s? + Datas relativas + Permissões são necessárias para instalar extensões. Toque aqui para conceder. + Não tem repositórios definidos. + Adicionar repositório + URL do repositório + Perfil de exibição personalizado + Sem conexão de internet + Substituir + A impressão digital da chave de assinatura já existe + Flash ao mudar de página + Local de armazenamento + Criar + Configurações da app + A sincronizar a biblioteca + A atualizar a biblioteca… (%s) + HTTP %d, verifique o site na WebView + Deseja ordenar as categorias alfabeticamente? + Avaliação no monitorador + Aplicar + OK + Reverter para o padrão + Bem-vindo(a)! + Pular + Selecionar uma pasta + Uma pasta deve ser selecionada + A atualizar de uma versão anterior e não tem certeza do que selecionar? Consulte o guia de armazenamento para mais informações. + Guia de armazenamento + Permissão de instalação de apps + Para instalar extensões de fontes. + Permissão de notificação + Uso de pilha em plano de fundo + Evite interrupções para tarefas longas como atualizações da biblioteca, descargas e restauração de backups. + Conceder + Novo no %s? Recomendamos dar uma olhada no guia de introdução. + Nord + \"%1$s\" ao invés de \"%2$s\" + Adiciona repositórios adicionais ao Mihon. Deve ser uma URL que termine com \"index.min.json\". + URL do repositório inválido + Deseja deletar o repositório \"%s\"? + Abrir repositório da fonte + O repositório %1$s tem a mesma impressão digital da chave de assinatura que %2$s. +\nSe isto for esperado, %2$s será substituído, caso contrário, entre em contato com o mantenedor do repositório. + Reduz o efeito fantasma em ecrãs de e-ink + Erro completo: + Configurações das fontes + Incluir configurações sensíveis (tokens de login dos monitoradores, por exemplo) + Não foi possível criar o ficheiro do backup + Licenciado - Nenhum capítulo para mostrar + Há resultados + Em breve + Nunca + Guia de próximos lançamentos + Próximo mês + Mês anterior + Também remover do %s + Vamos definir algumas coisas primeiro. Sempre pode fazer alterações nas configurações depois também. + Próximo + Começar + Escolha uma pasta onde o %1$s irá armazenar as descargas de capítulos, backups e mais. +\n +\nUma pasta dedicada é recomendada. +\n +\nPasta selecionada: %2$s + Apagar scanlators + Nenhum scanlator encontrado + Ordenar as categorias + Mover série para o final + Guia de introdução + Atualização inteligente + Revogar a confiabilidade de extensões desconhecidas + Repositórios de extensões + Este repositório já existe! + Local de armazenamento não definido + Login do monitorador + Usado para backups automáticos, descargas de capítulos e na fonte local. + Último backup automático feito em: %s + Sincronização da biblioteca finalizada + Novos capítulos com previsão de serem lançados em torno de %1$s, verificando em torno de cada %2$s. + Isto irá remover o monitoramento localmente. + Desativar redução de zoom + Exibir as próximas atualizações + A seguir \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ro/plurals.xml b/i18n/src/commonMain/moko-resources/ro/plurals.xml similarity index 95% rename from i18n/src/commonMain/resources/MR/ro/plurals.xml rename to i18n/src/commonMain/moko-resources/ro/plurals.xml index 17e794c1c..2ba7c8dbe 100644 --- a/i18n/src/commonMain/resources/MR/ro/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ro/plurals.xml @@ -85,4 +85,9 @@ %d repozitorii %d de repozitorii + + Mâine + În %1$d zile + În %1$d de zile + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ro/strings.xml b/i18n/src/commonMain/moko-resources/ro/strings.xml similarity index 97% rename from i18n/src/commonMain/resources/MR/ro/strings.xml rename to i18n/src/commonMain/moko-resources/ro/strings.xml index b4b2fdebf..d43caf5c5 100644 --- a/i18n/src/commonMain/resources/MR/ro/strings.xml +++ b/i18n/src/commonMain/moko-resources/ro/strings.xml @@ -95,11 +95,9 @@ Nesigură Dezinstalează Extensie nesigură - Extensia a fost semnată cu un certificat nesigur și nu a fost activată. + Extensiile rău intenționate pot citi orice credențiale de autentificare stocate sau pot executa cod arbitrar. \n -\nO extensie periculoasă ar putea citi orice date de logare stocate în Mihon sau executa cod periculos. -\n -\nPrin acordarea încrederii acestui certificat acceptați riscurile menționate. +\nAcordând încredere acestei extensii, acceptați aceste riscuri. Ecran complet Animați tranzițiile de pagini Capitole descărcate @@ -119,7 +117,7 @@ Mod de citire implicit De la stânga la dreapta De la dreapta la stânga - Vertical + Paginat (vertical) Benzi desenate web Setări pagini Tip de scalare @@ -376,7 +374,7 @@ Grilă confortabilă Migrează Date - Fișierul de rezerva este invalid + Fișier de rezervă invalid: Nu s-au găsit pagini Tab-uri Afișează filele categoriei @@ -460,9 +458,7 @@ În formă de L Data obținerii capitolului Urmărit - Datele din fișierul de rezervă vor fi restaurate. -\n -\nVa trebui să instalați toate extensiile lipsă și să vă conectați ulterior la serviciile de urmărire pentru a le utiliza. + Este posibil să fie necesar să instalați extensiile lipsă și să vă conectați ulterior la serviciile de urmărire pentru a le utiliza. Nu vor fi descărcate înscrierile din categoriile excluse, chiar dacă acestea se află și în categoriile incluse. Descărcare automată Dreapta @@ -797,4 +793,18 @@ Acest repozitoriu există deja! Șterge repozitoriu URL repozitoriu invalid + Viitoare + Nord + Repozitoriul %1$s are acceeași amprenta digitală a cheii de semnare ca și %2$s +\nÎn cazul în care asta este la ce vă așteptați, %2$s va fi înlocuit, altfel contatati întreținătorul repozitoriului. + Profil de afișare personalizat + Adăugați oricum + Migrați intrare existentă + Înlocuiți + Amprenta digitală a cheii de semnare există deja + Dezactivați funcția de zoom-out + Vizualizați actualizările viitoare + Ghidul viitor + Luna următoare + Luna anterioară \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ru/plurals.xml b/i18n/src/commonMain/moko-resources/ru/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/ru/plurals.xml rename to i18n/src/commonMain/moko-resources/ru/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/ru/strings.xml b/i18n/src/commonMain/moko-resources/ru/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/ru/strings.xml rename to i18n/src/commonMain/moko-resources/ru/strings.xml index bf0909033..1337da8d9 100644 --- a/i18n/src/commonMain/resources/MR/ru/strings.xml +++ b/i18n/src/commonMain/moko-resources/ru/strings.xml @@ -809,4 +809,5 @@ Предстоящее Предстоящее руководство Следующий месяц + Не удалось получить постоянный доступ к папке. Приложение может работать некорректно. \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/sa/plurals.xml b/i18n/src/commonMain/moko-resources/sa/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sa/plurals.xml rename to i18n/src/commonMain/moko-resources/sa/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/sa/strings.xml b/i18n/src/commonMain/moko-resources/sa/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sa/strings.xml rename to i18n/src/commonMain/moko-resources/sa/strings.xml diff --git a/i18n/src/commonMain/resources/MR/sah/plurals.xml b/i18n/src/commonMain/moko-resources/sah/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sah/plurals.xml rename to i18n/src/commonMain/moko-resources/sah/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/sah/strings.xml b/i18n/src/commonMain/moko-resources/sah/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sah/strings.xml rename to i18n/src/commonMain/moko-resources/sah/strings.xml diff --git a/i18n/src/commonMain/resources/MR/sc/plurals.xml b/i18n/src/commonMain/moko-resources/sc/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sc/plurals.xml rename to i18n/src/commonMain/moko-resources/sc/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/sc/strings.xml b/i18n/src/commonMain/moko-resources/sc/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sc/strings.xml rename to i18n/src/commonMain/moko-resources/sc/strings.xml diff --git a/i18n/src/commonMain/resources/MR/sdh/plurals.xml b/i18n/src/commonMain/moko-resources/sdh/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sdh/plurals.xml rename to i18n/src/commonMain/moko-resources/sdh/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/sdh/strings.xml b/i18n/src/commonMain/moko-resources/sdh/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sdh/strings.xml rename to i18n/src/commonMain/moko-resources/sdh/strings.xml diff --git a/i18n/src/commonMain/resources/MR/sk/plurals.xml b/i18n/src/commonMain/moko-resources/sk/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sk/plurals.xml rename to i18n/src/commonMain/moko-resources/sk/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/sk/strings.xml b/i18n/src/commonMain/moko-resources/sk/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sk/strings.xml rename to i18n/src/commonMain/moko-resources/sk/strings.xml diff --git a/i18n/src/commonMain/resources/MR/sq/plurals.xml b/i18n/src/commonMain/moko-resources/sq/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sq/plurals.xml rename to i18n/src/commonMain/moko-resources/sq/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/sq/strings.xml b/i18n/src/commonMain/moko-resources/sq/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sq/strings.xml rename to i18n/src/commonMain/moko-resources/sq/strings.xml diff --git a/i18n/src/commonMain/resources/MR/sr/plurals.xml b/i18n/src/commonMain/moko-resources/sr/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/sr/plurals.xml rename to i18n/src/commonMain/moko-resources/sr/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/sr/strings.xml b/i18n/src/commonMain/moko-resources/sr/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/sr/strings.xml rename to i18n/src/commonMain/moko-resources/sr/strings.xml index 36eb985a0..aab9d56c0 100644 --- a/i18n/src/commonMain/resources/MR/sr/strings.xml +++ b/i18n/src/commonMain/moko-resources/sr/strings.xml @@ -801,4 +801,9 @@ Ни једна датотека није одабрана Примени Врати на подразумевано + Dodati svejedno + Predstojeći + Migrirajte postojeći unos + Zameniti + Otisak prsta za otključavanje već postoji \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/sv/plurals.xml b/i18n/src/commonMain/moko-resources/sv/plurals.xml similarity index 95% rename from i18n/src/commonMain/resources/MR/sv/plurals.xml rename to i18n/src/commonMain/moko-resources/sv/plurals.xml index 976b04004..83e463a38 100644 --- a/i18n/src/commonMain/resources/MR/sv/plurals.xml +++ b/i18n/src/commonMain/moko-resources/sv/plurals.xml @@ -68,4 +68,8 @@ %d förråd %d flera förråd + + Imorgon + Om %1$d dagar + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/sv/strings.xml b/i18n/src/commonMain/moko-resources/sv/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/sv/strings.xml rename to i18n/src/commonMain/moko-resources/sv/strings.xml index 5231c7374..bce82c14c 100644 --- a/i18n/src/commonMain/resources/MR/sv/strings.xml +++ b/i18n/src/commonMain/moko-resources/sv/strings.xml @@ -796,4 +796,18 @@ Öppenkällkods förråd Snart Anpassad uppdateringsfrekvens: + Inaktivera utzoomning + Lägg till ändå + Ersätt + Anpassad skärmprofil + Kommande + Föregående månad + Nästa månad + Migrera existerande post + Signeringsnyckel-fingeravtryck finns redan + Förrådet %1$s har samma Signering av nyckelfingeravtryck som %2$s. +\nOm detta förväntas kommer %2$s att ersättas, annars kontakta den förråds-ansvarige. + Visa kommande uppdateringar + Kommande guide + Misslyckades med att skaffa beständig mappåtkomst. Appen kan bete sig oväntat. \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/te/plurals.xml b/i18n/src/commonMain/moko-resources/te/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/te/plurals.xml rename to i18n/src/commonMain/moko-resources/te/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/te/strings.xml b/i18n/src/commonMain/moko-resources/te/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/te/strings.xml rename to i18n/src/commonMain/moko-resources/te/strings.xml diff --git a/i18n/src/commonMain/resources/MR/th/plurals.xml b/i18n/src/commonMain/moko-resources/th/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/th/plurals.xml rename to i18n/src/commonMain/moko-resources/th/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/th/strings.xml b/i18n/src/commonMain/moko-resources/th/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/th/strings.xml rename to i18n/src/commonMain/moko-resources/th/strings.xml diff --git a/i18n/src/commonMain/resources/MR/tr/plurals.xml b/i18n/src/commonMain/moko-resources/tr/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/tr/plurals.xml rename to i18n/src/commonMain/moko-resources/tr/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/tr/strings.xml b/i18n/src/commonMain/moko-resources/tr/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/tr/strings.xml rename to i18n/src/commonMain/moko-resources/tr/strings.xml diff --git a/i18n/src/commonMain/resources/MR/uk/plurals.xml b/i18n/src/commonMain/moko-resources/uk/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/uk/plurals.xml rename to i18n/src/commonMain/moko-resources/uk/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/uk/strings.xml b/i18n/src/commonMain/moko-resources/uk/strings.xml similarity index 98% rename from i18n/src/commonMain/resources/MR/uk/strings.xml rename to i18n/src/commonMain/moko-resources/uk/strings.xml index d03c85884..276fd442d 100644 --- a/i18n/src/commonMain/resources/MR/uk/strings.xml +++ b/i18n/src/commonMain/moko-resources/uk/strings.xml @@ -78,7 +78,7 @@ Кожні 6 годин Кожні 12 годин Щодня - Кожні 2 дні + Що 2 дні Щонеділі Всі Обмеження пристрою для автоматичних оновлень @@ -796,4 +796,16 @@ Почнемо Давайте для початку налаштуємо дещо. Ви завжди зможете змінити ці налаштування потім. Гайд новачка + Додати у будь-якому разі + Відбиток пальця для підпису ключа вже існує + Репозиторій %1$s має такий самий відбиток ключа підпису, як і %2$s. +\nЯкщо це очікувано, %2$s буде замінено, в іншому випадку зверніться до вашого обслуговуючого репозиторію. + Переглянути майбутні оновлення + Попередній місяць + Перенести існуючий запис + Замінити + Власний профіль відображення + Вимкнути зменшення масштабу + Незабаром + Наступний місяць \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/uz/plurals.xml b/i18n/src/commonMain/moko-resources/uz/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/uz/plurals.xml rename to i18n/src/commonMain/moko-resources/uz/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/uz/strings.xml b/i18n/src/commonMain/moko-resources/uz/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/uz/strings.xml rename to i18n/src/commonMain/moko-resources/uz/strings.xml diff --git a/i18n/src/commonMain/resources/MR/vi/plurals.xml b/i18n/src/commonMain/moko-resources/vi/plurals.xml similarity index 90% rename from i18n/src/commonMain/resources/MR/vi/plurals.xml rename to i18n/src/commonMain/moko-resources/vi/plurals.xml index df9978d19..013eada7a 100644 --- a/i18n/src/commonMain/resources/MR/vi/plurals.xml +++ b/i18n/src/commonMain/moko-resources/vi/plurals.xml @@ -48,4 +48,10 @@ %d ngày + + %d kho + + + Trong %1$d ngày + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/vi/strings.xml b/i18n/src/commonMain/moko-resources/vi/strings.xml similarity index 95% rename from i18n/src/commonMain/resources/MR/vi/strings.xml rename to i18n/src/commonMain/moko-resources/vi/strings.xml index ca4675d6e..31a3761cc 100644 --- a/i18n/src/commonMain/resources/MR/vi/strings.xml +++ b/i18n/src/commonMain/moko-resources/vi/strings.xml @@ -781,4 +781,34 @@ Mã lỗi đầy đủ: Cần phải cấp phép để cài đặt tiện ích mở rộng. Bấm đây để cấp. Bao gồm những cài đặt nhạy cảm (ví dụ như token đăng nhập tracker) + Luôn luôn thêm + Phương bắc + Cập nhật thông minh + Sắp tới + Chuyển vào các mục hiện có + Cập nhật từ phiên bản cũ hơn và không biết nên chọn gì? Tham khảo hướng dẫn để biết thêm thông tin. + Thêm nguồn bổ sung vào Mihon. Địa chỉ URL này cần phải kết thúc với \"index.min.json\". + Kho lưu trữ %1$s có Dấu vân tay Khóa ký giống hệt như %2$s. +\nNếu điều này là dự kiến, %2$s sẽ bị thay thế. Nếu không, vui lòng liên hệ với người quản lý kho lưu trữ của bạn. + Bạn chưa có nguồn nào. + Thu hồi quyền truy cập của tiện ích không xác định + Thêm nguồn + Nguồn đã có rồi! + Xóa nguồn + Hướng dẫn + Quản lý nguồn + URL nguồn không hợp lệ + URL nguồn + Bạn có muốn xóa nguồn \"%s\"? + Mở nguồn + Sớm + Tần suất cập nhật tùy chỉnh: + Tùy chỉnh hồ sơ màn hình + Tắt thu nhỏ + Xem các cập nhật sắp tới + Hướng dẫn sắp tới + Tháng tiếp theo + Tháng trước + Thay thế + Dấu vân tay cho khóa ký đã được tạo trước đó. \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml b/i18n/src/commonMain/moko-resources/zh-rCN/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml rename to i18n/src/commonMain/moko-resources/zh-rCN/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml b/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/zh-rCN/strings.xml rename to i18n/src/commonMain/moko-resources/zh-rCN/strings.xml diff --git a/i18n/src/commonMain/resources/MR/zh-rTW/plurals.xml b/i18n/src/commonMain/moko-resources/zh-rTW/plurals.xml similarity index 100% rename from i18n/src/commonMain/resources/MR/zh-rTW/plurals.xml rename to i18n/src/commonMain/moko-resources/zh-rTW/plurals.xml diff --git a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml b/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml similarity index 99% rename from i18n/src/commonMain/resources/MR/zh-rTW/strings.xml rename to i18n/src/commonMain/moko-resources/zh-rTW/strings.xml index d2ec23ffe..e4dfbc1f6 100644 --- a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -809,4 +809,5 @@ 上個月 預告 檢視新刊預告 + 無法取得永久性資料夾存取權,應用程式可能會表現異常。 \ No newline at end of file diff --git a/presentation-core/build.gradle.kts b/presentation-core/build.gradle.kts index c7b64ad42..c6e5c0b65 100644 --- a/presentation-core/build.gradle.kts +++ b/presentation-core/build.gradle.kts @@ -21,7 +21,6 @@ dependencies { implementation(compose.activity) implementation(compose.foundation) implementation(compose.material3.core) - implementation(compose.material.core) implementation(compose.material.icons) implementation(compose.animation) implementation(compose.animation.graphics) @@ -35,7 +34,7 @@ dependencies { tasks { // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) withType { - kotlinOptions.freeCompilerArgs += listOf( + compilerOptions.freeCompilerArgs.addAll( "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.material.ExperimentalMaterialApi", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", 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 568332ca7..bb9ed8e85 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 @@ -10,7 +10,6 @@ import androidx.compose.foundation.gestures.DraggableAnchors import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.gestures.animateTo -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding @@ -39,7 +38,6 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp @@ -54,7 +52,6 @@ private val sheetAnimationSpec = tween(durationMillis = 350) @Composable fun AdaptiveSheet( isTabletUi: Boolean, - tonalElevation: Dp, enableSwipeDismiss: Boolean, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, @@ -78,8 +75,7 @@ fun AdaptiveSheet( Box( modifier = Modifier .clickable( - enabled = true, - interactionSource = remember { MutableInteractionSource() }, + interactionSource = null, indication = null, onClick = internalOnDismissRequest, ) @@ -91,7 +87,7 @@ fun AdaptiveSheet( modifier = Modifier .requiredWidthIn(max = 460.dp) .clickable( - interactionSource = remember { MutableInteractionSource() }, + interactionSource = null, indication = null, onClick = {}, ) @@ -99,7 +95,7 @@ fun AdaptiveSheet( .padding(vertical = 16.dp) .then(modifier), shape = MaterialTheme.shapes.extraLarge, - tonalElevation = tonalElevation, + color = MaterialTheme.colorScheme.surfaceContainerHigh, content = { BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest) content() @@ -122,14 +118,14 @@ fun AdaptiveSheet( ) } val internalOnDismissRequest = { - if (anchoredDraggableState.currentValue == 0) { + if (anchoredDraggableState.settledValue == 0) { scope.launch { anchoredDraggableState.animateTo(1) } } } Box( modifier = Modifier .clickable( - interactionSource = remember { MutableInteractionSource() }, + interactionSource = null, indication = null, onClick = internalOnDismissRequest, ) @@ -147,7 +143,7 @@ fun AdaptiveSheet( modifier = Modifier .widthIn(max = 460.dp) .clickable( - interactionSource = remember { MutableInteractionSource() }, + interactionSource = null, indication = null, onClick = {}, ) @@ -155,7 +151,9 @@ fun AdaptiveSheet( if (enableSwipeDismiss) { Modifier.nestedScroll( remember(anchoredDraggableState) { - anchoredDraggableState.preUpPostDownNestedScrollConnection() + anchoredDraggableState.preUpPostDownNestedScrollConnection( + onFling = { scope.launch { anchoredDraggableState.settle(it) } } + ) }, ) } else { @@ -180,7 +178,7 @@ fun AdaptiveSheet( .navigationBarsPadding() .statusBarsPadding(), shape = MaterialTheme.shapes.extraLarge, - tonalElevation = tonalElevation, + color = MaterialTheme.colorScheme.surfaceContainerHigh, content = { BackHandler( enabled = anchoredDraggableState.targetValue == 0, @@ -192,7 +190,7 @@ fun AdaptiveSheet( LaunchedEffect(anchoredDraggableState) { scope.launch { anchoredDraggableState.animateTo(0) } - snapshotFlow { anchoredDraggableState.currentValue } + snapshotFlow { anchoredDraggableState.settledValue } .drop(1) .filter { it == 1 } .collectLatest { @@ -203,55 +201,51 @@ fun AdaptiveSheet( } } -private fun AnchoredDraggableState.preUpPostDownNestedScrollConnection() = - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.Drag) { - 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.Drag) { - 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()) { - settle(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 { - val toFling = available.toFloat() - return if (toFling > 0) { - settle(toFling) - available - } else { - Velocity.Zero - } - } - - private fun Float.toOffset(): Offset = Offset(0f, this) - - @JvmName("velocityToFloat") - private fun Velocity.toFloat() = this.y - - @JvmName("offsetToFloat") - private fun Offset.toFloat(): Float = this.y } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + return 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 onPostFling(consumed: Velocity, available: Velocity): Velocity { + onFling(available.toFloat()) + return available + } + + private fun Float.toOffset(): Offset = Offset(0f, this) + + @JvmName("velocityToFloat") + private fun Velocity.toFloat() = this.y + + @JvmName("offsetToFloat") + private fun Offset.toFloat(): Float = this.y +} 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 4389fb482..1d8cf2d5f 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 @@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp @@ -18,9 +17,8 @@ import androidx.compose.ui.unit.dp fun Pill( text: String, modifier: Modifier = Modifier, - color: Color = MaterialTheme.colorScheme.background, - contentColor: Color = MaterialTheme.colorScheme.onBackground, - elevation: Dp = 1.dp, + color: Color = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor: Color = MaterialTheme.colorScheme.onSurface, fontSize: TextUnit = LocalTextStyle.current.fontSize, ) { Surface( @@ -29,7 +27,6 @@ fun Pill( shape = MaterialTheme.shapes.extraLarge, color = color, contentColor = contentColor, - tonalElevation = elevation, ) { Box( modifier = Modifier 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 abdf61c9f..4fd4b7571 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 @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material.ContentAlpha import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material.icons.filled.ArrowUpward @@ -56,6 +55,8 @@ object SettingsItemsPaddings { val Vertical = 10.dp } +private const val DisabledContentAlpha = 0.38f + @Composable fun HeadingItem(labelRes: StringResource) { HeadingItem(stringResource(labelRes)) @@ -278,7 +279,7 @@ fun TriStateItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.large), ) { - val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled + val stateAlpha = if (enabled && onClick != null) 1f else DisabledContentAlpha Icon( imageVector = when (state) { @@ -291,7 +292,7 @@ fun TriStateItem( MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha) } else { when (onClick) { - null -> MaterialTheme.colorScheme.onSurface.copy(alpha = ContentAlpha.disabled) + null -> MaterialTheme.colorScheme.onSurface.copy(alpha = DisabledContentAlpha) else -> MaterialTheme.colorScheme.primary } }, 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 6a645ed6a..5e65c89a2 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 @@ -100,7 +100,6 @@ fun Button( val containerColor = colors.containerColor(enabled).value val contentColor = colors.contentColor(enabled).value val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp - val tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp Surface( onClick = onClick, @@ -109,7 +108,6 @@ fun Button( shape = shape, color = containerColor, contentColor = contentColor, - tonalElevation = tonalElevation, shadowElevation = shadowElevation, border = border, interactionSource = interactionSource, 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 9642cec71..08b10cdaa 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 @@ -1,34 +1,16 @@ package tachiyomi.presentation.core.components.material -import androidx.compose.animation.core.animate import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.pulltorefresh.PullToRefreshContainer -import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp -import kotlin.math.abs -import kotlin.math.pow /** * @param refreshing Whether the layout is currently refreshing @@ -40,248 +22,32 @@ import kotlin.math.pow @Composable fun PullRefresh( refreshing: Boolean, - enabled: () -> Boolean, + enabled: Boolean, onRefresh: () -> Unit, modifier: Modifier = Modifier, indicatorPadding: PaddingValues = PaddingValues(0.dp), content: @Composable () -> Unit, ) { - val state = rememberPullToRefreshState( - isRefreshing = refreshing, - extraVerticalOffset = indicatorPadding.calculateTopPadding(), - enabled = enabled, - onRefresh = onRefresh, - ) - - Box(modifier.nestedScroll(state.nestedScrollConnection)) { + val state = rememberPullToRefreshState() + Box( + modifier = modifier + .pullToRefresh( + isRefreshing = refreshing, + state = state, + enabled = enabled, + onRefresh = onRefresh, + ), + ) { content() - val contentPadding = remember(indicatorPadding) { - object : PaddingValues { - override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = - indicatorPadding.calculateLeftPadding(layoutDirection) - - override fun calculateTopPadding(): Dp = 0.dp - - override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = - indicatorPadding.calculateRightPadding(layoutDirection) - - override fun calculateBottomPadding(): Dp = - indicatorPadding.calculateBottomPadding() - } - } - PullToRefreshContainer( - state = state, + PullToRefreshDefaults.Indicator( modifier = Modifier .align(Alignment.TopCenter) - .padding(contentPadding), + .padding(indicatorPadding), + isRefreshing = refreshing, + state = state, containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } - -@Composable -private fun rememberPullToRefreshState( - isRefreshing: Boolean, - extraVerticalOffset: Dp, - positionalThreshold: Dp = 64.dp, - enabled: () -> Boolean = { true }, - onRefresh: () -> Unit, -): PullToRefreshStateImpl { - val density = LocalDensity.current - val extraVerticalOffsetPx = with(density) { extraVerticalOffset.toPx() } - val positionalThresholdPx = with(density) { positionalThreshold.toPx() } - return rememberSaveable( - extraVerticalOffset, - positionalThresholdPx, - enabled, - onRefresh, - saver = PullToRefreshStateImpl.Saver( - extraVerticalOffset = extraVerticalOffsetPx, - positionalThreshold = positionalThresholdPx, - enabled = enabled, - onRefresh = onRefresh, - ), - ) { - PullToRefreshStateImpl( - initialRefreshing = isRefreshing, - extraVerticalOffset = extraVerticalOffsetPx, - positionalThreshold = positionalThresholdPx, - enabled = enabled, - onRefresh = onRefresh, - ) - }.also { - LaunchedEffect(isRefreshing) { - if (isRefreshing && !it.isRefreshing) { - it.startRefreshAnimated() - } else if (!isRefreshing && it.isRefreshing) { - it.endRefreshAnimated() - } - } - } -} - -/** - * Creates a [PullToRefreshState]. - * - * @param positionalThreshold The positional threshold, in pixels, in which a refresh is triggered - * @param extraVerticalOffset Extra vertical offset, in pixels, for the "refreshing" state - * @param initialRefreshing The initial refreshing value of [PullToRefreshState] - * @param enabled a callback used to determine whether scroll events are to be handled by this - * @param onRefresh a callback to run when pull-to-refresh action is triggered by user - * [PullToRefreshState] - */ -private class PullToRefreshStateImpl( - initialRefreshing: Boolean, - private val extraVerticalOffset: Float, - override val positionalThreshold: Float, - enabled: () -> Boolean, - private val onRefresh: () -> Unit, -) : PullToRefreshState { - - override val progress get() = adjustedDistancePulled / positionalThreshold - override var verticalOffset by mutableFloatStateOf(if (initialRefreshing) refreshingVerticalOffset else 0f) - - override var isRefreshing by mutableStateOf(initialRefreshing) - - private val refreshingVerticalOffset: Float - get() = positionalThreshold + extraVerticalOffset - - override fun startRefresh() { - isRefreshing = true - verticalOffset = refreshingVerticalOffset - } - - suspend fun startRefreshAnimated() { - isRefreshing = true - animateTo(refreshingVerticalOffset) - } - - override fun endRefresh() { - verticalOffset = 0f - isRefreshing = false - } - - suspend fun endRefreshAnimated() { - animateTo(0f) - isRefreshing = false - } - - override var nestedScrollConnection = object : NestedScrollConnection { - override fun onPreScroll( - available: Offset, - source: NestedScrollSource, - ): Offset = when { - !enabled() -> Offset.Zero - // Swiping up - source == NestedScrollSource.Drag && available.y < 0 -> { - consumeAvailableOffset(available) - } - else -> Offset.Zero - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset = when { - !enabled() -> Offset.Zero - // Swiping down - source == NestedScrollSource.Drag && available.y > 0 -> { - consumeAvailableOffset(available) - } - else -> Offset.Zero - } - - override suspend fun onPreFling(available: Velocity): Velocity { - return Velocity(0f, onRelease(available.y)) - } - } - - /** Helper method for nested scroll connection */ - fun consumeAvailableOffset(available: Offset): Offset { - val y = if (isRefreshing) { - 0f - } else { - val newOffset = (distancePulled + available.y).coerceAtLeast(0f) - val dragConsumed = newOffset - distancePulled - distancePulled = newOffset - verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress.coerceIn(0f, 1f)) - dragConsumed - } - return Offset(0f, y) - } - - /** Helper method for nested scroll connection. Calls onRefresh callback when triggered */ - suspend fun onRelease(velocity: Float): Float { - if (isRefreshing) return 0f // Already refreshing, do nothing - // Trigger refresh - if (adjustedDistancePulled > positionalThreshold) { - onRefresh() - startRefreshAnimated() - } else { - animateTo(0f) - } - - val consumed = when { - // We are flinging without having dragged the pull refresh (for example a fling inside - // a list) - don't consume - distancePulled == 0f -> 0f - // If the velocity is negative, the fling is upwards, and we don't want to prevent the - // the list from scrolling - velocity < 0f -> 0f - // We are showing the indicator, and the fling is downwards - consume everything - else -> velocity - } - distancePulled = 0f - return consumed - } - - suspend fun animateTo(offset: Float) { - animate(initialValue = verticalOffset, targetValue = offset) { value, _ -> - verticalOffset = value - } - } - - /** Provides custom vertical offset behavior for [PullToRefreshContainer] */ - fun calculateVerticalOffset(): Float = when { - // If drag hasn't gone past the threshold, the position is the adjustedDistancePulled. - adjustedDistancePulled <= positionalThreshold -> adjustedDistancePulled - else -> { - // How far beyond the threshold pull has gone, as a percentage of the threshold. - val overshootPercent = abs(progress) - 1.0f - // Limit the overshoot to 200%. Linear between 0 and 200. - val linearTension = overshootPercent.coerceIn(0f, 2f) - // Non-linear tension. Increases with linearTension, but at a decreasing rate. - val tensionPercent = linearTension - linearTension.pow(2) / 4 - // The additional offset beyond the threshold. - val extraOffset = positionalThreshold * tensionPercent - positionalThreshold + extraOffset - } - } - - companion object { - /** The default [Saver] for [PullToRefreshStateImpl]. */ - fun Saver( - extraVerticalOffset: Float, - positionalThreshold: Float, - enabled: () -> Boolean, - onRefresh: () -> Unit, - ) = Saver( - save = { it.isRefreshing }, - restore = { isRefreshing -> - PullToRefreshStateImpl( - initialRefreshing = isRefreshing, - extraVerticalOffset = extraVerticalOffset, - positionalThreshold = positionalThreshold, - enabled = enabled, - onRefresh = onRefresh, - ) - }, - ) - } - - private var distancePulled by mutableFloatStateOf(0f) - private val adjustedDistancePulled: Float get() = distancePulled * 0.5f -} 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 e472e5127..0e857ef75 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 @@ -6,13 +6,13 @@ import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box -import androidx.compose.material.ripple import androidx.compose.material3.ColorScheme import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.contentColorFor import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.NonRestartableComposable 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 1abb3205a..cdabf1b49 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 @@ -3,63 +3,16 @@ package tachiyomi.presentation.core.util import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue @Composable -fun LazyListState.isScrolledToStart(): Boolean { +fun LazyListState.shouldExpandFAB(): Boolean { return remember { derivedStateOf { - val firstItem = layoutInfo.visibleItemsInfo.firstOrNull() - firstItem == null || firstItem.offset == layoutInfo.viewportStartOffset + (firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0) || + lastScrolledBackward || + !canScrollForward } - }.value -} - -@Composable -fun LazyListState.isScrolledToEnd(): Boolean { - return remember { - derivedStateOf { - val lastItem = layoutInfo.visibleItemsInfo.lastOrNull() - lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset - } - }.value -} - -@Composable -fun LazyListState.isScrollingUp(): Boolean { - var previousIndex by remember { mutableIntStateOf(firstVisibleItemIndex) } - var previousScrollOffset by remember { mutableIntStateOf(firstVisibleItemScrollOffset) } - return remember { - derivedStateOf { - if (previousIndex != firstVisibleItemIndex) { - previousIndex > firstVisibleItemIndex - } else { - previousScrollOffset >= firstVisibleItemScrollOffset - }.also { - previousIndex = firstVisibleItemIndex - previousScrollOffset = firstVisibleItemScrollOffset - } - } - }.value -} - -@Composable -fun LazyListState.isScrollingDown(): Boolean { - var previousIndex by remember { mutableIntStateOf(firstVisibleItemIndex) } - var previousScrollOffset by remember { mutableIntStateOf(firstVisibleItemScrollOffset) } - return remember { - derivedStateOf { - if (previousIndex != firstVisibleItemIndex) { - previousIndex < firstVisibleItemIndex - } else { - previousScrollOffset <= firstVisibleItemScrollOffset - }.also { - previousIndex = firstVisibleItemIndex - previousScrollOffset = firstVisibleItemScrollOffset - } - } - }.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 411fc9983..875cd4583 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 @@ -1,7 +1,6 @@ package tachiyomi.presentation.core.util import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.isImeVisible @@ -42,14 +41,12 @@ fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(SecondaryItemAlpha) fun Modifier.clickableNoIndication( onLongClick: (() -> Unit)? = null, onClick: () -> Unit, -): Modifier = composed { - Modifier.combinedClickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onLongClick = onLongClick, - onClick = onClick, - ) -} +) = this.combinedClickable( + interactionSource = null, + indication = null, + onLongClick = onLongClick, + onClick = onClick, +) /** * For TextField, the provided [action] will be invoked when diff --git a/presentation-core/src/main/res/values-night/colors_lavender.xml b/presentation-core/src/main/res/values-night/colors_lavender.xml index d76e55486..f9d558025 100644 --- a/presentation-core/src/main/res/values-night/colors_lavender.xml +++ b/presentation-core/src/main/res/values-night/colors_lavender.xml @@ -17,7 +17,7 @@ #111129 #A177FF #111129 - #A177FF + #423271 #111129 #5E25E1 #E8E8E8 diff --git a/presentation-core/src/main/res/values-night/colors_midnightdusk.xml b/presentation-core/src/main/res/values-night/colors_midnightdusk.xml index d107312aa..f10e84662 100644 --- a/presentation-core/src/main/res/values-night/colors_midnightdusk.xml +++ b/presentation-core/src/main/res/values-night/colors_midnightdusk.xml @@ -17,7 +17,7 @@ #FFFFFF #F02475 #FFFFFF - #F02475 + #66183C #FFFFFF #55971C #FFFFFF diff --git a/presentation-core/src/main/res/values-night/colors_nord.xml b/presentation-core/src/main/res/values-night/colors_nord.xml index f9ab8ce50..058d52a4a 100644 --- a/presentation-core/src/main/res/values-night/colors_nord.xml +++ b/presentation-core/src/main/res/values-night/colors_nord.xml @@ -7,7 +7,7 @@ #2E3440 #81A1C1 #2E3440 - #81A1C1 + #506275 #2E3440 #5E81AC #000000 diff --git a/presentation-core/src/main/res/values-night/colors_tako.xml b/presentation-core/src/main/res/values-night/colors_tako.xml index 907486c60..63c2db1ce 100644 --- a/presentation-core/src/main/res/values-night/colors_tako.xml +++ b/presentation-core/src/main/res/values-night/colors_tako.xml @@ -16,7 +16,7 @@ #F3B375 #38294E #F3B375 - #38294E + #5C4D4B #F3B375 #38294E #66577E diff --git a/presentation-core/src/main/res/values/colors_lavender.xml b/presentation-core/src/main/res/values/colors_lavender.xml index f6cfda65c..02d6274e3 100644 --- a/presentation-core/src/main/res/values/colors_lavender.xml +++ b/presentation-core/src/main/res/values/colors_lavender.xml @@ -16,7 +16,7 @@ #EDE2FF #7B46AF #EDE2FF - #7B46AF + #C9B0E6 #EDE2FF #EDE2FF #7B46AF diff --git a/presentation-core/src/main/res/values/colors_nord.xml b/presentation-core/src/main/res/values/colors_nord.xml index 7aab3eb43..b04ae3eec 100644 --- a/presentation-core/src/main/res/values/colors_nord.xml +++ b/presentation-core/src/main/res/values/colors_nord.xml @@ -8,7 +8,7 @@ #000000 #81A1C1 #2E3440 - #81A1C1 + #91B4D7 #2E3440 #88C0D0 #2E3440 diff --git a/presentation-core/src/main/res/values/colors_tako.xml b/presentation-core/src/main/res/values/colors_tako.xml index 0976d1b14..6a002cd2c 100644 --- a/presentation-core/src/main/res/values/colors_tako.xml +++ b/presentation-core/src/main/res/values/colors_tako.xml @@ -17,7 +17,7 @@ #F3B375 #66577E #F3B375 - #66577E + #C8BED0 #F3B375 #F3B375 #574360 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 4fa7850a8..bc9c08faf 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt @@ -22,6 +22,7 @@ import androidx.glance.layout.fillMaxSize import androidx.glance.layout.padding import androidx.glance.unit.ColorProvider import coil3.annotation.ExperimentalCoilApi +import coil3.asDrawable import coil3.executeBlocking import coil3.imageLoader import coil3.request.CachePolicy diff --git a/settings.gradle.kts b/settings.gradle.kts index b1a2a2dd5..76b88ed6e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,16 @@ pluginManagement { mavenCentral() maven(url = "https://www.jitpack.io") } + // https://issuetracker.google.com/344363457 + // TODO: Remove when AGP's bundled R8 is updated + buildscript { + repositories { + maven("https://storage.googleapis.com/r8-releases/raw") + } + dependencies { + classpath("com.android.tools:r8:8.5.21") + } + } } dependencyResolutionManagement { diff --git a/source-api/build.gradle.kts b/source-api/build.gradle.kts index ae28caa14..ad562dcd5 100644 --- a/source-api/build.gradle.kts +++ b/source-api/build.gradle.kts @@ -13,6 +13,9 @@ kotlin { api(libs.injekt.core) api(libs.rxjava) api(libs.jsoup) + + implementation(project.dependencies.platform(compose.bom)) + implementation(compose.runtime) } } val androidMain by getting { @@ -38,7 +41,7 @@ android { tasks { withType { - kotlinOptions.freeCompilerArgs += listOf( + compilerOptions.freeCompilerArgs.addAll( "-Xexpect-actual-classes", ) } 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 77f339b9d..6c9935266 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 @@ -1,5 +1,8 @@ package eu.kanade.tachiyomi.source.model +import androidx.compose.runtime.Stable + +@Stable data class FilterList(val list: List>) : List> by list { constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) diff --git a/source-local/build.gradle.kts b/source-local/build.gradle.kts index b63ae8fab..25c268a34 100644 --- a/source-local/build.gradle.kts +++ b/source-local/build.gradle.kts @@ -12,7 +12,6 @@ kotlin { api(projects.i18n) implementation(libs.unifile) - implementation(libs.bundles.archive) } } val androidMain by getting { @@ -40,7 +39,7 @@ android { tasks { withType { - kotlinOptions.freeCompilerArgs += listOf( + compilerOptions.freeCompilerArgs.addAll( "-Xexpect-actual-classes", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", ) 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 9e2aa8406..2d9725ad8 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder @@ -16,21 +17,20 @@ import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority -import mihon.core.common.extensions.toZipFile +import mihon.core.common.archive.archiveReader import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.serialization.XML import tachiyomi.core.common.i18n.stringResource +import tachiyomi.core.common.storage.extension +import tachiyomi.core.common.storage.nameWithoutExtension +import tachiyomi.core.common.util.lang.withIOContext +import tachiyomi.core.common.util.system.ImageUtil +import tachiyomi.core.common.util.system.logcat import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.tachiyomi.MangaDetails -import tachiyomi.core.common.storage.extension -import tachiyomi.core.common.storage.nameWithoutExtension -import tachiyomi.core.common.storage.openReadOnlyChannel -import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.core.common.util.system.ImageUtil -import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.chapter.service.ChapterRecognition import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR @@ -44,7 +44,6 @@ import uy.kohesive.injekt.injectLazy import java.io.InputStream import java.nio.charset.StandardCharsets import kotlin.time.Duration.Companion.days -import com.github.junrar.Archive as JunrarArchive import tachiyomi.domain.source.model.Source as DomainSource actual class LocalSource( @@ -186,9 +185,7 @@ actual class LocalSource( // Copy ComicInfo.xml from chapter archive to top level if found noXmlFile == null -> { - val chapterArchives = mangaDirFiles - .filter(Archive::isSupported) - .toList() + val chapterArchives = mangaDirFiles.filter(Archive::isSupported) val copiedFile = copyComicInfoFileFromArchive(chapterArchives, mangaDir) if (copiedFile != null) { @@ -208,26 +205,10 @@ actual class LocalSource( private fun copyComicInfoFileFromArchive(chapterArchives: List, folder: UniFile): UniFile? { for (chapter in chapterArchives) { - when (Format.valueOf(chapter)) { - is Format.Zip -> { - chapter.openReadOnlyChannel(context).toZipFile().use { zip -> - zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> - zip.getInputStream(comicInfoFile).buffered().use { stream -> - return copyComicInfoFile(stream, folder) - } - } - } + chapter.archiveReader(context).use { reader -> + reader.getInputStream(COMIC_INFO_FILE)?.use { stream -> + return copyComicInfoFile(stream, folder) } - is Format.Rar -> { - JunrarArchive(chapter.openInputStream()).use { rar -> - rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> - rar.getInputStream(comicInfoFile).buffered().use { stream -> - return copyComicInfoFile(stream, folder) - } - } - } - } - else -> {} } } return null @@ -253,7 +234,7 @@ actual class LocalSource( override suspend fun getChapterList(manga: SManga): List = withIOContext { val chapters = fileSystem.getFilesInMangaDirectory(manga.url) // Only keep supported formats - .filter { it.isDirectory || Archive.isSupported(it) } + .filter { it.isDirectory || Archive.isSupported(it) || it.extension.equals("epub", true) } .map { chapterFile -> SChapter.create().apply { url = "${manga.url}/${chapterFile.name}" @@ -269,7 +250,7 @@ actual class LocalSource( val format = Format.valueOf(chapterFile) if (format is Format.Epub) { - EpubFile(format.file.openReadOnlyChannel(context)).use { epub -> + EpubFile(format.file.archiveReader(context)).use { epub -> epub.fillMetadata(manga, this) } } @@ -294,14 +275,14 @@ actual class LocalSource( override fun getFilterList() = FilterList(OrderBy.Popular(context)) // Unused stuff - override suspend fun getPageList(chapter: SChapter) = throw UnsupportedOperationException("Unused") + override suspend fun getPageList(chapter: SChapter): List = throw UnsupportedOperationException("Unused") fun getFormat(chapter: SChapter): Format { try { val (mangaDirName, chapterName) = chapter.url.split('/', limit = 2) return fileSystem.getBaseDirectory() - ?.findFile(mangaDirName, true) - ?.findFile(chapterName, true) + ?.findFile(mangaDirName) + ?.findFile(chapterName) ?.let(Format.Companion::valueOf) ?: throw Exception(context.stringResource(MR.strings.chapter_not_found)) } catch (e: Format.UnknownFormatException) { @@ -327,31 +308,22 @@ actual class LocalSource( entry?.let { coverManager.update(manga, it.openInputStream()) } } - is Format.Zip -> { - format.file.openReadOnlyChannel(context).toZipFile().use { zip -> - val entry = zip.entries.toList() - .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } + 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)!! } } + } - entry?.let { coverManager.update(manga, zip.getInputStream(it)) } - } - } - is Format.Rar -> { - JunrarArchive(format.file.openInputStream()).use { archive -> - val entry = archive.fileHeaders - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } - - entry?.let { coverManager.update(manga, archive.getInputStream(it)) } + entry?.let { coverManager.update(manga, reader.getInputStream(it.name)!!) } } } is Format.Epub -> { - EpubFile(format.file.openReadOnlyChannel(context)).use { epub -> - val entry = epub.getImagesFromPages() - .firstOrNull() - ?.let { epub.getEntry(it) } + EpubFile(format.file.archiveReader(context)).use { epub -> + val entry = epub.getImagesFromPages().firstOrNull() - entry?.let { coverManager.update(manga, epub.getInputStream(it)) } + entry?.let { coverManager.update(manga, epub.getInputStream(it)!!) } } } } 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 402df7e00..69faaa2b7 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 @@ -17,7 +17,7 @@ actual class LocalSourceFileSystem( actual fun getMangaDirectory(name: String): UniFile? { return getBaseDirectory() - ?.findFile(name, true) + ?.findFile(name) ?.takeIf { it.isDirectory } } 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 e968adc7d..ea18e9b53 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 @@ -5,9 +5,9 @@ import tachiyomi.core.common.storage.extension object Archive { - private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") + private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "7z", "cb7", "tar", "cbt") fun isSupported(file: UniFile): Boolean { - return file.extension in SUPPORTED_ARCHIVE_TYPES + return 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 5b22e41e2..ad53d407c 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 @@ -2,25 +2,22 @@ package tachiyomi.source.local.io import com.hippo.unifile.UniFile 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 Zip(val file: UniFile) : Format - data class Rar(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) = with(file) { - when { - isDirectory -> Directory(this) - extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this) - extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this) - extension.equals("epub", true) -> Epub(this) - 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() } } }