Merge branch 'master' into controls_refactor

This commit is contained in:
Quickdesh 2024-02-07 14:41:39 -05:00
commit 40d8028c4a
657 changed files with 20215 additions and 16660 deletions

View file

@ -3,10 +3,11 @@
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v0.12.3.10)
- To the latest version of the app (stable is v0.15.2.4)
- All extensions
- I have gone through the FAQ (https://aniyomi.org/docs/faq/general) and troubleshooting guide (https://aniyomi.org/docs/guides/troubleshooting/)
- If this is an issue with an anime extension, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi-extensions
- If this is an issue with an official anime extension, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi-extensions
- If this is an issue with an official manga extension and this issue can be replicated in the Tachiyomi app, that I should be opening an issue in https://github.com/tachiyomiorg/extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
- I will fill out the title and the information in this template

View file

@ -2,10 +2,13 @@ blank_issues_enabled: false
contact_links:
- name: ⚠️ Anime extension/source issue
url: https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose
about: Issues and requests for extensions and sources should be opened in the aniyomi-extensions repository instead
about: Issues and requests for official extensions and sources should be opened in the aniyomi-extensions repository instead
- name: 📦 Aniyomi extensions
url: https://aniyomi.org/extensions/
about: Anime extensions and sources
- name: 🧑‍💻 Aniyomi help discord
url: https://discord.gg/F32UjdJZrR
about: Common questions are answered here
- name: 🖥️ Aniyomi website
url: https://aniyomi.org/
about: Guides, troubleshooting, and answers to common questions

View file

@ -52,7 +52,7 @@ body:
label: Aniyomi version
description: You can find your Aniyomi version in **More → About**.
placeholder: |
Example: "0.12.3.10"
Example: "0.15.2.4"
validations:
required: true
@ -93,11 +93,13 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
required: true
- label: If this is an issue with an official manga extension and this issue can be replicated in the Tachiyomi app, that I should be opening an issue in [Tachiyomi's extensions repository](https://github.com/tachiyomiorg/extensions/issues/new/choose).
required: true
- label: I have gone through the [FAQ](https://aniyomi.org/docs/faq/general) and [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
- label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
required: true
- label: I have updated all installed extensions.
required: true

View file

@ -30,9 +30,9 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
required: true
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
- label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View file

@ -3,8 +3,10 @@ on:
pull_request:
paths-ignore:
- '**.md'
- 'i18n/src/main/res/**/strings.xml'
- 'i18n/src/main/res/**/strings-aniyomi.xml'
- 'i18n/src/commonMain/resources/**/strings-aniyomi.xml'
- 'i18n/src/commonMain/resources/**/strings.xml'
- 'i18n/src/commonMain/resources/**/plurals-aniyomi.xml'
- 'i18n/src/commonMain/resources/**/plurals.xml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}

View file

@ -66,6 +66,8 @@ jobs:
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "34.0.0"
- name: Clean up build artifacts
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'aniyomiorg/aniyomi'

View file

@ -2,23 +2,21 @@
|-------|-----------|-------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| [![CI](https://github.com/aniyomiorg/aniyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/aniyomiorg/aniyomi/actions/workflows/build_push.yml) | [![latest preview build](https://img.shields.io/github/v/release/aniyomiorg/aniyomi-preview.svg?maxAge=3600&label=download)](https://github.com/aniyomiorg/aniyomi-preview/releases) | [![CodeFactor](https://www.codefactor.io/repository/github/aniyomiorg/aniyomi/badge)](https://www.codefactor.io/repository/github/aniyomiorg/aniyomi) | [![stable release](https://img.shields.io/github/release/aniyomiorg/aniyomi.svg?maxAge=3600&label=download)](https://github.com/aniyomiorg/aniyomi/releases) | [![Translation status](https://hosted.weblate.org/widgets/aniyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/aniyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/841701076242530374?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/F32UjdJZrR) |
# ![app icon](.github/readme-images/app-icon.png)Aniyomi
Aniyomi is an unofficial fork of the free and open source manga reader [Tachiyomi](https://github.com/tachiyomiorg/tachiyomi) that adds anime capabilities! For Android 6.0 and above.
Aniyomi is a video player and image viewer for Android 6.0 and above.
## Features
Features include:
* Watching anime from [a variety of sources](https://github.com/aniyomiorg/aniyomi-extensions)
* Everything you know and love about Tachiyomi:
* Online reading from a variety of sources
* Local reading of downloaded content
* A configurable reader with multiple viewers, reading directions and other settings.
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
* Categories to organize your library
* Light and dark themes
* Schedule updating your library for new chapters
* Create backups locally to read offline or to your desired cloud service
* Watching videos
* View images
* Local reading/watching of downloaded content
* A configurable reader with multiple viewers, reading directions and other settings.
* A configurable player built on mpv-android with multiple options and settings
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
* Categories to organize your library
* Light and dark themes
* Create backups locally to read/watch offline or to your desired cloud service
## Download
Get the app from the [releases page](https://github.com/aniyomiorg/aniyomi/releases).

View file

@ -20,8 +20,8 @@ android {
defaultConfig {
applicationId = "xyz.jmir.tachiyomi.mi"
versionCode = 113
versionName = "0.14.7"
versionCode = 121
versionName = "0.15.2.4"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -130,6 +130,7 @@ android {
buildFeatures {
viewBinding = true
compose = true
buildConfig = true
// Disable some unused things
aidl = false
@ -253,7 +254,7 @@ dependencies {
implementation(libs.logcat)
// Crash reports
implementation(libs.acra.http)
implementation(libs.bundles.acra)
// Shizuku
implementation(libs.bundles.shizuku)

View file

@ -50,7 +50,7 @@
##---------------Begin: proguard configuration for kotlinx.serialization ----------
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
-dontnote kotlinx.serialization.** # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {

View file

@ -8,7 +8,9 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@ -20,10 +22,14 @@
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<!-- To view extension packages in API 30+ -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
<uses-permission
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
@ -45,13 +51,64 @@
<activity
android:name=".ui.main.MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.Tachiyomi.SplashScreen"
android:exported="true">
android:theme="@style/Theme.Tachiyomi.SplashScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep link to add manga repos -->
<intent-filter android:label="@string/action_add_repo">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tachiyomi" />
<data android:host="add-repo" />
</intent-filter>
<!-- Deep link to add manga repos -->
<intent-filter android:label="@string/action_add_repo">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="aniyomi" />
<data android:host="add-repo" />
</intent-filter>
<!-- Open backup files -->
<intent-filter android:label="@string/pref_restore_backup">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:host="*" />
<data android:mimeType="*/*" />
<!--
Work around Android's ugly primitive PatternMatcher
implementation that can't cope with finding a . early in
the path unless it's explicitly matched.
See https://stackoverflow.com/a/31028507
-->
<data android:pathPattern=".*\\.tachibk" />
<data android:pathPattern=".*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
</intent-filter>
<!--suppress AndroidDomInspection -->
<meta-data
android:name="android.app.shortcuts"
@ -59,17 +116,17 @@
</activity>
<activity
android:process=":error_handler"
android:name=".crash.CrashActivity"
android:exported="false" />
android:exported="false"
android:process=":error_handler" />
<activity
android:name=".ui.deeplink.anime.DeepLinkAnimeActivity"
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay"
android:exported="true"
android:label="@string/action_global_anime_search"
android:exported="true">
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -93,10 +150,10 @@
<activity
android:name=".ui.deeplink.manga.DeepLinkMangaActivity"
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay"
android:exported="true"
android:label="@string/action_global_manga_search"
android:exported="true">
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -124,14 +181,15 @@
<activity
android:name=".ui.reader.ReaderActivity"
android:launchMode="singleTask"
android:exported="false">
android:exported="false"
android:launchMode="singleTask">
<intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter>
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions"/>
<meta-data
android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions" />
</activity>
<activity
android:name=".ui.player.PlayerActivity"
@ -151,8 +209,8 @@
</activity>
<activity
android:name=".ui.security.UnlockActivity"
android:theme="@style/Theme.Tachiyomi"
android:exported="false" />
android:exported="false"
android:theme="@style/Theme.Tachiyomi" />
<activity
android:name=".ui.webview.WebViewActivity"
@ -161,39 +219,31 @@
<activity
android:name=".extension.manga.util.MangaExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false" />
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name=".extension.anime.util.AnimeExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false" />
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name=".ui.setting.track.TrackLoginActivity"
android:label="@string/track_activity_name"
android:exported="true">
android:exported="true"
android:label="@string/track_activity_name">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="anilist-auth"/>
<data android:host="bangumi-auth"/>
<data android:host="myanimelist-auth"/>
<data android:host="shikimori-auth"/>
<data android:scheme="tachiyomi"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="simkl-auth"/>
<data android:scheme="aniyomi"/>
<data android:host="myanimelist-auth" />
<data android:host="anilist-auth" />
<data android:host="bangumi-auth" />
<data android:host="shikimori-auth" />
<data android:host="simkl-auth"/>
</intent-filter>
</activity>
@ -201,17 +251,15 @@
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<service
android:name=".extension.util.ExtensionInstallService"
android:exported="false" />
<service
android:name=".extension.manga.util.MangaExtensionInstallService"
android:exported="false" />
android:exported="false"
android:foregroundServiceType="shortService" />
<service
android:name=".extension.anime.util.AnimeExtensionInstallService"
android:exported="false" />
android:exported="false"
android:foregroundServiceType="shortService" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
@ -239,9 +287,9 @@
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data

View file

@ -8,12 +8,20 @@ import eu.kanade.domain.entries.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.entries.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.entries.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.entries.manga.interactor.UpdateManga
import eu.kanade.domain.extension.anime.interactor.CreateAnimeExtensionRepo
import eu.kanade.domain.extension.anime.interactor.DeleteAnimeExtensionRepo
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionLanguages
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionRepos
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionSources
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionsByType
import eu.kanade.domain.extension.anime.interactor.TrustAnimeExtension
import eu.kanade.domain.extension.manga.interactor.CreateMangaExtensionRepo
import eu.kanade.domain.extension.manga.interactor.DeleteMangaExtensionRepo
import eu.kanade.domain.extension.manga.interactor.GetExtensionSources
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionLanguages
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionRepos
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionsByType
import eu.kanade.domain.extension.manga.interactor.TrustMangaExtension
import eu.kanade.domain.items.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.items.chapter.interactor.SetReadStatus
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource
@ -84,19 +92,19 @@ import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory
import tachiyomi.domain.category.manga.repository.MangaCategoryRepository
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
import tachiyomi.domain.entries.anime.interactor.GetAnime
import tachiyomi.domain.entries.anime.interactor.GetAnimeByUrlAndSourceId
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
import tachiyomi.domain.entries.anime.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
import tachiyomi.domain.entries.anime.repository.AnimeRepository
import tachiyomi.domain.entries.manga.interactor.GetAnimeByUrlAndSourceId
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetManga
import tachiyomi.domain.entries.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
@ -322,5 +330,14 @@ class DomainModule : InjektModule {
addFactory { ToggleLanguage(get()) }
addFactory { ToggleMangaSource(get()) }
addFactory { ToggleMangaSourcePin(get()) }
addFactory { TrustAnimeExtension(get()) }
addFactory { TrustMangaExtension(get()) }
addFactory { CreateMangaExtensionRepo(get()) }
addFactory { DeleteMangaExtensionRepo(get()) }
addFactory { GetMangaExtensionRepos(get()) }
addFactory { CreateAnimeExtensionRepo(get()) }
addFactory { DeleteAnimeExtensionRepo(get()) }
addFactory { GetAnimeExtensionRepos(get()) }
}
}

View file

@ -35,10 +35,10 @@ class BasePreferences(
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
enum class ExtensionInstaller(val titleRes: StringResource) {
LEGACY(MR.strings.ext_installer_legacy),
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),
SHIZUKU(MR.strings.ext_installer_shizuku),
PRIVATE(MR.strings.ext_installer_private),
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
LEGACY(MR.strings.ext_installer_legacy, true),
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
SHIZUKU(MR.strings.ext_installer_shizuku, false),
PRIVATE(MR.strings.ext_installer_private, false),
}
}

View file

@ -81,9 +81,9 @@ class UpdateAnime(
dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = animeFetchInterval.getWindow(dateTime),
): Boolean {
return animeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
?.let { animeRepository.updateAnime(it) }
?: false
return animeRepository.updateAnime(
animeFetchInterval.toAnimeUpdate(anime, dateTime, window),
)
}
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {

View file

@ -81,9 +81,9 @@ class UpdateManga(
dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = mangaFetchInterval.getWindow(dateTime),
): Boolean {
return mangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
?.let { mangaRepository.updateManga(it) }
?: false
return mangaRepository.updateManga(
mangaFetchInterval.toMangaUpdate(manga, dateTime, window),
)
}
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {

View file

@ -0,0 +1,38 @@
package eu.kanade.domain.extension.anime.interactor
import eu.kanade.domain.source.service.SourcePreferences
import okhttp3.HttpUrl.Companion.toHttpUrl
import tachiyomi.core.preference.plusAssign
class CreateAnimeExtensionRepo(private val preferences: SourcePreferences) {
fun await(name: String): Result {
if (name.matches(githubRepoRegex)) {
val rawUrl = name.toHttpUrl().newBuilder().apply {
removePathSegment(2) // Remove /blob/
host("raw.githubusercontent.com")
}.build().toString()
preferences.animeExtensionRepos() += rawUrl.removeSuffix("/index.min.json")
return Result.Success
}
// Do not allow invalid formats
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_ANIYOMI_REPO_BASE_URL)) {
return Result.InvalidUrl
}
preferences.animeExtensionRepos() += name.removeSuffix("/index.min.json")
return Result.Success
}
sealed interface Result {
data object InvalidUrl : Result
data object Success : Result
}
}
const val OFFICIAL_ANIYOMI_REPO_BASE_URL = "https://raw.githubusercontent.com/aniyomiorg/aniyomi-extensions/repo"
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
private val githubRepoRegex = """https://github\.com/[^/]+/[^/]+/blob/(?:[^/]+/)+index\.min\.json$""".toRegex()

View file

@ -0,0 +1,11 @@
package eu.kanade.domain.extension.anime.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.preference.minusAssign
class DeleteAnimeExtensionRepo(private val preferences: SourcePreferences) {
fun await(repo: String) {
preferences.animeExtensionRepos() -= repo
}
}

View file

@ -0,0 +1,11 @@
package eu.kanade.domain.extension.anime.interactor
import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow
class GetAnimeExtensionRepos(private val preferences: SourcePreferences) {
fun subscribe(): Flow<Set<String>> {
return preferences.animeExtensionRepos().changes()
}
}

View file

@ -0,0 +1,35 @@
package eu.kanade.domain.extension.anime.interactor
import android.content.pm.PackageInfo
import androidx.core.content.pm.PackageInfoCompat
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.preference.getAndSet
class TrustAnimeExtension(
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() ||
signatureHash == officialSignature
}
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
preferences.trustedExtensions().getAndSet { exts ->
// Remove previously trusted versions
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
removed.also {
it += "$pkgName:$versionCode:$signatureHash"
}
}
}
fun revokeAll() {
preferences.trustedExtensions().delete()
}
}
// jmir1's key
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"

View file

@ -0,0 +1,38 @@
package eu.kanade.domain.extension.manga.interactor
import eu.kanade.domain.source.service.SourcePreferences
import okhttp3.HttpUrl.Companion.toHttpUrl
import tachiyomi.core.preference.plusAssign
class CreateMangaExtensionRepo(private val preferences: SourcePreferences) {
fun await(name: String): Result {
if (name.matches(githubRepoRegex)) {
val rawUrl = name.toHttpUrl().newBuilder().apply {
removePathSegment(2) // Remove /blob/
host("raw.githubusercontent.com")
}.build().toString()
preferences.mangaExtensionRepos() += rawUrl.removeSuffix("/index.min.json")
return Result.Success
}
// Do not allow invalid formats
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_TACHIYOMI_REPO_BASE_URL)) {
return Result.InvalidUrl
}
preferences.mangaExtensionRepos() += name.removeSuffix("/index.min.json")
return Result.Success
}
sealed interface Result {
data object InvalidUrl : Result
data object Success : Result
}
}
const val OFFICIAL_TACHIYOMI_REPO_BASE_URL = "https://raw.githubusercontent.com/tachiyomiorg/extensions/repo"
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
private val githubRepoRegex = """https://github\.com/[^/]+/[^/]+/blob/(?:[^/]+/)+index\.min\.json$""".toRegex()

View file

@ -0,0 +1,11 @@
package eu.kanade.domain.extension.manga.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.preference.minusAssign
class DeleteMangaExtensionRepo(private val preferences: SourcePreferences) {
fun await(repo: String) {
preferences.mangaExtensionRepos() -= repo
}
}

View file

@ -0,0 +1,11 @@
package eu.kanade.domain.extension.manga.interactor
import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow
class GetMangaExtensionRepos(private val preferences: SourcePreferences) {
fun subscribe(): Flow<Set<String>> {
return preferences.mangaExtensionRepos().changes()
}
}

View file

@ -0,0 +1,35 @@
package eu.kanade.domain.extension.manga.interactor
import android.content.pm.PackageInfo
import androidx.core.content.pm.PackageInfoCompat
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.preference.getAndSet
class TrustMangaExtension(
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() ||
signatureHash == officialSignature
}
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
preferences.trustedExtensions().getAndSet { exts ->
// Remove previously trusted versions
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
removed.also {
it += "$pkgName:$versionCode:$signatureHash"
}
}
}
fun revokeAll() {
preferences.trustedExtensions().delete()
}
}
// inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"

View file

@ -22,7 +22,6 @@ import tachiyomi.domain.items.chapter.repository.ChapterRepository
import tachiyomi.domain.items.chapter.service.ChapterRecognition
import tachiyomi.source.local.entries.manga.isLocal
import java.lang.Long.max
import java.time.Instant
import java.time.ZonedDateTime
import java.util.TreeSet
@ -57,6 +56,7 @@ class SyncChaptersWithSource(
}
val now = ZonedDateTime.now()
val nowMillis = now.toInstant().toEpochMilli()
val sourceChapters = rawSourceChapters
.distinctBy { it.url }
@ -67,36 +67,27 @@ class SyncChaptersWithSource(
.copy(mangaId = manga.id, sourceOrder = i.toLong())
}
// Chapters from db.
val dbChapters = getChaptersByMangaId.await(manga.id)
// Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>()
// Chapters whose metadata have changed.
val toChange = mutableListOf<Chapter>()
// Chapters from the db not in source.
val toDelete = dbChapters.filterNot { dbChapter ->
val newChapters = mutableListOf<Chapter>()
val updatedChapters = mutableListOf<Chapter>()
val removedChapters = dbChapters.filterNot { dbChapter ->
sourceChapters.any { sourceChapter ->
dbChapter.url == sourceChapter.url
}
}
val rightNow = Instant.now().toEpochMilli()
// Used to not set upload date of older chapters
// to a higher value than newer chapters
var maxSeenUploadDate = 0L
val sManga = manga.toSManga()
for (sourceChapter in sourceChapters) {
var chapter = sourceChapter
// Update metadata from source if necessary.
if (source is HttpSource) {
val sChapter = chapter.toSChapter()
source.prepareNewChapter(sChapter, sManga)
source.prepareNewChapter(sChapter, manga.toSManga())
chapter = chapter.copyFromSChapter(sChapter)
}
@ -112,13 +103,13 @@ class SyncChaptersWithSource(
if (dbChapter == null) {
val toAddChapter = if (chapter.dateUpload == 0L) {
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
chapter.copy(dateUpload = altDateUpload)
} else {
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
chapter
}
toAdd.add(toAddChapter)
newChapters.add(toAddChapter)
} else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(
@ -144,13 +135,13 @@ class SyncChaptersWithSource(
if (chapter.dateUpload != 0L) {
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
}
toChange.add(toChangeChapter)
updatedChapters.add(toChangeChapter)
}
}
}
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval(
manga,
@ -167,20 +158,20 @@ class SyncChaptersWithSource(
val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
toDelete.forEach { chapter ->
removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
deletedChapterNumbers.add(chapter.chapterNumber)
}
val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to it.dateFetch }
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the chapters from most to less recent, which is common.
var itemCount = toAdd.size
var updatedToAdd = toAdd.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
var itemCount = newChapters.size
var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
@ -199,8 +190,8 @@ class SyncChaptersWithSource(
chapter
}
if (toDelete.isNotEmpty()) {
val toDeleteIds = toDelete.map { it.id }
if (removedChapters.isNotEmpty()) {
val toDeleteIds = removedChapters.map { it.id }
chapterRepository.removeChaptersWithIds(toDeleteIds)
}
@ -208,8 +199,8 @@ class SyncChaptersWithSource(
updatedToAdd = chapterRepository.addAllChapters(updatedToAdd)
}
if (toChange.isNotEmpty()) {
val chapterUpdates = toChange.map { it.toChapterUpdate() }
if (updatedChapters.isNotEmpty()) {
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates)
}
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)

View file

@ -21,7 +21,6 @@ import tachiyomi.domain.items.episode.repository.EpisodeRepository
import tachiyomi.domain.items.episode.service.EpisodeRecognition
import tachiyomi.source.local.entries.anime.isLocal
import java.lang.Long.max
import java.time.Instant
import java.time.ZonedDateTime
import java.util.TreeSet
@ -55,6 +54,7 @@ class SyncEpisodesWithSource(
}
val now = ZonedDateTime.now()
val nowMillis = now.toInstant().toEpochMilli()
val sourceEpisodes = rawSourceEpisodes
.distinctBy { it.url }
@ -65,36 +65,27 @@ class SyncEpisodesWithSource(
.copy(animeId = anime.id, sourceOrder = i.toLong())
}
// Episodes from db.
val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
// Episodes from the source not in db.
val toAdd = mutableListOf<Episode>()
// Episodes whose metadata have changed.
val toChange = mutableListOf<Episode>()
// Episodes from the db not in source.
val toDelete = dbEpisodes.filterNot { dbEpisode ->
val newEpisodes = mutableListOf<Episode>()
val updatedEpisodes = mutableListOf<Episode>()
val removedEpisodes = dbEpisodes.filterNot { dbEpisode ->
sourceEpisodes.any { sourceEpisode ->
dbEpisode.url == sourceEpisode.url
}
}
val rightNow = Instant.now().toEpochMilli()
// Used to not set upload date of older episodes
// to a higher value than newer episodes
var maxSeenUploadDate = 0L
val sAnime = anime.toSAnime()
for (sourceEpisode in sourceEpisodes) {
var episode = sourceEpisode
// Update metadata from source if necessary.
if (source is AnimeHttpSource) {
val sEpisode = episode.toSEpisode()
source.prepareNewEpisode(sEpisode, sAnime)
source.prepareNewEpisode(sEpisode, anime.toSAnime())
episode = episode.copyFromSEpisode(sEpisode)
}
@ -110,13 +101,13 @@ class SyncEpisodesWithSource(
if (dbEpisode == null) {
val toAddEpisode = if (episode.dateUpload == 0L) {
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
episode.copy(dateUpload = altDateUpload)
} else {
maxSeenUploadDate = max(maxSeenUploadDate, sourceEpisode.dateUpload)
episode
}
toAdd.add(toAddEpisode)
newEpisodes.add(toAddEpisode)
} else {
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(
@ -144,13 +135,13 @@ class SyncEpisodesWithSource(
dateUpload = sourceEpisode.dateUpload,
)
}
toChange.add(toChangeEpisode)
updatedEpisodes.add(toChangeEpisode)
}
}
}
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
if (newEpisodes.isEmpty() && removedEpisodes.isEmpty() && updatedEpisodes.isEmpty()) {
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
updateAnime.awaitUpdateFetchInterval(
anime,
@ -167,20 +158,20 @@ class SyncEpisodesWithSource(
val deletedSeenEpisodeNumbers = TreeSet<Double>()
val deletedBookmarkedEpisodeNumbers = TreeSet<Double>()
toDelete.forEach { episode ->
removedEpisodes.forEach { episode ->
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
if (episode.bookmark) deletedBookmarkedEpisodeNumbers.add(episode.episodeNumber)
deletedEpisodeNumbers.add(episode.episodeNumber)
}
val deletedEpisodeNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
val deletedEpisodeNumberDateFetchMap = removedEpisodes.sortedByDescending { it.dateFetch }
.associate { it.episodeNumber to it.dateFetch }
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the episodes from most to less recent, which is common.
var itemCount = toAdd.size
var updatedToAdd = toAdd.map { toAddItem ->
var episode = toAddItem.copy(dateFetch = rightNow + itemCount--)
var itemCount = newEpisodes.size
var updatedToAdd = newEpisodes.map { toAddItem ->
var episode = toAddItem.copy(dateFetch = nowMillis + itemCount--)
if (episode.isRecognizedNumber.not() || episode.episodeNumber !in deletedEpisodeNumbers) return@map episode
@ -199,8 +190,8 @@ class SyncEpisodesWithSource(
episode
}
if (toDelete.isNotEmpty()) {
val toDeleteIds = toDelete.map { it.id }
if (removedEpisodes.isNotEmpty()) {
val toDeleteIds = removedEpisodes.map { it.id }
episodeRepository.removeEpisodesWithIds(toDeleteIds)
}
@ -208,8 +199,8 @@ class SyncEpisodesWithSource(
updatedToAdd = episodeRepository.addAllEpisodes(updatedToAdd)
}
if (toChange.isNotEmpty()) {
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
if (updatedEpisodes.isNotEmpty()) {
val episodeUpdates = updatedEpisodes.map { it.toEpisodeUpdate() }
updateEpisode.awaitAll(episodeUpdates)
}
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)

View file

@ -37,7 +37,14 @@ class SourcePreferences(
SetMigrateSorting.Direction.ASCENDING,
)
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
fun animeExtensionRepos() = preferenceStore.getStringSet("anime_extension_repos", emptySet())
fun mangaExtensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
fun trustedExtensions() = preferenceStore.getStringSet(
Preference.appStateKey("trusted_extensions"),
emptySet(),
)
// Mixture Sources

View file

@ -25,14 +25,14 @@ class RefreshAnimeTracks(
suspend fun await(animeId: Long): List<Pair<Tracker?, Throwable>> {
return supervisorScope {
return@supervisorScope getTracks.await(animeId)
.map { it to trackerManager.get(it.syncId) }
.map { it to trackerManager.get(it.trackerId) }
.filter { (_, service) -> service?.isLoggedIn == true }
.map { (track, service) ->
async {
return@async try {
val updatedTrack = service!!.animeService.refresh(track.toDbTrack())
insertTrack.await(updatedTrack.toDomainTrack()!!)
syncEpisodeProgressWithTrack.await(animeId, track, service.animeService)
val updatedTrack = service!!.animeService.refresh(track.toDbTrack()).toDomainTrack()!!
insertTrack.await(updatedTrack)
syncEpisodeProgressWithTrack.await(animeId, updatedTrack, service.animeService)
null
} catch (e: Throwable) {
service to e

View file

@ -28,7 +28,7 @@ class TrackEpisode(
if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track ->
val service = trackerManager.get(track.syncId)
val service = trackerManager.get(track.trackerId)
if (service == null || !service.isLoggedIn || episodeNumber <= track.lastEpisodeSeen) {
return@mapNotNull null
}

View file

@ -13,10 +13,10 @@ fun AnimeTrack.copyPersonalFrom(other: AnimeTrack): AnimeTrack {
)
}
fun AnimeTrack.toDbTrack(): DbAnimeTrack = DbAnimeTrack.create(syncId).also {
fun AnimeTrack.toDbTrack(): DbAnimeTrack = DbAnimeTrack.create(trackerId).also {
it.id = id
it.anime_id = animeId
it.media_id = remoteId
it.remote_id = remoteId
it.library_id = libraryId
it.title = title
it.last_episode_seen = lastEpisodeSeen.toFloat()
@ -33,14 +33,16 @@ fun DbAnimeTrack.toDomainTrack(idRequired: Boolean = true): AnimeTrack? {
return AnimeTrack(
id = trackId,
animeId = anime_id,
syncId = sync_id.toLong(),
remoteId = media_id,
trackerId = tracker_id.toLong(),
remoteId = remote_id,
libraryId = library_id,
title = title,
lastEpisodeSeen = last_episode_seen.toDouble(),
totalEpisodes = total_episodes.toLong(),
status = status.toLong(),
score = score.toDouble(),
// Jank workaround due to precision issues while converting
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
score = score.toString().toDouble(),
remoteUrl = tracking_url,
startDate = started_watching_date,
finishDate = finished_watching_date,

View file

@ -25,14 +25,14 @@ class RefreshMangaTracks(
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
return supervisorScope {
return@supervisorScope getTracks.await(mangaId)
.map { it to trackerManager.get(it.syncId) }
.map { it to trackerManager.get(it.trackerId) }
.filter { (_, service) -> service?.isLoggedIn == true }
.map { (track, service) ->
async {
return@async try {
val updatedTrack = service!!.mangaService.refresh(track.toDbTrack())
insertTrack.await(updatedTrack.toDomainTrack()!!)
syncChapterProgressWithTrack.await(mangaId, track, service.mangaService)
val updatedTrack = service!!.mangaService.refresh(track.toDbTrack()).toDomainTrack()!!
insertTrack.await(updatedTrack)
syncChapterProgressWithTrack.await(mangaId, updatedTrack, service.mangaService)
null
} catch (e: Throwable) {
service to e

View file

@ -27,7 +27,7 @@ class TrackChapter(
if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track ->
val service = trackerManager.get(track.syncId)
val service = trackerManager.get(track.trackerId)
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
return@mapNotNull null

View file

@ -13,10 +13,10 @@ fun MangaTrack.copyPersonalFrom(other: MangaTrack): MangaTrack {
)
}
fun MangaTrack.toDbTrack(): DbMangaTrack = DbMangaTrack.create(syncId).also {
fun MangaTrack.toDbTrack(): DbMangaTrack = DbMangaTrack.create(trackerId).also {
it.id = id
it.manga_id = mangaId
it.media_id = remoteId
it.remote_id = remoteId
it.library_id = libraryId
it.title = title
it.last_chapter_read = lastChapterRead.toFloat()
@ -33,14 +33,16 @@ fun DbMangaTrack.toDomainTrack(idRequired: Boolean = true): MangaTrack? {
return MangaTrack(
id = trackId,
mangaId = manga_id,
syncId = sync_id.toLong(),
remoteId = media_id,
trackerId = tracker_id.toLong(),
remoteId = remote_id,
libraryId = library_id,
title = title,
lastChapterRead = last_chapter_read.toDouble(),
totalChapters = total_chapters.toLong(),
status = status.toLong(),
score = score.toDouble(),
// Jank workaround due to precision issues while converting
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
score = score.toString().toDouble(),
remoteUrl = tracking_url,
startDate = started_reading_date,
finishDate = finished_reading_date,

View file

@ -2,22 +2,35 @@ package eu.kanade.domain.track.service
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
class TrackPreferences(
private val preferenceStore: PreferenceStore,
) {
fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
fun trackUsername(tracker: Tracker) = preferenceStore.getString(
Preference.privateKey("pref_mangasync_username_${tracker.id}"),
"",
)
fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
"",
)
fun setCredentials(sync: Tracker, username: String, password: String) {
trackUsername(sync).set(username)
trackPassword(sync).set(password)
fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean(
Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"),
false,
)
fun setCredentials(tracker: Tracker, username: String, password: String) {
trackUsername(tracker).set(username)
trackPassword(tracker).set(password)
trackAuthExpired(tracker).set(false)
}
fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
@ -29,12 +42,4 @@ class TrackPreferences(
"show_next_episode_airing_time",
true,
)
companion object {
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
private fun trackToken(syncId: Long) = "track_token_$syncId"
}
}

View file

@ -2,6 +2,8 @@ package eu.kanade.domain.ui
import android.os.Build
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.NavStyle
import eu.kanade.domain.ui.model.StartScreen
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.tachiyomi.util.system.DeviceUtil
@ -34,6 +36,10 @@ class UiPreferences(
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
fun startScreen() = preferenceStore.getEnum("start_screen", StartScreen.ANIME)
fun navStyle() = preferenceStore.getEnum("bottom_rail_nav_style", NavStyle.MOVE_HISTORY_TO_MORE)
companion object {
fun dateFormat(format: String): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)

View file

@ -15,6 +15,7 @@ enum class AppTheme(val titleRes: StringResource?) {
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
MOCHA(MR.strings.theme_mocha),
SAPPHIRE(MR.strings.theme_sapphire),
NORD(MR.strings.theme_nord),
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
TAKO(MR.strings.theme_tako),
TEALTURQUOISE(MR.strings.theme_tealturquoise),

View file

@ -0,0 +1,48 @@
package eu.kanade.domain.ui.model
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CollectionsBookmark
import androidx.compose.material.icons.outlined.History
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.BrowseTab
import eu.kanade.tachiyomi.ui.history.HistoriesTab
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibraryTab
import eu.kanade.tachiyomi.ui.library.manga.MangaLibraryTab
import eu.kanade.tachiyomi.ui.more.MoreTab
import eu.kanade.tachiyomi.ui.updates.UpdatesTab
import tachiyomi.i18n.MR
enum class NavStyle(
val titleRes: StringResource,
val moreTab: Tab,
) {
MOVE_MANGA_TO_MORE(titleRes = MR.strings.pref_bottom_nav_no_manga, moreTab = MangaLibraryTab),
MOVE_UPDATES_TO_MORE(titleRes = MR.strings.pref_bottom_nav_no_updates, moreTab = UpdatesTab),
MOVE_HISTORY_TO_MORE(titleRes = MR.strings.pref_bottom_nav_no_history, moreTab = HistoriesTab),
;
val moreIcon: ImageVector
@Composable
get() = when (this) {
MOVE_MANGA_TO_MORE -> Icons.Outlined.CollectionsBookmark
MOVE_UPDATES_TO_MORE -> ImageVector.vectorResource(id = R.drawable.ic_updates_outline_24dp)
MOVE_HISTORY_TO_MORE -> Icons.Outlined.History
}
val tabs: List<Tab>
get() {
return mutableListOf(
AnimeLibraryTab,
MangaLibraryTab,
UpdatesTab,
HistoriesTab,
BrowseTab(),
MoreTab,
).apply { remove(this@NavStyle.moreTab) }
}
}

View file

@ -0,0 +1,18 @@
package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.ui.browse.BrowseTab
import eu.kanade.tachiyomi.ui.history.HistoriesTab
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibraryTab
import eu.kanade.tachiyomi.ui.library.manga.MangaLibraryTab
import eu.kanade.tachiyomi.ui.updates.UpdatesTab
import tachiyomi.i18n.MR
enum class StartScreen(val titleRes: StringResource, val tab: Tab) {
ANIME(MR.strings.label_anime, AnimeLibraryTab),
MANGA(MR.strings.manga, MangaLibraryTab),
UPDATES(MR.strings.label_recent_updates, UpdatesTab),
HISTORY(MR.strings.label_recent_manga, HistoriesTab),
BROWSE(MR.strings.browse, BrowseTab()),
}

View file

@ -39,7 +39,7 @@ fun GlobalSearchResultItem(
modifier = Modifier
.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.tiny,
end = MaterialTheme.padding.extraSmall,
)
.fillMaxWidth()
.clickable(onClick = onClick),

View file

@ -16,8 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
@ -36,6 +35,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -52,6 +52,7 @@ import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeExtensionDetailsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
@ -65,14 +66,23 @@ fun AnimeExtensionDetailsScreen(
navigateUp: () -> Unit,
state: AnimeExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit,
onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit,
onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
) {
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
regex.find(state.extension?.repoUrl.orEmpty())
?.let {
val (user, repo) = it.destructured
"https://github.com/$user/$repo"
}
?: state.extension?.repoUrl
}
Scaffold(
topBar = { scrollBehavior ->
AppBar(
@ -82,19 +92,14 @@ fun AnimeExtensionDetailsScreen(
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
if (state.extension?.isUnofficial == false) {
if (url != null) {
add(
AppBar.Action(
title = stringResource(MR.strings.whats_new),
icon = Icons.Outlined.History,
onClick = onClickWhatsNew,
),
)
add(
AppBar.Action(
title = stringResource(MR.strings.action_faq_and_guides),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onClickReadme,
title = stringResource(MR.strings.action_open_repo),
icon = Icons.AutoMirrored.Outlined.Launch,
onClick = {
uriHandler.openUri(url)
},
),
)
}
@ -124,7 +129,7 @@ fun AnimeExtensionDetailsScreen(
) { paddingValues ->
if (state.extension == null) {
EmptyScreen(
stringRes = MR.strings.empty_screen,
MR.strings.empty_screen,
modifier = Modifier.padding(paddingValues),
)
return@Scaffold
@ -145,7 +150,7 @@ fun AnimeExtensionDetailsScreen(
private fun AnimeExtensionDetails(
contentPadding: PaddingValues,
extension: AnimeExtension.Installed,
sources: List<AnimeExtensionSourceItem>,
sources: ImmutableList<AnimeExtensionSourceItem>,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
@ -156,12 +161,7 @@ private fun AnimeExtensionDetails(
ScrollbarLazyColumn(
contentPadding = contentPadding,
) {
when {
extension.isUnofficial ->
item {
WarningBanner(MR.strings.unofficial_anime_extension_message)
}
extension.isObsolete ->
if (extension.isObsolete) {
item {
WarningBanner(MR.strings.obsolete_extension_message)
}
@ -296,7 +296,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
OutlinedButton(
modifier = Modifier.weight(1f),

View file

@ -1,6 +1,7 @@
package eu.kanade.presentation.browse.anime
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -37,16 +38,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.core.util.fastDistinctBy
import eu.kanade.presentation.browse.BaseBrowseItem
import eu.kanade.presentation.browse.anime.components.AnimeExtensionIcon
import eu.kanade.presentation.browse.manga.ExtensionHeader
import eu.kanade.presentation.browse.manga.ExtensionTrustDialog
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh
@ -65,7 +70,7 @@ fun AnimeExtensionScreen(
searchQuery: String?,
onLongClickItem: (AnimeExtension) -> Unit,
onClickItemCancel: (AnimeExtension) -> Unit,
onClickItemWebView: (AnimeExtension.Available) -> Unit,
onOpenWebView: (AnimeExtension.Available) -> Unit,
onInstallExtension: (AnimeExtension.Available) -> Unit,
onUninstallExtension: (AnimeExtension) -> Unit,
onUpdateExtension: (AnimeExtension.Installed) -> Unit,
@ -98,7 +103,7 @@ fun AnimeExtensionScreen(
contentPadding = contentPadding,
onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel,
onClickItemWebView = onClickItemWebView,
onOpenWebView = onOpenWebView,
onInstallExtension = onInstallExtension,
onUninstallExtension = onUninstallExtension,
onUpdateExtension = onUpdateExtension,
@ -116,7 +121,7 @@ private fun AnimeExtensionContent(
state: AnimeExtensionsScreenModel.State,
contentPadding: PaddingValues,
onLongClickItem: (AnimeExtension) -> Unit,
onClickItemWebView: (AnimeExtension.Available) -> Unit,
onOpenWebView: (AnimeExtension.Available) -> Unit,
onClickItemCancel: (AnimeExtension) -> Unit,
onInstallExtension: (AnimeExtension.Available) -> Unit,
onUninstallExtension: (AnimeExtension) -> Unit,
@ -125,11 +130,24 @@ private fun AnimeExtensionContent(
onOpenExtension: (AnimeExtension.Installed) -> Unit,
onClickUpdateAll: () -> Unit,
) {
val context = LocalContext.current
var trustState by remember { mutableStateOf<AnimeExtension.Untrusted?>(null) }
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
FastScrollLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
if (!installGranted && state.installer?.requiresSystemPermission == true) {
item(key = "extension-permissions-warning") {
WarningBanner(
textRes = MR.strings.ext_permission_install_apps_warning,
modifier = Modifier.clickable {
context.launchRequestPackageInstallsPermission()
},
)
}
}
state.items.forEach { (header, items) ->
item(
contentType = "header",
@ -168,7 +186,7 @@ private fun AnimeExtensionContent(
}
items(
items = items,
items = items.fastDistinctBy { it.hashCode() },
contentType = { "item" },
key = { "extension-${it.hashCode()}" },
) { item ->
@ -183,8 +201,14 @@ private fun AnimeExtensionContent(
}
},
onLongClickItem = onLongClickItem,
onClickItemSecondaryAction = {
when (it) {
is AnimeExtension.Available -> onOpenWebView(it)
is AnimeExtension.Installed -> onOpenExtension(it)
else -> {}
}
},
onClickItemCancel = onClickItemCancel,
onClickItemWebView = onClickItemWebView,
onClickItemAction = {
when (it) {
is AnimeExtension.Available -> onInstallExtension(it)
@ -227,10 +251,10 @@ private fun AnimeExtensionItem(
item: AnimeExtensionUiModel.Item,
onClickItem: (AnimeExtension) -> Unit,
onLongClickItem: (AnimeExtension) -> Unit,
onClickItemWebView: (AnimeExtension.Available) -> Unit,
onClickItemCancel: (AnimeExtension) -> Unit,
onClickItemAction: (AnimeExtension) -> Unit,
modifier: Modifier = Modifier,
onClickItemSecondaryAction: (AnimeExtension) -> Unit,
) {
val (extension, installStep) = item
BaseBrowseItem(
@ -271,9 +295,9 @@ private fun AnimeExtensionItem(
AnimeExtensionItemActions(
extension = extension,
installStep = installStep,
onClickItemWebView = onClickItemWebView,
onClickItemCancel = onClickItemCancel,
onClickItemAction = onClickItemAction,
onClickItemSecondaryAction = onClickItemSecondaryAction,
)
},
) {
@ -303,7 +327,7 @@ private fun AnimeExtensionItemContent(
// Won't look good but it's not like we can ellipsize overflowing content
FlowRow(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is AnimeExtension.Installed && extension.lang.isNotEmpty()) {
@ -323,7 +347,6 @@ private fun AnimeExtensionItemContent(
val warning = when {
extension is AnimeExtension.Untrusted -> MR.strings.ext_untrusted
extension is AnimeExtension.Installed && extension.isUnofficial -> MR.strings.ext_unofficial
extension is AnimeExtension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
extension.isNsfw -> MR.strings.ext_nsfw_short
else -> null
@ -358,15 +381,15 @@ private fun AnimeExtensionItemActions(
extension: AnimeExtension,
installStep: InstallStep,
modifier: Modifier = Modifier,
onClickItemWebView: (AnimeExtension.Available) -> Unit = {},
onClickItemCancel: (AnimeExtension) -> Unit = {},
onClickItemAction: (AnimeExtension) -> Unit = {},
onClickItemSecondaryAction: (AnimeExtension) -> Unit = {},
) {
val isIdle = installStep.isCompleted()
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
when {
!isIdle -> {
@ -388,6 +411,13 @@ private fun AnimeExtensionItemActions(
installStep == InstallStep.Idle -> {
when (extension) {
is AnimeExtension.Installed -> {
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.action_settings),
)
}
if (extension.hasUpdate) {
IconButton(onClick = { onClickItemAction(extension) }) {
Icon(
@ -396,13 +426,6 @@ private fun AnimeExtensionItemActions(
)
}
}
IconButton(onClick = { onClickItemAction(extension) }) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.action_settings),
)
}
}
is AnimeExtension.Untrusted -> {
IconButton(onClick = { onClickItemAction(extension) }) {
@ -415,7 +438,7 @@ private fun AnimeExtensionItemActions(
is AnimeExtension.Available -> {
if (extension.sources.isNotEmpty()) {
IconButton(
onClick = { onClickItemWebView(extension) },
onClick = { onClickItemSecondaryAction(extension) },
) {
Icon(
imageVector = Icons.Outlined.Public,

View file

@ -74,9 +74,10 @@ internal fun GlobalSearchContent(
items.forEach { (source, result) ->
item(key = source.id) {
GlobalSearchResultItem(
title = fromSourceId
?.let { "${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
subtitle = LocaleHelper.getDisplayName(source.lang),
title = fromSourceId?.let {
"${source.name}".takeIf { source.id == fromSourceId }
} ?: source.name,
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
onClick = { onClickSource(source) },
) {
when (result) {

View file

@ -26,6 +26,7 @@ import eu.kanade.presentation.browse.anime.components.AnimeSourceIcon
import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem
import eu.kanade.tachiyomi.ui.browse.anime.migration.sources.MigrateAnimeSourceScreenModel
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge
@ -75,7 +76,7 @@ fun MigrateAnimeSourceScreen(
@Composable
private fun MigrateAnimeSourceList(
list: List<Pair<AnimeSource, Long>>,
list: ImmutableList<Pair<AnimeSource, Long>>,
contentPadding: PaddingValues,
onClickItem: (AnimeSource) -> Unit,
onLongClickItem: (AnimeSource) -> Unit,

View file

@ -38,7 +38,7 @@ fun GlobalAnimeSearchCardRow(
LazyRow(
contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
items(titles) {
val title by getAnime(it)

View file

@ -74,9 +74,10 @@ internal fun GlobalSearchContent(
items.forEach { (source, result) ->
item(key = source.id) {
GlobalSearchResultItem(
title = fromSourceId
?.let { "${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
subtitle = LocaleHelper.getDisplayName(source.lang),
title = fromSourceId?.let {
"${source.name}".takeIf { source.id == fromSourceId }
} ?: source.name,
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
onClick = { onClickSource(source) },
) {
when (result) {

View file

@ -16,8 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
@ -38,6 +37,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -53,6 +53,7 @@ import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaExtensionDetailsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
@ -62,18 +63,27 @@ import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
@Composable
fun ExtensionDetailsScreen(
fun MangaExtensionDetailsScreen(
navigateUp: () -> Unit,
state: MangaExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit,
onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit,
onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
) {
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
regex.find(state.extension?.repoUrl.orEmpty())
?.let {
val (user, repo) = it.destructured
"https://github.com/$user/$repo"
}
?: state.extension?.repoUrl
}
Scaffold(
topBar = { scrollBehavior ->
AppBar(
@ -83,19 +93,14 @@ fun ExtensionDetailsScreen(
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
if (state.extension?.isUnofficial == false) {
if (url != null) {
add(
AppBar.Action(
title = stringResource(MR.strings.whats_new),
icon = Icons.Outlined.History,
onClick = onClickWhatsNew,
),
)
add(
AppBar.Action(
title = stringResource(MR.strings.action_faq_and_guides),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onClickReadme,
title = stringResource(MR.strings.action_open_repo),
icon = Icons.AutoMirrored.Outlined.Launch,
onClick = {
uriHandler.openUri(url)
},
),
)
}
@ -125,7 +130,7 @@ fun ExtensionDetailsScreen(
) { paddingValues ->
if (state.extension == null) {
EmptyScreen(
stringRes = MR.strings.empty_screen,
MR.strings.empty_screen,
modifier = Modifier.padding(paddingValues),
)
return@Scaffold
@ -146,7 +151,7 @@ fun ExtensionDetailsScreen(
private fun ExtensionDetails(
contentPadding: PaddingValues,
extension: MangaExtension.Installed,
sources: List<MangaExtensionSourceItem>,
sources: ImmutableList<MangaExtensionSourceItem>,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
@ -157,12 +162,7 @@ private fun ExtensionDetails(
ScrollbarLazyColumn(
contentPadding = contentPadding,
) {
when {
extension.isUnofficial ->
item {
WarningBanner(MR.strings.unofficial_extension_message)
}
extension.isObsolete ->
if (extension.isObsolete) {
item {
WarningBanner(MR.strings.obsolete_extension_message)
}
@ -295,7 +295,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
OutlinedButton(
modifier = Modifier.weight(1f),

View file

@ -1,6 +1,7 @@
package eu.kanade.presentation.browse.manga
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -40,14 +41,18 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.StringResource
import eu.kanade.core.util.fastDistinctBy
import eu.kanade.presentation.browse.BaseBrowseItem
import eu.kanade.presentation.browse.manga.components.MangaExtensionIcon
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh
@ -67,7 +72,7 @@ fun MangaExtensionScreen(
searchQuery: String?,
onLongClickItem: (MangaExtension) -> Unit,
onClickItemCancel: (MangaExtension) -> Unit,
onClickItemWebView: (MangaExtension.Available) -> Unit,
onOpenWebView: (MangaExtension.Available) -> Unit,
onInstallExtension: (MangaExtension.Available) -> Unit,
onUninstallExtension: (MangaExtension) -> Unit,
onUpdateExtension: (MangaExtension.Installed) -> Unit,
@ -100,7 +105,7 @@ fun MangaExtensionScreen(
contentPadding = contentPadding,
onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel,
onClickItemWebView = onClickItemWebView,
onOpenWebView = onOpenWebView,
onInstallExtension = onInstallExtension,
onUninstallExtension = onUninstallExtension,
onUpdateExtension = onUpdateExtension,
@ -118,7 +123,7 @@ private fun ExtensionContent(
state: MangaExtensionsScreenModel.State,
contentPadding: PaddingValues,
onLongClickItem: (MangaExtension) -> Unit,
onClickItemWebView: (MangaExtension.Available) -> Unit,
onOpenWebView: (MangaExtension.Available) -> Unit,
onClickItemCancel: (MangaExtension) -> Unit,
onInstallExtension: (MangaExtension.Available) -> Unit,
onUninstallExtension: (MangaExtension) -> Unit,
@ -127,11 +132,24 @@ private fun ExtensionContent(
onOpenExtension: (MangaExtension.Installed) -> Unit,
onClickUpdateAll: () -> Unit,
) {
val context = LocalContext.current
var trustState by remember { mutableStateOf<MangaExtension.Untrusted?>(null) }
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
FastScrollLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
if (!installGranted && state.installer?.requiresSystemPermission == true) {
item(key = "extension-permissions-warning") {
WarningBanner(
textRes = MR.strings.ext_permission_install_apps_warning,
modifier = Modifier.clickable {
context.launchRequestPackageInstallsPermission()
},
)
}
}
state.items.forEach { (header, items) ->
item(
contentType = "header",
@ -170,7 +188,7 @@ private fun ExtensionContent(
}
items(
items = items,
items = items.fastDistinctBy { it.hashCode() },
contentType = { "item" },
key = { "extension-${it.hashCode()}" },
) { item ->
@ -185,7 +203,13 @@ private fun ExtensionContent(
}
},
onLongClickItem = onLongClickItem,
onClickItemWebView = onClickItemWebView,
onClickItemSecondaryAction = {
when (it) {
is MangaExtension.Available -> onOpenWebView(it)
is MangaExtension.Installed -> onOpenExtension(it)
else -> {}
}
},
onClickItemCancel = onClickItemCancel,
onClickItemAction = {
when (it) {
@ -229,9 +253,9 @@ private fun ExtensionItem(
item: MangaExtensionUiModel.Item,
onClickItem: (MangaExtension) -> Unit,
onLongClickItem: (MangaExtension) -> Unit,
onClickItemWebView: (MangaExtension.Available) -> Unit,
onClickItemCancel: (MangaExtension) -> Unit,
onClickItemAction: (MangaExtension) -> Unit,
onClickItemSecondaryAction: (MangaExtension) -> Unit,
modifier: Modifier = Modifier,
) {
val (extension, installStep) = item
@ -273,9 +297,9 @@ private fun ExtensionItem(
ExtensionItemActions(
extension = extension,
installStep = installStep,
onClickItemWebView = onClickItemWebView,
onClickItemCancel = onClickItemCancel,
onClickItemAction = onClickItemAction,
onClickItemSecondaryAction = onClickItemSecondaryAction,
)
},
) {
@ -305,7 +329,7 @@ private fun ExtensionItemContent(
// Won't look good but it's not like we can ellipsize overflowing content
FlowRow(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is MangaExtension.Installed && extension.lang.isNotEmpty()) {
@ -325,7 +349,6 @@ private fun ExtensionItemContent(
val warning = when {
extension is MangaExtension.Untrusted -> MR.strings.ext_untrusted
extension is MangaExtension.Installed && extension.isUnofficial -> MR.strings.ext_unofficial
extension is MangaExtension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
extension.isNsfw -> MR.strings.ext_nsfw_short
else -> null
@ -360,15 +383,15 @@ private fun ExtensionItemActions(
extension: MangaExtension,
installStep: InstallStep,
modifier: Modifier = Modifier,
onClickItemWebView: (MangaExtension.Available) -> Unit = {},
onClickItemCancel: (MangaExtension) -> Unit = {},
onClickItemAction: (MangaExtension) -> Unit = {},
onClickItemSecondaryAction: (MangaExtension) -> Unit = {},
) {
val isIdle = installStep.isCompleted()
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
when {
!isIdle -> {
@ -390,6 +413,13 @@ private fun ExtensionItemActions(
installStep == InstallStep.Idle -> {
when (extension) {
is MangaExtension.Installed -> {
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.action_settings),
)
}
if (extension.hasUpdate) {
IconButton(onClick = { onClickItemAction(extension) }) {
Icon(
@ -398,13 +428,6 @@ private fun ExtensionItemActions(
)
}
}
IconButton(onClick = { onClickItemAction(extension) }) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.action_settings),
)
}
}
is MangaExtension.Untrusted -> {
IconButton(onClick = { onClickItemAction(extension) }) {
@ -417,7 +440,7 @@ private fun ExtensionItemActions(
is MangaExtension.Available -> {
if (extension.sources.isNotEmpty()) {
IconButton(
onClick = { onClickItemWebView(extension) },
onClick = { onClickItemSecondaryAction(extension) },
) {
Icon(
imageVector = Icons.Outlined.Public,

View file

@ -26,6 +26,7 @@ import eu.kanade.presentation.browse.manga.components.BaseMangaSourceItem
import eu.kanade.presentation.browse.manga.components.MangaSourceIcon
import eu.kanade.tachiyomi.ui.browse.manga.migration.sources.MigrateMangaSourceScreenModel
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge
@ -75,7 +76,7 @@ fun MigrateMangaSourceScreen(
@Composable
private fun MigrateSourceList(
list: List<Pair<Source, Long>>,
list: ImmutableList<Pair<Source, Long>>,
contentPadding: PaddingValues,
onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit,

View file

@ -38,7 +38,7 @@ fun GlobalMangaSearchCardRow(
LazyRow(
contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
items(titles) {
val title by getManga(it)

View file

@ -27,6 +27,8 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import eu.kanade.core.preference.asToggleableState
import eu.kanade.presentation.category.visualName
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import tachiyomi.core.preference.CheckboxState
import tachiyomi.domain.category.model.Category
@ -39,12 +41,12 @@ import kotlin.time.Duration.Companion.seconds
fun CategoryCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
categories: List<Category>,
categories: ImmutableList<String>,
) {
var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
val nameAlreadyExists = remember(name) { categories.contains(name) }
AlertDialog(
onDismissRequest = onDismissRequest,
@ -69,10 +71,13 @@ fun CategoryCreateDialog(
},
text = {
OutlinedTextField(
modifier = Modifier.focusRequester(focusRequester),
modifier = Modifier
.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
label = { Text(text = stringResource(MR.strings.name)) },
label = {
Text(text = stringResource(MR.strings.name))
},
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
MR.strings.error_category_exists
@ -98,14 +103,14 @@ fun CategoryCreateDialog(
fun CategoryRenameDialog(
onDismissRequest: () -> Unit,
onRename: (String) -> Unit,
categories: List<Category>,
category: Category,
categories: ImmutableList<String>,
category: String,
) {
var name by remember { mutableStateOf(category.name) }
var name by remember { mutableStateOf(category) }
var valueHasChanged by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
val nameAlreadyExists = remember(name) { categories.contains(name) }
AlertDialog(
onDismissRequest = onDismissRequest,
@ -162,7 +167,7 @@ fun CategoryRenameDialog(
fun CategoryDeleteDialog(
onDismissRequest: () -> Unit,
onDelete: () -> Unit,
category: Category,
category: String,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
@ -183,7 +188,7 @@ fun CategoryDeleteDialog(
Text(text = stringResource(MR.strings.delete_category))
},
text = {
Text(text = stringResource(MR.strings.delete_category_confirmation, category.name))
Text(text = stringResource(MR.strings.delete_category_confirmation, category))
},
)
}
@ -219,7 +224,7 @@ fun CategorySortAlphabeticallyDialog(
@Composable
fun ChangeCategoryDialog(
initialSelection: List<CheckboxState<Category>>,
initialSelection: ImmutableList<CheckboxState<Category>>,
onDismissRequest: () -> Unit,
onEditCategories: () -> Unit,
onConfirm: (List<Long>, List<Long>) -> Unit,
@ -291,7 +296,7 @@ fun ChangeCategoryDialog(
if (index != -1) {
val mutableList = selection.toMutableList()
mutableList[index] = it.next()
selection = mutableList.toList()
selection = mutableList.toList().toImmutableList()
}
}
Row(
@ -325,7 +330,3 @@ fun ChangeCategoryDialog(
},
)
}
internal fun List<Category>.anyWithName(name: String): Boolean {
return any { name == it.name }
}

View file

@ -6,6 +6,7 @@ import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.i18n.stringResource
@ -16,11 +17,13 @@ import tachiyomi.presentation.core.util.isScrollingUp
fun CategoryFloatingActionButton(
lazyListState: LazyListState,
onCreate: () -> Unit,
modifier: Modifier = Modifier,
) {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
onClick = onCreate,
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
modifier = modifier,
)
}

View file

@ -52,7 +52,7 @@ fun CategoryListItem(
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Text(
text = category.name,
modifier = Modifier
@ -64,13 +64,13 @@ fun CategoryListItem(
onClick = { onMoveUp(category) },
enabled = canMoveUp,
) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
}
IconButton(
onClick = { onMoveDown(category) },
enabled = canMoveDown,
) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) {

View file

@ -0,0 +1,40 @@
package eu.kanade.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.util.lang.toRelativeString
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@Composable
fun relativeDateText(
dateEpochMillis: Long,
): String {
return relativeDateText(
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L },
)
}
@Composable
fun relativeDateText(
date: Date?,
): String {
val context = LocalContext.current
val preferences = remember { Injekt.get<UiPreferences>() }
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
return date
?.toRelativeString(
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
?: stringResource(MR.strings.not_applicable)
}

View file

@ -1,7 +1,10 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.RadioButtonChecked
@ -22,12 +25,17 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
/**
* DropdownMenu but overlaps anchor and has width constraints to better
* match non-Compose implementation.
*/
@Composable
fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(8.dp, (-56).dp),
scrollState: ScrollState = rememberScrollState(),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit,
) {
@ -36,6 +44,7 @@ fun DropdownMenu(
onDismissRequest = onDismissRequest,
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
offset = offset,
scrollState = scrollState,
properties = properties,
content = content,
)
@ -45,6 +54,7 @@ fun DropdownMenu(
fun RadioMenuItem(
text: @Composable () -> Unit,
isChecked: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
DropdownMenuItem(
@ -64,6 +74,7 @@ fun RadioMenuItem(
)
}
},
modifier = modifier,
)
}
@ -71,10 +82,12 @@ fun RadioMenuItem(
fun NestedMenuItem(
text: @Composable () -> Unit,
children: @Composable ColumnScope.(() -> Unit) -> Unit,
modifier: Modifier = Modifier,
) {
var nestedExpanded by remember { mutableStateOf(false) }
val closeMenu = { nestedExpanded = false }
Box {
DropdownMenuItem(
text = text,
onClick = { nestedExpanded = true },
@ -89,7 +102,9 @@ fun NestedMenuItem(
DropdownMenu(
expanded = nestedExpanded,
onDismissRequest = closeMenu,
modifier = modifier,
) {
children(closeMenu)
}
}
}

View file

@ -3,7 +3,9 @@ package eu.kanade.presentation.components
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.entries.DownloadAction
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
@ -14,20 +16,24 @@ fun EntryDownloadDropdownMenu(
onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit,
isManga: Boolean,
modifier: Modifier = Modifier,
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
) {
val downloadAmount = if (isManga) MR.plurals.download_amount_manga else MR.plurals.download_amount_anime
val downloadAmount = if (isManga) MR.plurals.download_amount else MR.plurals.download_amount_anime
val downloadUnviewed = if (isManga) MR.strings.download_unread else MR.strings.download_unseen
listOfNotNull(
val options = persistentListOf(
DownloadAction.NEXT_1_ITEM to pluralStringResource(downloadAmount, 1, 1),
DownloadAction.NEXT_5_ITEMS to pluralStringResource(downloadAmount, 5, 5),
DownloadAction.NEXT_10_ITEMS to pluralStringResource(downloadAmount, 10, 10),
DownloadAction.NEXT_25_ITEMS to pluralStringResource(downloadAmount, 25, 25),
DownloadAction.UNVIEWED_ITEMS to stringResource(downloadUnviewed),
).map { (downloadAction, string) ->
)
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
options.map { (downloadAction, string) ->
DropdownMenuItem(
text = { Text(text = string) },
onClick = {

View file

@ -1,30 +0,0 @@
package eu.kanade.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import eu.kanade.tachiyomi.util.lang.toRelativeString
import tachiyomi.presentation.core.components.ListGroupHeader
import java.text.DateFormat
import java.util.Date
@Composable
fun RelativeDateHeader(
date: Date,
relativeTime: Boolean,
dateFormat: DateFormat,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
ListGroupHeader(
modifier = modifier,
text = remember {
date.toRelativeString(
context,
relativeTime,
dateFormat,
)
},
)
}

View file

@ -53,6 +53,7 @@ import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.entries.anime.model.episodesFiltered
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.entries.DownloadAction
import eu.kanade.presentation.entries.EntryScreenItem
import eu.kanade.presentation.entries.anime.components.AnimeActionRow
@ -70,10 +71,9 @@ import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeSourcePreferencesScreen
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.coroutines.delay
import tachiyomi.domain.entries.anime.model.Anime
@ -90,17 +90,15 @@ 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 java.text.DateFormat
import java.util.Date
import tachiyomi.source.local.entries.anime.isLocal
import java.time.Instant
import java.util.concurrent.TimeUnit
@Composable
fun AnimeScreen(
state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
fetchInterval: Int?,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
nextUpdate: Instant?,
isTabletUi: Boolean,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
@ -156,16 +154,14 @@ fun AnimeScreen(
val navigator = LocalNavigator.currentOrThrow
val onSettingsClicked: (() -> Unit)? = {
navigator.push(SourcePreferencesScreen(state.source.id))
navigator.push(AnimeSourcePreferencesScreen(state.source.id))
}.takeIf { state.source is ConfigurableAnimeSource }
if (!isTabletUi) {
AnimeScreenSmallImpl(
state = state,
snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
nextUpdate = nextUpdate,
episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction,
showNextEpisodeAirTime = showNextEpisodeAirTime,
@ -204,13 +200,11 @@ fun AnimeScreen(
AnimeScreenLargeImpl(
state = state,
snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
nextUpdate = nextUpdate,
episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction,
showNextEpisodeAirTime = showNextEpisodeAirTime,
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
onBackClicked = onBackClicked,
onEpisodeClicked = onEpisodeClicked,
onDownloadEpisode = onDownloadEpisode,
@ -249,9 +243,7 @@ fun AnimeScreen(
private fun AnimeScreenSmallImpl(
state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
nextUpdate: Instant?,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
showNextEpisodeAirTime: Boolean,
@ -455,7 +447,7 @@ private fun AnimeScreenSmallImpl(
AnimeActionRow(
favorite = state.anime.favorite,
trackingCount = state.trackingCount,
fetchInterval = fetchInterval,
nextUpdate = nextUpdate,
isUserIntervalMode = state.anime.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
@ -526,8 +518,6 @@ private fun AnimeScreenSmallImpl(
anime = state.anime,
episodes = listItem,
isAnyEpisodeSelected = episodes.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction,
onEpisodeClicked = onEpisodeClicked,
@ -546,9 +536,7 @@ private fun AnimeScreenSmallImpl(
fun AnimeScreenLargeImpl(
state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
nextUpdate: Instant?,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
showNextEpisodeAirTime: Boolean,
@ -734,7 +722,7 @@ fun AnimeScreenLargeImpl(
AnimeActionRow(
favorite = state.anime.favorite,
trackingCount = state.trackingCount,
fetchInterval = fetchInterval,
nextUpdate = nextUpdate,
isUserIntervalMode = state.anime.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
@ -812,8 +800,6 @@ fun AnimeScreenLargeImpl(
anime = state.anime,
episodes = listItem,
isAnyEpisodeSelected = episodes.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction,
onEpisodeClicked = onEpisodeClicked,
@ -884,8 +870,6 @@ private fun LazyListScope.sharedEpisodeItems(
anime: Anime,
episodes: List<EpisodeList>,
isAnyEpisodeSelected: Boolean,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
onEpisodeClicked: (Episode, Boolean) -> Unit,
@ -904,7 +888,6 @@ private fun LazyListScope.sharedEpisodeItems(
contentType = { EntryScreenItem.ITEM },
) { episodeItem ->
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
when (episodeItem) {
is EpisodeList.MissingCount -> {
@ -920,15 +903,7 @@ private fun LazyListScope.sharedEpisodeItems(
} else {
episodeItem.episode.name
},
date = episodeItem.episode.dateUpload
.takeIf { it > 0L }
?.let {
Date(it).toRelativeString(
context,
dateRelativeTime,
dateFormat,
)
},
date = relativeDateText(episodeItem.episode.dateUpload),
watchProgress = episodeItem.episode.lastSecondSeen
.takeIf { !episodeItem.episode.seen && it > 0L }
?.let {
@ -942,7 +917,7 @@ private fun LazyListScope.sharedEpisodeItems(
seen = episodeItem.episode.seen,
bookmark = episodeItem.episode.bookmark,
selected = episodeItem.selected,
downloadIndicatorEnabled = !isAnyEpisodeSelected,
downloadIndicatorEnabled = !isAnyEpisodeSelected && !anime.isLocal(),
downloadStateProvider = { episodeItem.downloadState },
downloadProgressProvider = { episodeItem.downloadProgress },
episodeSwipeStartAction = episodeSwipeStartAction,

View file

@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
@ -28,7 +29,7 @@ fun DuplicateAnimeDialog(
},
confirmButton = {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
TextButton(
onClick = {

View file

@ -21,6 +21,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Input
import androidx.compose.material.icons.outlined.NavigateNext
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.SystemUpdateAlt
@ -256,6 +257,18 @@ private fun VideoList(
)
}
},
onIntPlayerClicked = {
scope.launch {
MainActivity.startPlayerActivity(
context,
anime.id,
episode.id,
false,
selectedVideo,
videoList,
)
}
},
)
}
}
@ -289,6 +302,7 @@ private fun QualityOptions(
onExtDownloadClicked: () -> Unit = {},
onCopyClicked: () -> Unit = {},
onExtPlayerClicked: () -> Unit = {},
onIntPlayerClicked: () -> Unit = {},
) {
val closeMenu = { EpisodeOptionsDialogScreen.onDismissDialog() }
@ -325,6 +339,15 @@ private fun QualityOptions(
closeMenu()
},
)
ClickableRow(
text = stringResource(MR.strings.action_play_internally),
icon = Icons.Outlined.Input,
onClick = {
onIntPlayerClicked()
closeMenu()
},
)
}
}

View file

@ -188,9 +188,9 @@ fun AnimeEpisodeListItem(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (watchProgress != null || scanlator != null) DotSeparatorText()
}
if (watchProgress != null) {
DotSeparatorText()
Text(
text = watchProgress,
maxLines = 1,
@ -200,6 +200,7 @@ fun AnimeEpisodeListItem(
if (scanlator != null) DotSeparatorText()
}
if (scanlator != null) {
DotSeparatorText()
Text(
text = scanlator,
maxLines = 1,
@ -210,18 +211,16 @@ fun AnimeEpisodeListItem(
}
}
if (onDownloadClick != null) {
EpisodeDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = onDownloadClick,
onClick = { onDownloadClick?.invoke(it) },
)
}
}
}
}
}
private fun getSwipeAction(

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -87,14 +88,14 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.secondaryItemAlpha
import kotlin.math.absoluteValue
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable
fun AnimeInfoBox(
modifier: Modifier = Modifier,
isTabletUi: Boolean,
appBarPadding: Dp,
title: String,
@ -106,6 +107,7 @@ fun AnimeInfoBox(
status: Long,
onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
// Backdrop
@ -164,10 +166,9 @@ fun AnimeInfoBox(
@Composable
fun AnimeActionRow(
modifier: Modifier = Modifier,
favorite: Boolean,
trackingCount: Int,
fetchInterval: Int?,
nextUpdate: Instant?,
isUserIntervalMode: Boolean,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
@ -175,9 +176,20 @@ fun AnimeActionRow(
onTrackingClicked: () -> Unit,
onEditIntervalClicked: (() -> Unit)?,
onEditCategory: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
// TODO: show something better when using custom interval
val nextUpdateDays = remember(nextUpdate) {
return@remember if (nextUpdate != null) {
val now = Instant.now()
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
} else {
null
}
}
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
AnimeActionButton(
title = if (favorite) {
@ -190,18 +202,20 @@ fun AnimeActionRow(
onClick = onAddToLibraryClicked,
onLongClick = onEditCategory,
)
if (onEditIntervalClicked != null && fetchInterval != null) {
AnimeActionButton(
title = pluralStringResource(
title = when (nextUpdateDays) {
null -> stringResource(MR.strings.not_applicable)
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
else -> pluralStringResource(
MR.plurals.day,
count = fetchInterval.absoluteValue,
fetchInterval.absoluteValue,
),
count = nextUpdateDays,
nextUpdateDays,
)
},
icon = Icons.Default.HourglassEmpty,
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onEditIntervalClicked,
onClick = { onEditIntervalClicked?.invoke() },
)
}
AnimeActionButton(
title = if (trackingCount == 0) {
stringResource(MR.strings.manga_tracking_tab)
@ -227,12 +241,12 @@ fun AnimeActionRow(
@Composable
fun ExpandableAnimeDescription(
modifier: Modifier = Modifier,
defaultExpandState: Boolean,
description: String?,
tagsProvider: () -> List<String>?,
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
val (expanded, onExpanded) = rememberSaveable {
@ -288,7 +302,7 @@ fun ExpandableAnimeDescription(
if (expanded) {
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
tags.forEach {
TagsChip(
@ -304,7 +318,7 @@ fun ExpandableAnimeDescription(
} else {
LazyRow(
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
items(items = tags) {
TagsChip(
@ -407,15 +421,15 @@ private fun AnimeAndSourceTitlesSmall(
}
@Composable
private fun AnimeContentInfo(
private fun ColumnScope.AnimeContentInfo(
title: String,
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
artist: String?,
status: Long,
sourceName: String,
isStubSource: Boolean,
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
) {
val context = LocalContext.current
Text(
@ -439,7 +453,7 @@ private fun AnimeContentInfo(
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@ -470,7 +484,7 @@ private fun AnimeContentInfo(
if (!artist.isNullOrBlank() && author != artist) {
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(

View file

@ -20,8 +20,8 @@ import tachiyomi.presentation.core.components.material.padding
@Composable
fun BaseAnimeListItem(
modifier: Modifier = Modifier,
anime: Anime,
modifier: Modifier = Modifier,
onClickItem: () -> Unit = {},
onClickCover: () -> Unit = onClickItem,
cover: @Composable RowScope.() -> Unit = { defaultCover(anime, onClickCover) },

View file

@ -46,10 +46,10 @@ enum class EpisodeDownloadAction {
@Composable
fun EpisodeDownloadIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
downloadStateProvider: () -> AnimeDownload.State,
downloadProgressProvider: () -> Int,
onClick: (EpisodeDownloadAction) -> Unit,
modifier: Modifier = Modifier,
) {
when (val downloadState = downloadStateProvider()) {
AnimeDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
@ -106,10 +106,10 @@ private fun NotDownloadedIndicator(
@Composable
private fun DownloadingIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
downloadState: AnimeDownload.State,
downloadProgressProvider: () -> Int,
onClick: (EpisodeDownloadAction) -> Unit,
modifier: Modifier = Modifier,
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(

View file

@ -2,13 +2,24 @@ package eu.kanade.presentation.entries.components
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun DotSeparatorText() {
Text(text = "")
fun DotSeparatorText(
modifier: Modifier = Modifier,
) {
Text(
text = "",
modifier = modifier,
)
}
@Composable
fun DotSeparatorNoSpaceText() {
Text(text = "")
fun DotSeparatorNoSpaceText(
modifier: Modifier = Modifier,
) {
Text(
text = "",
modifier = modifier,
)
}

View file

@ -225,7 +225,10 @@ private fun RowScope.Button(
onClick: () -> Unit,
content: (@Composable () -> Unit)? = null,
) {
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
val animatedWeight by animateFloatAsState(
targetValue = if (toConfirm) 2f else 1f,
label = "weight",
)
Column(
modifier = Modifier
.size(48.dp)
@ -262,13 +265,13 @@ private fun RowScope.Button(
@Composable
fun LibraryBottomActionMenu(
visible: Boolean,
modifier: Modifier = Modifier,
onChangeCategoryClicked: () -> Unit,
onMarkAsViewedClicked: () -> Unit,
onMarkAsUnviewedClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: () -> Unit,
isManga: Boolean,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = visible,

View file

@ -20,7 +20,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
@ -161,6 +160,14 @@ fun EntryToolbar(
),
)
}
if (onClickSettings != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.settings),
onClick = onClickSettings,
),
)
}
}
.build(),
)

View file

@ -22,8 +22,8 @@ enum class ItemCover(val ratio: Float) {
@Composable
operator fun invoke(
modifier: Modifier = Modifier,
data: Any?,
modifier: Modifier = Modifier,
contentDescription: String = "",
shape: Shape = MaterialTheme.shapes.extraSmall,
onClick: (() -> Unit)? = null,

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
@ -23,16 +24,17 @@ fun ItemHeader(
missingItemsCount: Int,
onClick: () -> Unit,
isManga: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.clickable(
enabled = enabled,
onClick = onClick,
)
.padding(horizontal = 16.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
Text(
text = if (itemCount == null) {

View file

@ -1,23 +1,37 @@
package eu.kanade.presentation.entries.components
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
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.unit.DpSize
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
@Composable
fun DeleteItemsDialog(
@ -55,21 +69,65 @@ fun DeleteItemsDialog(
@Composable
fun SetIntervalDialog(
interval: Int,
nextUpdate: Instant?,
onDismissRequest: () -> Unit,
onValueChanged: (Int) -> Unit,
isManga: Boolean,
onValueChanged: ((Int) -> Unit)? = null,
) {
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
val nextUpdateDays = remember(nextUpdate) {
return@remember if (nextUpdate != null) {
val now = Instant.now()
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
} else {
null
}
}
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.manga_modify_calculated_interval_title)) },
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
text = {
Column {
if (nextUpdateDays != null && nextUpdateDays >= 0 && interval >= 0) {
Text(
stringResource(
if (isManga) {
MR.strings.manga_interval_expected_update
} else {
MR.strings.anime_interval_expected_update
},
pluralStringResource(
MR.plurals.day,
count = nextUpdateDays,
nextUpdateDays,
),
pluralStringResource(
MR.plurals.day,
count = interval.absoluteValue,
interval.absoluteValue,
),
),
)
Spacer(Modifier.height(MaterialTheme.padding.small))
}
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
Text(stringResource(MR.strings.manga_interval_custom_amount))
BoxWithConstraints(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..28)
val maxInterval = if (isManga) {
MangaFetchInterval.MAX_INTERVAL
} else {
AnimeFetchInterval.MAX_INTERVAL
}
val items = (0..maxInterval)
.map {
if (it == 0) {
stringResource(MR.strings.label_default)
@ -85,6 +143,8 @@ fun SetIntervalDialog(
onSelectionChanged = { selectedInterval = it },
)
}
}
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
@ -93,7 +153,7 @@ fun SetIntervalDialog(
},
confirmButton = {
TextButton(onClick = {
onValueChanged(selectedInterval)
onValueChanged?.invoke(selectedInterval)
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))

View file

@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
@ -28,7 +29,7 @@ fun DuplicateMangaDialog(
},
confirmButton = {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
TextButton(
onClick = {

View file

@ -49,6 +49,7 @@ import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.entries.DownloadAction
import eu.kanade.presentation.entries.EntryScreenItem
import eu.kanade.presentation.entries.components.EntryBottomActionMenu
@ -67,7 +68,6 @@ import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
import eu.kanade.tachiyomi.ui.entries.manga.ChapterList
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
@ -83,16 +83,14 @@ 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 java.text.DateFormat
import java.util.Date
import tachiyomi.source.local.entries.manga.isLocal
import java.time.Instant
@Composable
fun MangaScreen(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
fetchInterval: Int?,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
nextUpdate: Instant?,
isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -152,9 +150,7 @@ fun MangaScreen(
MangaScreenSmallImpl(
state = state,
snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
nextUpdate = nextUpdate,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onBackClicked = onBackClicked,
@ -190,11 +186,9 @@ fun MangaScreen(
MangaScreenLargeImpl(
state = state,
snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
nextUpdate = nextUpdate,
onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
@ -231,9 +225,7 @@ fun MangaScreen(
private fun MangaScreenSmallImpl(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
@ -425,7 +417,7 @@ private fun MangaScreenSmallImpl(
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
fetchInterval = fetchInterval,
nextUpdate = nextUpdate,
isUserIntervalMode = state.manga.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
@ -469,8 +461,6 @@ private fun MangaScreenSmallImpl(
manga = state.manga,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked,
@ -488,9 +478,7 @@ private fun MangaScreenSmallImpl(
fun MangaScreenLargeImpl(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
@ -670,7 +658,7 @@ fun MangaScreenLargeImpl(
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
fetchInterval = fetchInterval,
nextUpdate = nextUpdate,
isUserIntervalMode = state.manga.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
@ -721,8 +709,6 @@ fun MangaScreenLargeImpl(
manga = state.manga,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked,
@ -775,7 +761,7 @@ private fun SharedMangaBottomActionMenu(
onDeleteClicked = {
onMultiDeleteClicked(selected.fastMap { it.chapter })
}.takeIf {
onDownloadChapter != null && selected.fastAny { it.downloadState == MangaDownload.State.DOWNLOADED }
selected.fastAny { it.downloadState == MangaDownload.State.DOWNLOADED }
},
isManga = true,
)
@ -785,8 +771,6 @@ private fun LazyListScope.sharedChapterItems(
manga: Manga,
chapters: List<ChapterList>,
isAnyChapterSelected: Boolean,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit,
@ -805,7 +789,6 @@ private fun LazyListScope.sharedChapterItems(
contentType = { EntryScreenItem.ITEM },
) { item ->
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
when (item) {
is ChapterList.MissingCount -> {
@ -821,15 +804,7 @@ private fun LazyListScope.sharedChapterItems(
} else {
item.chapter.name
},
date = item.chapter.dateUpload
.takeIf { it > 0L }
?.let {
Date(it).toRelativeString(
context,
dateRelativeTime,
dateFormat,
)
},
date = relativeDateText(item.chapter.dateUpload),
readProgress = item.chapter.lastPageRead
.takeIf { !item.chapter.read && it > 0L }
?.let {
@ -842,7 +817,7 @@ private fun LazyListScope.sharedChapterItems(
read = item.chapter.read,
bookmark = item.chapter.bookmark,
selected = item.selected,
downloadIndicatorEnabled = !isAnyChapterSelected,
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
downloadStateProvider = { item.downloadState },
downloadProgressProvider = { item.downloadProgress },
chapterSwipeStartAction = chapterSwipeStartAction,

View file

@ -20,8 +20,8 @@ import tachiyomi.presentation.core.components.material.padding
@Composable
fun BaseMangaListItem(
modifier: Modifier = Modifier,
manga: Manga,
modifier: Modifier = Modifier,
onClickItem: () -> Unit = {},
onClickCover: () -> Unit = onClickItem,
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },

View file

@ -45,10 +45,10 @@ enum class ChapterDownloadAction {
@Composable
fun ChapterDownloadIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
downloadStateProvider: () -> MangaDownload.State,
downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit,
modifier: Modifier = Modifier,
) {
when (val downloadState = downloadStateProvider()) {
MangaDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
@ -105,10 +105,10 @@ private fun NotDownloadedIndicator(
@Composable
private fun DownloadingIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
downloadState: MangaDownload.State,
downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit,
modifier: Modifier = Modifier,
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(

View file

@ -186,9 +186,9 @@ fun MangaChapterListItem(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (readProgress != null || scanlator != null) DotSeparatorText()
}
if (readProgress != null) {
DotSeparatorText()
Text(
text = readProgress,
maxLines = 1,
@ -197,6 +197,7 @@ fun MangaChapterListItem(
)
}
if (scanlator != null) {
DotSeparatorText()
Text(
text = scanlator,
maxLines = 1,
@ -207,18 +208,16 @@ fun MangaChapterListItem(
}
}
if (onDownloadClick != null) {
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = onDownloadClick,
onClick = { onDownloadClick?.invoke(it) },
)
}
}
}
}
}
private fun getSwipeAction(

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -87,7 +88,8 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.secondaryItemAlpha
import kotlin.math.absoluteValue
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@ -166,7 +168,7 @@ fun MangaInfoBox(
fun MangaActionRow(
favorite: Boolean,
trackingCount: Int,
fetchInterval: Int?,
nextUpdate: Instant?,
isUserIntervalMode: Boolean,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
@ -178,6 +180,16 @@ fun MangaActionRow(
) {
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
// TODO: show something better when using custom interval
val nextUpdateDays = remember(nextUpdate) {
return@remember if (nextUpdate != null) {
val now = Instant.now()
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
} else {
null
}
}
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
MangaActionButton(
title = if (favorite) {
@ -190,18 +202,20 @@ fun MangaActionRow(
onClick = onAddToLibraryClicked,
onLongClick = onEditCategory,
)
if (onEditIntervalClicked != null && fetchInterval != null) {
MangaActionButton(
title = pluralStringResource(
title = when (nextUpdateDays) {
null -> stringResource(MR.strings.not_applicable)
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
else -> pluralStringResource(
MR.plurals.day,
count = fetchInterval.absoluteValue,
fetchInterval.absoluteValue,
),
count = nextUpdateDays,
nextUpdateDays,
)
},
icon = Icons.Default.HourglassEmpty,
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onEditIntervalClicked,
onClick = { onEditIntervalClicked?.invoke() },
)
}
MangaActionButton(
title = if (trackingCount == 0) {
stringResource(MR.strings.manga_tracking_tab)
@ -287,7 +301,7 @@ fun ExpandableMangaDescription(
if (expanded) {
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
tags.forEach {
TagsChip(
@ -303,7 +317,7 @@ fun ExpandableMangaDescription(
} else {
LazyRow(
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
items(items = tags) {
TagsChip(
@ -406,7 +420,7 @@ private fun MangaAndSourceTitlesSmall(
}
@Composable
private fun MangaContentInfo(
private fun ColumnScope.MangaContentInfo(
title: String,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
@ -438,7 +452,7 @@ private fun MangaContentInfo(
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@ -469,7 +483,7 @@ private fun MangaContentInfo(
if (!artist.isNullOrBlank() && author != artist) {
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(

View file

@ -3,6 +3,7 @@ package eu.kanade.presentation.history
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -11,10 +12,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.random.Random
@ -32,7 +33,7 @@ fun HistoryDeleteDialog(
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
val subtitle = if (isManga) {
MR.strings.dialog_with_checkbox_remove_description
@ -42,7 +43,11 @@ fun HistoryDeleteDialog(
Text(text = stringResource(subtitle))
LabeledCheckbox(
label = stringResource(MR.strings.dialog_with_checkbox_reset),
label = if (isManga) {
stringResource(MR.strings.dialog_with_checkbox_reset)
} else {
stringResource(MR.strings.dialog_with_checkbox_reset_anime)
},
checked = removeEverything,
onCheckedChange = { removeEverything = it },
)

View file

@ -6,24 +6,20 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.history.anime.components.AnimeHistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@Composable
@ -33,7 +29,6 @@ fun AnimeHistoryScreen(
onClickCover: (animeId: Long) -> Unit,
onClickResume: (animeId: Long, episodeId: Long) -> Unit,
onDialogChange: (AnimeHistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
searchQuery: String? = null,
) {
Scaffold(
@ -53,17 +48,12 @@ fun AnimeHistoryScreen(
modifier = Modifier.padding(contentPadding),
)
} else {
AnimeHistoryContent(
AnimeHistoryScreenContent(
history = it,
contentPadding = contentPadding,
onClickCover = { history -> onClickCover(history.animeId) },
onClickResume = { history -> onClickResume(history.animeId, history.episodeId) },
onClickDelete = { item ->
onDialogChange(
AnimeHistoryScreenModel.Dialog.Delete(item),
)
},
preferences = preferences,
onClickDelete = { item -> onDialogChange(AnimeHistoryScreenModel.Dialog.Delete(item)) },
)
}
}
@ -71,17 +61,13 @@ fun AnimeHistoryScreen(
}
@Composable
private fun AnimeHistoryContent(
private fun AnimeHistoryScreenContent(
history: List<AnimeHistoryUiModel>,
contentPadding: PaddingValues,
onClickCover: (AnimeHistoryWithRelations) -> Unit,
onClickResume: (AnimeHistoryWithRelations) -> Unit,
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
preferences: UiPreferences,
) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
@ -97,11 +83,9 @@ private fun AnimeHistoryContent(
) { item ->
when (item) {
is AnimeHistoryUiModel.Header -> {
RelativeDateHeader(
ListGroupHeader(
modifier = Modifier.animateItemPlacement(),
date = item.date,
relativeTime = relativeTime,
dateFormat = dateFormat,
text = relativeDateText(item.date),
)
}
is AnimeHistoryUiModel.Item -> {
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
preferences = UiPreferences(
InMemoryPreferenceStore(
sequenceOf(
InMemoryPreferenceStore.InMemoryPreference(
key = "relative_time_v2",
data = false,
defaultValue = false,
),
),
),
),
)
}
}

View file

@ -6,24 +6,20 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.history.manga.components.MangaHistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@Composable
@ -33,7 +29,6 @@ fun MangaHistoryScreen(
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (MangaHistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
searchQuery: String? = null,
) {
Scaffold(
@ -53,17 +48,12 @@ fun MangaHistoryScreen(
modifier = Modifier.padding(contentPadding),
)
} else {
MangaHistoryContent(
MangaHistoryScreenContent(
history = it,
contentPadding = contentPadding,
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item ->
onDialogChange(
MangaHistoryScreenModel.Dialog.Delete(item),
)
},
preferences = preferences,
onClickDelete = { item -> onDialogChange(MangaHistoryScreenModel.Dialog.Delete(item)) },
)
}
}
@ -71,17 +61,13 @@ fun MangaHistoryScreen(
}
@Composable
private fun MangaHistoryContent(
private fun MangaHistoryScreenContent(
history: List<MangaHistoryUiModel>,
contentPadding: PaddingValues,
onClickCover: (MangaHistoryWithRelations) -> Unit,
onClickResume: (MangaHistoryWithRelations) -> Unit,
onClickDelete: (MangaHistoryWithRelations) -> Unit,
preferences: UiPreferences,
) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
@ -97,11 +83,9 @@ private fun MangaHistoryContent(
) { item ->
when (item) {
is MangaHistoryUiModel.Header -> {
RelativeDateHeader(
ListGroupHeader(
modifier = Modifier.animateItemPlacement(),
date = item.date,
relativeTime = relativeTime,
dateFormat = dateFormat,
text = relativeDateText(item.date),
)
}
is MangaHistoryUiModel.Item -> {
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
preferences = UiPreferences(
InMemoryPreferenceStore(
sequenceOf(
InMemoryPreferenceStore.InMemoryPreference(
key = "relative_time_v2",
data = false,
defaultValue = false,
),
),
),
),
)
}
}

View file

@ -16,6 +16,8 @@ import androidx.compose.ui.platform.LocalConfiguration
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibrarySettingsScreenModel
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.preference.TriState
import tachiyomi.domain.category.model.Category
@ -74,6 +76,8 @@ private fun ColumnScope.FilterPage(
) {
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedAnime().collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
val autoUpdateAnimeRestrictions by screenModel.libraryPreferences.autoUpdateItemRestrictions().collectAsState()
TriStateItem(
label = stringResource(MR.strings.label_downloaded),
state = if (downloadedOnly) {
@ -108,6 +112,18 @@ private fun ColumnScope.FilterPage(
state = filterCompleted,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedAnime) },
)
// TODO: re-enable when custom intervals are ready for stable
if (
(isDevFlavor || isPreviewBuildType) &&
LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in autoUpdateAnimeRestrictions
) {
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_interval_custom),
state = filterIntervalCustom,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
)
}
val trackers = remember { screenModel.trackers }
when (trackers.size) {

View file

@ -40,6 +40,7 @@ fun LibraryToolbar(
searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
navigateUp: (() -> Unit)? = null,
) = when {
selectedCount > 0 -> LibrarySelectionToolbar(
selectedCount = selectedCount,
@ -57,6 +58,7 @@ fun LibraryToolbar(
onClickGlobalUpdate = onClickGlobalUpdate,
onClickOpenRandomEntry = onClickOpenRandomEntry,
scrollBehavior = scrollBehavior,
navigateUp = navigateUp,
)
}
@ -71,6 +73,7 @@ private fun LibraryRegularToolbar(
onClickGlobalUpdate: () -> Unit,
onClickOpenRandomEntry: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
navigateUp: (() -> Unit)?,
) {
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
SearchToolbar(
@ -119,6 +122,7 @@ private fun LibraryRegularToolbar(
)
},
scrollBehavior = scrollBehavior,
navigateUp = navigateUp,
)
}

View file

@ -16,6 +16,8 @@ import androidx.compose.ui.platform.LocalConfiguration
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.library.manga.MangaLibrarySettingsScreenModel
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.preference.TriState
import tachiyomi.domain.category.model.Category
@ -74,6 +76,8 @@ private fun ColumnScope.FilterPage(
) {
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedManga().collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateItemRestrictions().collectAsState()
TriStateItem(
label = stringResource(MR.strings.label_downloaded),
state = if (downloadedOnly) {
@ -108,6 +112,18 @@ private fun ColumnScope.FilterPage(
state = filterCompleted,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedManga) },
)
// TODO: re-enable when custom intervals are ready for stable
if (
(isDevFlavor || isPreviewBuildType) &&
LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions
) {
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_interval_custom),
state = filterIntervalCustom,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
)
}
val trackers = remember { screenModel.trackers }
when (trackers.size) {

View file

@ -12,9 +12,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material.icons.outlined.CollectionsBookmark
import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Settings
@ -25,19 +23,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import eu.kanade.domain.ui.model.NavStyle
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.Constants
import eu.kanade.tachiyomi.ui.more.DownloadQueueState
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.injectLazy
@Composable
fun MoreScreen(
@ -47,6 +44,7 @@ fun MoreScreen(
incognitoMode: Boolean,
onIncognitoModeChange: (Boolean) -> Unit,
isFDroid: Boolean,
navStyle: NavStyle,
onClickAlt: () -> Unit,
onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit,
@ -107,23 +105,10 @@ fun MoreScreen(
item { HorizontalDivider() }
val libraryPreferences: LibraryPreferences by injectLazy()
item {
val bottomNavStyle = libraryPreferences.bottomNavStyle().get()
val titleRes = when (bottomNavStyle) {
0 -> MR.strings.label_recent_manga
1 -> MR.strings.label_recent_updates
else -> MR.strings.label_manga
}
val icon = when (bottomNavStyle) {
0 -> Icons.Outlined.History
1 -> ImageVector.vectorResource(id = R.drawable.ic_updates_outline_24dp)
else -> Icons.Outlined.CollectionsBookmark
}
TextPreferenceWidget(
title = stringResource(titleRes),
icon = icon,
title = navStyle.moreTab.options.title,
icon = navStyle.moreIcon,
onPreferenceClick = onClickAlt,
)
}
@ -148,6 +133,7 @@ fun MoreScreen(
}"
}
}
is DownloadQueueState.Downloading -> {
val pending = downloadQueueState.pending
pluralStringResource(

View file

@ -17,7 +17,7 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.Material3RichText
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.RichTextStringStyle
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR
@ -42,7 +42,7 @@ fun NewUpdateScreen(
rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate,
) {
Material3RichText(
RichText(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.padding.large),
@ -59,7 +59,7 @@ fun NewUpdateScreen(
modifier = Modifier.padding(top = MaterialTheme.padding.small),
) {
Text(text = stringResource(MR.strings.update_check_open))
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
Spacer(modifier = Modifier.width(MaterialTheme.padding.extraSmall))
Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
}
}

View file

@ -15,17 +15,22 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
internal fun GuidesStep(
onRestoreBackup: () -> Unit,
) {
internal class GuidesStep(
private val onRestoreBackup: () -> Unit,
) : OnboardingStep {
override val isComplete: Boolean = true
@Composable
override fun Content() {
val handler = LocalUriHandler.current
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
Button(
@ -36,6 +41,7 @@ internal fun GuidesStep(
}
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
@ -47,6 +53,7 @@ internal fun GuidesStep(
Text(stringResource(MR.strings.pref_restore_backup))
}
}
}
}
const val GETTING_STARTED_URL = "https://aniyomi.org/docs/guides/getting-started"
@ -57,6 +64,6 @@ private fun GuidesStepPreview() {
TachiyomiTheme {
GuidesStep(
onRestoreBackup = {},
)
).Content()
}
}

View file

@ -13,15 +13,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.util.system.toast
import soup.compose.material.motion.animation.materialSharedAxisX
import soup.compose.material.motion.animation.rememberSlideDistance
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@ -29,24 +26,21 @@ import tachiyomi.presentation.core.screens.InfoScreen
@Composable
fun OnboardingScreen(
storagePreferences: StoragePreferences,
uiPreferences: UiPreferences,
onComplete: () -> Unit,
onRestoreBackup: () -> Unit,
) {
val context = LocalContext.current
val slideDistance = rememberSlideDistance()
var currentStep by remember { mutableIntStateOf(0) }
val steps: List<@Composable () -> Unit> = remember {
var currentStep by rememberSaveable { mutableIntStateOf(0) }
val steps = remember {
listOf(
{ ThemeStep(uiPreferences = uiPreferences) },
{ StorageStep(storagePref = storagePreferences.baseStorageDirectory()) },
// TODO: prompt for notification permissions when bumping target to Android 13
{ GuidesStep(onRestoreBackup = onRestoreBackup) },
ThemeStep(),
StorageStep(),
PermissionStep(),
GuidesStep(onRestoreBackup = onRestoreBackup),
)
}
val isLastStep = currentStep == steps.size - 1
val isLastStep = currentStep == steps.lastIndex
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
@ -61,17 +55,13 @@ fun OnboardingScreen(
MR.strings.onboarding_action_next
},
),
canAccept = steps[currentStep].isComplete,
onAcceptClick = {
if (isLastStep) {
onComplete()
} else {
// TODO: this is kind of janky
if (currentStep == 1 && !storagePreferences.baseStorageDirectory().isSet()) {
context.toast(MR.strings.onboarding_storage_selection_required)
} else {
currentStep++
}
}
},
) {
Box(
@ -91,7 +81,7 @@ fun OnboardingScreen(
},
label = "stepContent",
) {
steps[it]()
steps[it].Content()
}
}
}

View file

@ -0,0 +1,11 @@
package eu.kanade.presentation.more.onboarding
import androidx.compose.runtime.Composable
internal interface OnboardingStep {
val isComplete: Boolean
@Composable
fun Content()
}

View file

@ -0,0 +1,160 @@
package eu.kanade.presentation.more.onboarding
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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 eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
internal class PermissionStep : OnboardingStep {
private var notificationGranted by mutableStateOf(false)
private var batteryGranted by mutableStateOf(false)
override val isComplete: Boolean = true
@Composable
override fun Content() {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val installGranted = rememberRequestPackageInstallsPermissionState()
DisposableEffect(lifecycleOwner.lifecycle) {
val observer = object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
} else {
true
}
batteryGranted = context.getSystemService<PowerManager>()!!
.isIgnoringBatteryOptimizations(context.packageName)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Column {
PermissionItem(
title = stringResource(MR.strings.onboarding_permission_install_apps),
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
granted = installGranted,
onButtonClick = {
context.launchRequestPackageInstallsPermission()
},
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permissionRequester = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = {
// no-op. resulting checks is being done on resume
},
)
PermissionItem(
title = stringResource(MR.strings.onboarding_permission_notifications),
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
granted = notificationGranted,
onButtonClick = { permissionRequester.launch(Manifest.permission.POST_NOTIFICATIONS) },
)
}
PermissionItem(
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
granted = batteryGranted,
onButtonClick = {
@SuppressLint("BatteryLife")
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
}
context.startActivity(intent)
},
)
}
}
@Composable
private fun SectionHeader(
text: String,
modifier: Modifier = Modifier,
) {
Text(
text = text,
style = MaterialTheme.typography.titleLarge,
modifier = modifier
.padding(horizontal = 16.dp)
.secondaryItemAlpha(),
)
}
@Composable
private fun PermissionItem(
title: String,
subtitle: String,
granted: Boolean,
modifier: Modifier = Modifier,
onButtonClick: () -> Unit,
) {
ListItem(
modifier = modifier,
headlineContent = { Text(text = title) },
supportingContent = { Text(text = subtitle) },
trailingContent = {
OutlinedButton(
enabled = !granted,
onClick = onButtonClick,
) {
if (granted) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
} else {
Text(stringResource(MR.strings.onboarding_permission_action_grant))
}
}
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
}

View file

@ -5,28 +5,53 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
import eu.kanade.tachiyomi.util.system.isTvBox
import eu.kanade.tachiyomi.util.system.toast
import tachiyomi.core.preference.Preference
import kotlinx.coroutines.flow.collectLatest
import tachiyomi.core.storage.AndroidStorageFolderProvider
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
internal fun StorageStep(
storagePref: Preference<String>,
) {
internal class StorageStep : OnboardingStep {
private val storagePref = Injekt.get<StoragePreferences>().baseStorageDirectory()
private val folderProvider = Injekt.get<AndroidStorageFolderProvider>()
private var _isComplete by mutableStateOf(false)
override val isComplete: Boolean
get() = _isComplete
@Composable
override fun Content() {
val context = LocalContext.current
val handler = LocalUriHandler.current
val isTvBox = isTvBox(LocalContext.current)
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(
stringResource(
@ -36,6 +61,22 @@ internal fun StorageStep(
),
)
if (isTvBox) {
if (!storagePref.isSet()) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
val storage = folderProvider.directory()
if (!storage.exists()) {
storage.mkdirs()
}
storagePref.set(storagePref.get())
},
) {
Text(stringResource(MR.strings.onboarding_storage_action_create_folder))
}
}
} else {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
@ -49,4 +90,24 @@ internal fun StorageStep(
Text(stringResource(MR.strings.onboarding_storage_action_select))
}
}
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
Text(stringResource(MR.strings.onboarding_storage_help_info, stringResource(MR.strings.app_name)))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { handler.openUri(SettingsDataScreen.HELP_URL) },
) {
Text(stringResource(MR.strings.onboarding_storage_help_action))
}
}
LaunchedEffect(Unit) {
storagePref.changes()
.collectLatest { _isComplete = storagePref.isSet() }
}
}
}

View file

@ -8,11 +8,17 @@ import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
internal fun ThemeStep(
uiPreferences: UiPreferences,
) {
internal class ThemeStep : OnboardingStep {
override val isComplete: Boolean = true
private val uiPreferences: UiPreferences = Injekt.get()
@Composable
override fun Content() {
val themeModePref = uiPreferences.themeMode()
val themeMode by themeModePref.collectAsState()
@ -37,4 +43,5 @@ internal fun ThemeStep(
onItemClick = { appThemePref.set(it) },
)
}
}
}

View file

@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import eu.kanade.tachiyomi.data.track.Tracker
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.core.preference.Preference as PreferenceData
@ -64,13 +66,13 @@ sealed class Preference {
val pref: PreferenceData<T>,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: T, entries: Map<T, String>) -> String? =
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
val entries: Map<T, String>,
val entries: ImmutableMap<T, String>,
) : PreferenceItem<T>() {
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(
@ -78,8 +80,8 @@ sealed class Preference {
)
@Composable
internal fun internalSubtitleProvider(value: Any?, entries: Map<out Any?, String>) =
subtitleProvider(value as T, entries as Map<T, String>)
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
subtitleProvider(value as T, entries as ImmutableMap<T, String>)
}
/**
@ -89,13 +91,13 @@ sealed class Preference {
val value: String,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: String, entries: Map<String, String>) -> String? =
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val entries: Map<String, String>,
val entries: ImmutableMap<String, String>,
) : PreferenceItem<String>()
/**
@ -106,7 +108,10 @@ sealed class Preference {
val pref: PreferenceData<Set<String>>,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: Set<String>, entries: Map<String, String>) -> String? = { v, e ->
val subtitleProvider: @Composable (
value: Set<String>,
entries: ImmutableMap<String, String>,
) -> String? = { v, e ->
val combined = remember(v) {
v.map { e[it] }
.takeIf { it.isNotEmpty() }
@ -118,7 +123,7 @@ sealed class Preference {
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
val entries: Map<String, String>,
val entries: ImmutableMap<String, String>,
) : PreferenceItem<Set<String>>()
/**
@ -184,6 +189,6 @@ sealed class Preference {
override val title: String,
override val enabled: Boolean = true,
val preferenceItems: List<PreferenceItem<out Any>>,
val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
) : Preference()
}

View file

@ -21,7 +21,6 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
@ -172,8 +171,8 @@ internal fun PreferenceItem(
)
}
is Preference.PreferenceItem.TrackerPreference -> {
val uName by Injekt.get<PreferenceStore>()
.getString(TrackPreferences.trackUsername(item.tracker.id))
val uName by Injekt.get<TrackPreferences>()
.trackUsername(item.tracker)
.collectAsState()
item.tracker.run {
TrackingPreferenceWidget(

View file

@ -8,6 +8,7 @@ import eu.kanade.core.preference.asState
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding
import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
@ -47,10 +48,15 @@ object AdvancedPlayerSettingsScreen : SearchableSettings {
postfix = if (mpvInput.asState(scope).value.lines().size > 2) "\n..." else "",
),
),
Preference.PreferenceItem.SwitchPreference(
title = context.stringResource(MR.strings.pref_gpu_next_title),
subtitle = context.stringResource(MR.strings.pref_gpu_next_subtitle),
pref = playerPreferences.gpuNext(),
),
Preference.PreferenceItem.ListPreference(
title = context.stringResource(MR.strings.pref_debanding_title),
pref = playerPreferences.videoDebanding(),
entries = VideoDebanding.entries.associateWith { context.stringResource(it.stringRes) }
entries = VideoDebanding.entries.associateWith { context.stringResource(it.stringRes) }.toImmutableMap()
),
)
}

View file

@ -11,7 +11,6 @@ import tachiyomi.presentation.core.i18n.stringResource
/**
* Returns a string of categories name for settings subtitle
*/
@ReadOnlyComposable
@Composable
fun getCategoriesLabel(

View file

@ -24,6 +24,8 @@ import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.extension.anime.interactor.TrustAnimeExtension
import eu.kanade.domain.extension.manga.interactor.TrustMangaExtension
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.source.service.SourcePreferences.DataSaver
import eu.kanade.presentation.more.settings.Preference
@ -32,9 +34,10 @@ import eu.kanade.presentation.more.settings.screen.advanced.ClearDatabaseScreen
import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
import eu.kanade.tachiyomi.data.library.anime.AnimeMetadataUpdateJob
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.library.manga.MangaMetadataUpdateJob
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_360
@ -44,6 +47,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.network.PREF_DOH_CONTROLD
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
import eu.kanade.tachiyomi.network.PREF_DOH_LIBREDNS
import eu.kanade.tachiyomi.network.PREF_DOH_MULLVAD
import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
@ -51,12 +55,17 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.launch
import logcat.LogPriority
import okhttp3.Headers
@ -159,7 +168,7 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_background_activity),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_disable_battery_optimization),
subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary),
@ -200,7 +209,7 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_data),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_invalidate_download_cache),
subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary),
@ -236,7 +245,7 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_network),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_clear_cookies),
onClick = {
@ -267,7 +276,7 @@ object SettingsAdvancedScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = networkPreferences.dohProvider(),
title = stringResource(MR.strings.pref_dns_over_https),
entries = mapOf(
entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled),
PREF_DOH_CLOUDFLARE to "Cloudflare",
PREF_DOH_GOOGLE to "Google",
@ -281,6 +290,7 @@ object SettingsAdvancedScreen : SearchableSettings {
PREF_DOH_CONTROLD to "Control D",
PREF_DOH_NJALLA to "Njalla",
PREF_DOH_SHECAN to "Shecan",
PREF_DOH_LIBREDNS to "LibreDNS",
),
onValueChanged = {
context.stringResource(MR.strings.requires_app_restart)
@ -317,16 +327,17 @@ object SettingsAdvancedScreen : SearchableSettings {
private fun getLibraryGroup(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val trackerManager = remember { Injekt.get<TrackerManager>() }
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_library),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_refresh_library_covers),
onClick = {
AnimeLibraryUpdateJob.startNow(context)
MangaLibraryUpdateJob.startNow(context)
AnimeMetadataUpdateJob.startNow(context)
MangaMetadataUpdateJob.startNow(context)
},
),
Preference.PreferenceItem.TextPreference(
@ -358,6 +369,8 @@ object SettingsAdvancedScreen : SearchableSettings {
val uriHandler = LocalUriHandler.current
val extensionInstallerPref = basePreferences.extensionInstaller()
var shizukuMissing by rememberSaveable { mutableStateOf(false) }
val trustAnimeExtension = remember { Injekt.get<TrustAnimeExtension>() }
val trustMangaExtension = remember { Injekt.get<TrustMangaExtension>() }
if (shizukuMissing) {
val dismiss = { shizukuMissing = false }
@ -388,12 +401,21 @@ object SettingsAdvancedScreen : SearchableSettings {
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_extensions),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = extensionInstallerPref,
title = stringResource(MR.strings.ext_installer_pref),
entries = extensionInstallerPref.entries
.associateWith { stringResource(it.titleRes) },
.filter {
// TODO: allow private option in stable versions once URL handling is more fleshed out
if (isPreviewBuildType || isDevFlavor) {
true
} else {
it != BasePreferences.ExtensionInstaller.PRIVATE
}
}
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
onValueChanged = {
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
!context.isShizukuInstalled
@ -405,6 +427,14 @@ object SettingsAdvancedScreen : SearchableSettings {
}
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.ext_revoke_trust),
onClick = {
trustMangaExtension.revokeAll()
trustAnimeExtension.revokeAll()
context.toast(MR.strings.requires_app_restart)
},
),
),
)
}
@ -416,12 +446,12 @@ object SettingsAdvancedScreen : SearchableSettings {
val dataSaver by sourcePreferences.dataSaver().collectAsState()
return Preference.PreferenceGroup(
title = stringResource(MR.strings.data_saver),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = sourcePreferences.dataSaver(),
title = stringResource(MR.strings.data_saver),
subtitle = stringResource(MR.strings.data_saver_summary),
entries = mapOf(
entries = persistentMapOf(
DataSaver.NONE to stringResource(MR.strings.disabled),
DataSaver.BANDWIDTH_HERO to stringResource(MR.strings.bandwidth_hero),
DataSaver.WSRV_NL to stringResource(MR.strings.wsrv),
@ -462,7 +492,7 @@ object SettingsAdvancedScreen : SearchableSettings {
"80%",
"90%",
"95%",
).associateBy { it.trimEnd('%').toInt() },
).associateBy { it.trimEnd('%').toInt() }.toPersistentMap(),
enabled = dataSaver != DataSaver.NONE,
),
kotlin.run {

View file

@ -1,34 +1,28 @@
package eu.kanade.presentation.more.settings.screen
import android.app.Activity
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat
import androidx.core.os.LocaleListCompat
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.NavStyle
import eu.kanade.domain.ui.model.StartScreen
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.appearance.AppLanguageScreen
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import org.xmlpull.v1.XmlPullParser
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.core.i18n.stringResource
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
@ -69,7 +63,7 @@ object SettingsAppearanceScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_theme),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.CustomPreference(
title = stringResource(MR.strings.pref_app_theme),
) {
@ -107,13 +101,8 @@ object SettingsAppearanceScreen : SearchableSettings {
uiPreferences: UiPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val langs = remember { getLangs(context) }
var currentLanguage by remember {
mutableStateOf(
AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "",
)
}
val now = remember { Instant.now().toEpochMilli() }
val dateFormat by uiPreferences.dateFormat().collectAsState()
@ -121,67 +110,43 @@ object SettingsAppearanceScreen : SearchableSettings {
UiPreferences.dateFormat(dateFormat).format(now)
}
LaunchedEffect(currentLanguage) {
val locale = if (currentLanguage.isEmpty()) {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(currentLanguage)
}
AppCompatDelegate.setApplicationLocales(locale)
}
val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
LaunchedEffect(Unit) {
libraryPrefs.bottomNavStyle().changes()
.drop(1)
.collectLatest { value ->
HomeScreen.tabs = when (value) {
0 -> HomeScreen.tabsNoHistory
1 -> HomeScreen.tabsNoUpdates
else -> HomeScreen.tabsNoManga
}
(context as? Activity)?.let {
ActivityCompat.recreate(it)
}
}
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_display),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = libraryPrefs.bottomNavStyle(),
title = stringResource(MR.strings.pref_bottom_nav_style),
entries = mapOf(
0 to stringResource(MR.strings.pref_bottom_nav_no_history),
1 to stringResource(MR.strings.pref_bottom_nav_no_updates),
2 to stringResource(MR.strings.pref_bottom_nav_no_manga),
),
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPrefs.isDefaultHomeTabLibraryManga(),
title = stringResource(MR.strings.pref_default_home_tab_library),
enabled = libraryPrefs.bottomNavStyle().get() != 2,
),
Preference.PreferenceItem.BasicListPreference(
value = currentLanguage,
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_app_language),
entries = langs,
onValueChanged = { newValue ->
currentLanguage = newValue
true
},
onClick = { navigator.push(AppLanguageScreen()) },
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.tabletUiMode(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
entries = TabletUiMode.entries.associateWith { stringResource(it.titleRes) },
entries = TabletUiMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
onValueChanged = {
context.stringResource(MR.strings.requires_app_restart)
true
},
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.startScreen(),
title = stringResource(MR.strings.pref_start_screen),
entries = StartScreen.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
onValueChanged = {
context.stringResource(MR.strings.requires_app_restart)
true
},
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.navStyle(),
title = "Navigation Style",
entries = NavStyle.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
onValueChanged = { true },
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.dateFormat(),
title = stringResource(MR.strings.pref_date_format),
@ -189,7 +154,8 @@ object SettingsAppearanceScreen : SearchableSettings {
.associateWith {
val formattedDate = UiPreferences.dateFormat(it).format(now)
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
},
}
.toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.relativeTime(),
@ -203,30 +169,6 @@ object SettingsAppearanceScreen : SearchableSettings {
),
)
}
private fun getLangs(context: Context): Map<String, String> {
val langs = mutableListOf<Pair<String, String>>()
val parser = context.resources.getXml(R.xml.locales_config)
var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && parser.name == "locale") {
for (i in 0..<parser.attributeCount) {
if (parser.getAttributeName(i) == "name") {
val langTag = parser.getAttributeValue(i)
val displayName = LocaleHelper.getDisplayName(langTag)
if (displayName.isNotEmpty()) {
langs.add(Pair(langTag, displayName))
}
}
}
}
eventType = parser.next()
}
langs.sortBy { it.second }
langs.add(0, Pair("", context.stringResource(MR.strings.label_default)))
return langs.toMap()
}
}
private val DateFormats = listOf(

View file

@ -2,15 +2,23 @@ package eu.kanade.presentation.more.settings.screen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.browse.AnimeExtensionReposScreen
import eu.kanade.presentation.more.settings.screen.browse.MangaExtensionReposScreen
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.i18n.stringResource
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
import uy.kohesive.injekt.api.get
@ -23,11 +31,16 @@ object SettingsBrowseScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
val mangaReposCount by sourcePreferences.mangaExtensionRepos().collectAsState()
val animeReposCount by sourcePreferences.animeExtensionRepos().collectAsState()
return listOf(
Preference.PreferenceGroup(
title = stringResource(MR.strings.label_sources),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.hideInAnimeLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_anime_library_items),
@ -36,11 +49,33 @@ object SettingsBrowseScreen : SearchableSettings {
pref = sourcePreferences.hideInMangaLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_manga_library_items),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.label_anime_extension_repos),
subtitle = pluralStringResource(
MR.plurals.num_repos,
animeReposCount.size,
animeReposCount.size,
),
onClick = {
navigator.push(AnimeExtensionReposScreen())
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.label_manga_extension_repos),
subtitle = pluralStringResource(
MR.plurals.num_repos,
mangaReposCount.size,
mangaReposCount.size,
),
onClick = {
navigator.push(MangaExtensionReposScreen())
},
),
),
),
Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_nsfw_content),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.showNsfwSource(),
title = stringResource(MR.strings.pref_show_nsfw_source),

View file

@ -4,63 +4,55 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.text.format.Formatter
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.hippo.unifile.UniFile
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
import eu.kanade.presentation.more.settings.screen.data.StorageInfo
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.EpisodeCache
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import logcat.LogPriority
import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.displayablePath
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.backup.service.FLAG_CATEGORIES
import tachiyomi.domain.backup.service.FLAG_CHAPTERS
import tachiyomi.domain.backup.service.FLAG_EXTENSIONS
import tachiyomi.domain.backup.service.FLAG_EXT_SETTINGS
import tachiyomi.domain.backup.service.FLAG_HISTORY
import tachiyomi.domain.backup.service.FLAG_SETTINGS
import tachiyomi.domain.backup.service.FLAG_TRACK
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR
@ -71,21 +63,35 @@ import uy.kohesive.injekt.api.get
object SettingsDataScreen : SearchableSettings {
val restorePreferenceKeyString = MR.strings.label_backup
const val HELP_URL = "https://aniyomi.org/docs/faq/storage"
@ReadOnlyComposable
@Composable
override fun getTitleRes() = MR.strings.label_data_storage
@Composable
override fun RowScope.AppBarAction() {
val uriHandler = LocalUriHandler.current
IconButton(onClick = { uriHandler.openUri(HELP_URL) }) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
contentDescription = stringResource(MR.strings.tracking_guide),
)
}
}
@Composable
override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
val storagePreferences = Injekt.get<StoragePreferences>()
return listOf(
return persistentListOf(
getStorageLocationPref(storagePreferences = storagePreferences),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(backupPreferences = backupPreferences),
getDataGroup(),
)
}
@ -107,8 +113,6 @@ object SettingsDataScreen : SearchableSettings {
UniFile.fromUri(context, uri)?.let {
storageDirPref.set(it.uri.toString())
}
Injekt.get<AnimeDownloadCache>().invalidateCache()
Injekt.get<MangaDownloadCache>().invalidateCache()
}
}
}
@ -120,13 +124,13 @@ object SettingsDataScreen : SearchableSettings {
val context = LocalContext.current
val storageDir by storageDirPref.collectAsState()
if (storageDir == storageDirPref.defaultValue()) {
if (!storageDirPref.isSet()) {
return stringResource(MR.strings.no_location_set)
}
return remember(storageDir) {
val file = UniFile.fromUri(context, storageDir.toUri())
file?.filePath ?: file?.uri?.toString()
file?.displayablePath
} ?: stringResource(MR.strings.invalid_location, storageDir)
}
@ -153,20 +157,75 @@ object SettingsDataScreen : SearchableSettings {
@Composable
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
}
},
) {
if (it == null) {
context.toast(MR.strings.file_null_uri_error)
return@rememberLauncherForActivityResult
}
navigator.push(RestoreBackupScreen(it.toString()))
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_backup),
preferenceItems = listOf(
preferenceItems = persistentListOf(
// Manual actions
getCreateBackupPref(),
getRestoreBackupPref(),
Preference.PreferenceItem.CustomPreference(
title = stringResource(restorePreferenceKeyString),
) {
BasePreferenceWidget(
subcomponent = {
MultiChoiceSegmentedButtonRow(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = PrefsHorizontalPadding),
) {
SegmentedButton(
checked = false,
onCheckedChange = { navigator.push(CreateBackupScreen()) },
shape = SegmentedButtonDefaults.itemShape(0, 2),
) {
Text(stringResource(MR.strings.pref_create_backup))
}
SegmentedButton(
checked = false,
onCheckedChange = {
if (!BackupRestoreJob.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(MR.strings.restore_miui_warning)
}
// no need to catch because it's wrapped with a chooser
chooseBackup.launch("*/*")
} else {
context.toast(MR.strings.restore_in_progress)
}
},
shape = SegmentedButtonDefaults.itemShape(1, 2),
) {
Text(stringResource(MR.strings.pref_restore_backup))
}
}
},
)
},
// Automatic backups
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.backupInterval(),
title = stringResource(MR.strings.pref_backup_interval),
entries = mapOf(
entries = persistentMapOf(
0 to stringResource(MR.strings.off),
6 to stringResource(MR.strings.update_6hour),
12 to stringResource(MR.strings.update_12hour),
@ -188,181 +247,44 @@ object SettingsDataScreen : SearchableSettings {
}
@Composable
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
val navigator = LocalNavigator.currentOrThrow
return Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_create_backup),
subtitle = stringResource(MR.strings.pref_create_backup_summ),
onClick = { navigator.push(CreateBackupScreen()) },
)
}
@Composable
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
private fun getDataGroup(): Preference.PreferenceGroup {
val context = LocalContext.current
var error by remember { mutableStateOf<Any?>(null) }
if (error != null) {
val onDismissRequest = { error = null }
when (val err = error) {
is InvalidRestore -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
dismissButton = {
TextButton(
onClick = {
context.copyToClipboard(err.message, err.message)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_copy_to_clipboard))
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_ok))
}
},
)
}
is MissingRestoreComponents -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
val msg = buildString {
append(stringResource(MR.strings.backup_restore_content_full))
if (err.sources.isNotEmpty()) {
append("\n\n").append(
stringResource(MR.strings.backup_restore_missing_sources),
)
err.sources.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
if (err.trackers.isNotEmpty()) {
append("\n\n").append(
stringResource(MR.strings.backup_restore_missing_trackers),
)
err.trackers.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
}
Text(text = msg)
}
},
confirmButton = {
TextButton(
onClick = {
BackupRestoreJob.start(context, err.uri)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_restore))
}
},
)
}
else -> error = null // Unknown
}
}
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
return Intent.createChooser(
intent,
context.stringResource(MR.strings.file_select_backup),
)
}
},
) {
if (it == null) {
context.stringResource(MR.strings.file_null_uri_error)
return@rememberLauncherForActivityResult
}
val results = try {
BackupFileValidator().validate(context, it)
} catch (e: Exception) {
error = InvalidRestore(it, e.message.toString())
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(context, it)
return@rememberLauncherForActivityResult
}
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
}
return Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_restore_backup),
subtitle = stringResource(MR.strings.pref_restore_backup_summ),
onClick = {
if (!BackupRestoreJob.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.stringResource(MR.strings.restore_miui_warning, Toast.LENGTH_LONG)
}
// no need to catch because it's wrapped with a chooser
chooseBackup.launch("*/*")
} else {
context.stringResource(MR.strings.restore_in_progress)
}
},
)
}
@Composable
private fun getDataGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val backupIntervalPref = backupPreferences.backupInterval()
val backupInterval by backupIntervalPref.collectAsState()
val chapterCache = remember { Injekt.get<ChapterCache>() }
val episodeCache = remember { Injekt.get<EpisodeCache>() }
var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
val cacheReadableMangaSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
val cacheReadableAnimeSize = remember(cacheReadableSizeSema) { episodeCache.readableSize }
val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_data),
preferenceItems = listOf(
getMangaStorageInfoPref(cacheReadableMangaSize),
getAnimeStorageInfoPref(cacheReadableAnimeSize),
title = stringResource(MR.strings.pref_storage_usage),
preferenceItems = persistentListOf(
Preference.PreferenceItem.CustomPreference(
title = stringResource(MR.strings.pref_storage_usage),
) {
BasePreferenceWidget(
subcomponent = {
StorageInfo(
modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
)
},
)
},
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_clear_chapter_cache),
subtitle = stringResource(
MR.strings.used_cache_both,
cacheReadableAnimeSize,
cacheReadableMangaSize,
),
subtitle = stringResource(MR.strings.used_cache, cacheReadableSize),
onClick = {
scope.launchNonCancellable {
try {
val deletedFiles = chapterCache.clear() + episodeCache.clear()
val deletedFiles = chapterCache.clear()
withUIContext {
context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles))
cacheReadableSizeSema++
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
withUIContext { context.stringResource(MR.strings.cache_delete_error) }
withUIContext { context.toast(MR.strings.cache_delete_error) }
}
}
},
@ -371,93 +293,7 @@ object SettingsDataScreen : SearchableSettings {
pref = libraryPreferences.autoClearItemCache(),
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
),
Preference.PreferenceItem.MultiSelectListPreference(
pref = backupPreferences.backupFlags(),
enabled = backupInterval != 0,
title = stringResource(MR.strings.pref_backup_flags),
subtitle = stringResource(MR.strings.pref_backup_flags_summary),
entries = mapOf(
FLAG_CATEGORIES to stringResource(MR.strings.general_categories),
FLAG_CHAPTERS to stringResource(MR.strings.chapters_episodes),
FLAG_HISTORY to stringResource(MR.strings.history),
FLAG_TRACK to stringResource(MR.strings.track),
FLAG_SETTINGS to stringResource(MR.strings.settings),
FLAG_EXT_SETTINGS to stringResource(MR.strings.extension_settings),
FLAG_EXTENSIONS to stringResource(MR.strings.label_extensions),
),
onValueChanged = {
if (FLAG_SETTINGS in it || FLAG_EXT_SETTINGS in it) {
context.stringResource(MR.strings.backup_settings_warning, Toast.LENGTH_LONG)
}
true
},
),
),
)
}
@Composable
fun getMangaStorageInfoPref(
chapterCacheReadableSize: String,
): Preference.PreferenceItem.CustomPreference {
val context = LocalContext.current
val available = remember {
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
}
val total = remember {
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
}
return Preference.PreferenceItem.CustomPreference(
title = stringResource(MR.strings.pref_manga_storage_usage),
) {
BasePreferenceWidget(
title = stringResource(MR.strings.pref_manga_storage_usage),
subcomponent = {
// TODO: downloads, SD cards, bar representation?, i18n
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
Text(text = "Available: $available / $total (chapter cache: $chapterCacheReadableSize)")
}
},
)
}
}
@Composable
fun getAnimeStorageInfoPref(
episodeCacheReadableSize: String,
): Preference.PreferenceItem.CustomPreference {
val context = LocalContext.current
val available = remember {
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
}
val total = remember {
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
}
return Preference.PreferenceItem.CustomPreference(
title = stringResource(MR.strings.pref_anime_storage_usage),
) {
BasePreferenceWidget(
title = stringResource(MR.strings.pref_anime_storage_usage),
subcomponent = {
// TODO: downloads, SD cards, bar representation?, i18n
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
Text(text = "Available: $available / $total (Episode cache: $episodeCacheReadableSize)")
}
},
)
}
}
}
private data class MissingRestoreComponents(
val uri: Uri,
val sources: List<String>,
val trackers: List<String>,
)
private data class InvalidRestore(
val uri: Uri? = null,
val message: String,
)

View file

@ -1,24 +1,42 @@
package eu.kanade.presentation.more.settings.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.util.fastMap
import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.OutlinedNumericChooser
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
@ -33,23 +51,59 @@ object SettingsDownloadScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetMangaCategories>() }
val allCategories by getCategories.subscribe().collectAsState(
initial = runBlocking { getCategories.await() },
val getMangaCategories = remember { Injekt.get<GetMangaCategories>() }
val allMangaCategories by getMangaCategories.subscribe().collectAsState(
initial = runBlocking { getMangaCategories.await() },
)
val getAnimeCategories = remember { Injekt.get<GetAnimeCategories>() }
val allAnimeCategories by getAnimeCategories.subscribe().collectAsState(
initial = runBlocking { getAnimeCategories.await() },
)
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
val basePreferences = remember { Injekt.get<BasePreferences>() }
val speedLimit by downloadPreferences.downloadSpeedLimit().collectAsState()
var currentSpeedLimit by remember { mutableIntStateOf(speedLimit) }
var showDownloadLimitDialog by rememberSaveable { mutableStateOf(false) }
if (showDownloadLimitDialog) {
DownloadLimitDialog(
initialValue = currentSpeedLimit,
onDismissRequest = { showDownloadLimitDialog = false },
onValueChanged = {
currentSpeedLimit = it
},
onConfirm = {
downloadPreferences.downloadSpeedLimit().set(currentSpeedLimit)
showDownloadLimitDialog = false
},
)
}
val multithreadingDownload by downloadPreferences.multithreadingDownload().collectAsState()
return listOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.downloadOnlyOverWifi(),
title = stringResource(MR.strings.connected_to_wifi),
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.multithreadingDownload(),
title = stringResource(MR.strings.multi_thread_download),
subtitle = stringResource(MR.strings.multi_thread_download_summary),
),
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.numberOfThreads(),
title = stringResource(MR.strings.multi_thread_download_threads_number),
subtitle = stringResource(MR.strings.multi_thread_download_threads_number_summary),
entries = (1..64).associateWith { it.toString() }.toImmutableMap(),
enabled = multithreadingDownload,
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.download_speed_limit),
subtitle = if (speedLimit == 0) {
stringResource(MR.strings.off)
} else {
"$speedLimit KiB/s"
},
onClick = { showDownloadLimitDialog = true },
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.saveChaptersAsCBZ(),
title = stringResource(MR.strings.save_chapter_as_cbz),
@ -62,17 +116,18 @@ object SettingsDownloadScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.numberOfDownloads(),
title = stringResource(MR.strings.pref_download_slots),
entries = (1..5).associateWith { it.toString() },
entries = (1..5).associateWith { it.toString() }.toImmutableMap(),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_slots_info)),
getDeleteChaptersGroup(
downloadPreferences = downloadPreferences,
categories = allCategories,
animeCategories = allAnimeCategories,
mangaCategories = allMangaCategories,
),
getAutoDownloadGroup(
downloadPreferences = downloadPreferences,
allCategories = allCategories,
allAnimeCategories = allAnimeCategories,
allMangaCategories = allMangaCategories,
),
getDownloadAheadGroup(downloadPreferences = downloadPreferences),
getExternalDownloaderGroup(
@ -85,11 +140,12 @@ object SettingsDownloadScreen : SearchableSettings {
@Composable
private fun getDeleteChaptersGroup(
downloadPreferences: DownloadPreferences,
categories: List<Category>,
animeCategories: List<Category>,
mangaCategories: List<Category>,
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_delete_chapters),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.removeAfterMarkedAsRead(),
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
@ -97,7 +153,7 @@ object SettingsDownloadScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.removeAfterReadSlots(),
title = stringResource(MR.strings.pref_remove_after_read),
entries = mapOf(
entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled),
0 to stringResource(MR.strings.last_read_chapter),
1 to stringResource(MR.strings.second_to_last),
@ -110,9 +166,13 @@ object SettingsDownloadScreen : SearchableSettings {
pref = downloadPreferences.removeBookmarkedChapters(),
title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
),
getExcludedAnimeCategoriesPreference(
downloadPreferences = downloadPreferences,
categories = { animeCategories },
),
getExcludedCategoriesPreference(
downloadPreferences = downloadPreferences,
categories = { categories },
categories = { mangaCategories },
),
),
)
@ -126,15 +186,31 @@ object SettingsDownloadScreen : SearchableSettings {
return Preference.PreferenceItem.MultiSelectListPreference(
pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(MR.strings.pref_remove_exclude_categories_manga),
entries = categories().associate { it.id.toString() to it.visualName },
entries = categories()
.associate { it.id.toString() to it.visualName }
.toImmutableMap(),
)
}
@Composable
private fun getExcludedAnimeCategoriesPreference(
downloadPreferences: DownloadPreferences,
categories: () -> List<Category>,
): Preference.PreferenceItem.MultiSelectListPreference {
return Preference.PreferenceItem.MultiSelectListPreference(
pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(MR.strings.pref_remove_exclude_categories_anime),
entries = categories()
.associate { it.id.toString() to it.visualName }
.toImmutableMap(),
)
}
@Composable
private fun getAutoDownloadGroup(
downloadPreferences: DownloadPreferences,
allCategories: List<Category>,
allAnimeCategories: List<Category>,
allMangaCategories: List<Category>,
): Preference.PreferenceGroup {
val downloadNewEpisodesPref = downloadPreferences.downloadNewEpisodes()
val downloadNewEpisodeCategoriesPref = downloadPreferences.downloadNewEpisodeCategories()
@ -179,9 +255,9 @@ object SettingsDownloadScreen : SearchableSettings {
TriStateListDialog(
title = stringResource(MR.strings.manga_categories),
message = stringResource(MR.strings.pref_download_new_categories_details),
items = allCategories,
initialChecked = included.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
initialInversed = excluded.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
items = allMangaCategories,
initialChecked = included.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } },
initialInversed = excluded.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } },
itemLabel = { it.visualName },
onDismissRequest = { showDialog = false },
onValueChanged = { newIncluded, newExcluded ->
@ -198,7 +274,7 @@ object SettingsDownloadScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_auto_download),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadNewEpisodesPref,
title = stringResource(MR.strings.pref_download_new_episodes),
@ -220,7 +296,7 @@ object SettingsDownloadScreen : SearchableSettings {
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.manga_categories),
subtitle = getCategoriesLabel(
allCategories = allCategories,
allCategories = allMangaCategories,
included = included,
excluded = excluded,
),
@ -237,36 +313,32 @@ object SettingsDownloadScreen : SearchableSettings {
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.download_ahead),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.autoDownloadWhileReading(),
title = stringResource(MR.strings.auto_download_while_reading),
entries = listOf(0, 2, 3, 5, 10).associateWith {
if (it == 0) {
stringResource(MR.strings.disabled)
} else {
pluralStringResource(
MR.plurals.next_unread_chapters,
count = it,
it,
)
}
},
),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.autoDownloadWhileWatching(),
title = stringResource(MR.strings.auto_download_while_watching),
entries = listOf(0, 2, 3, 5, 10).associateWith {
entries = listOf(0, 2, 3, 5, 10)
.associateWith {
if (it == 0) {
stringResource(MR.strings.disabled)
} else {
pluralStringResource(
MR.plurals.next_unseen_episodes,
count = it,
it,
)
pluralStringResource(MR.plurals.next_unseen_episodes, count = it, it)
}
},
}
.toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.autoDownloadWhileReading(),
title = stringResource(MR.strings.auto_download_while_reading),
entries = listOf(0, 2, 3, 5, 10)
.associateWith {
if (it == 0) {
stringResource(MR.strings.disabled)
} else {
pluralStringResource(MR.plurals.next_unread_chapters, count = it, it)
}
}
.toImmutableMap(),
),
Preference.PreferenceItem.InfoPreference(
stringResource(MR.strings.download_ahead_info),
@ -299,12 +371,11 @@ object SettingsDownloadScreen : SearchableSettings {
.map { pm.getApplicationLabel(it.applicationInfo).toString() }
val packageNamesMap: Map<String, String> =
packageNames.zip(packageNamesReadable)
.toMap()
mapOf("" to "None") + packageNames.zip(packageNamesReadable).toMap()
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_external_downloader),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = useExternalDownloader,
title = stringResource(MR.strings.pref_use_external_downloader),
@ -312,9 +383,58 @@ object SettingsDownloadScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = externalDownloaderPreference,
title = stringResource(MR.strings.pref_external_downloader_selection),
entries = mapOf("" to "None") + packageNamesMap,
entries = packageNamesMap.toPersistentMap(),
),
),
)
}
@Composable
private fun DownloadLimitDialog(
initialValue: Int,
onDismissRequest: () -> Unit,
onValueChanged: (newValue: Int) -> Unit,
onConfirm: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(stringResource(MR.strings.download_speed_limit)) },
text = {
Column {
Row(
modifier = Modifier
.padding(bottom = MaterialTheme.padding.medium)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
OutlinedNumericChooser(
label = stringResource(MR.strings.download_speed_limit),
placeholder = "0",
suffix = "KiB/s",
value = initialValue,
step = 100,
min = 0,
onValueChanged = onValueChanged,
)
}
Text(text = stringResource(MR.strings.download_speed_limit_hint))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
confirmButton = {
TextButton(
onClick = {
onConfirm()
},
) {
Text(text = stringResource(MR.strings.action_ok))
}
},
)
}
}

View file

@ -21,6 +21,9 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
import eu.kanade.tachiyomi.ui.category.CategoriesTab
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
@ -66,8 +69,8 @@ object SettingsLibraryScreen : SearchableSettings {
libraryPreferences,
),
getGlobalUpdateGroup(allCategories, allAnimeCategories, libraryPreferences),
getChapterSwipeActionsGroup(libraryPreferences),
getEpisodeSwipeActionsGroup(libraryPreferences),
getChapterSwipeActionsGroup(libraryPreferences),
)
}
@ -78,7 +81,6 @@ object SettingsLibraryScreen : SearchableSettings {
allAnimeCategories: List<Category>,
libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
val userAnimeCategoriesCount = allAnimeCategories.filterNot(Category::isSystemCategory).size
@ -96,13 +98,13 @@ object SettingsLibraryScreen : SearchableSettings {
allAnimeCategories.fastMap { it.id.toInt() }
val mangaLabels = listOf(stringResource(MR.strings.default_category_summary)) +
allCategories.fastMap { it.visualName(context) }
allCategories.fastMap { it.visualName }
val animeLabels = listOf(stringResource(MR.strings.default_category_summary)) +
allAnimeCategories.fastMap { it.visualName(context) }
allAnimeCategories.fastMap { it.visualName }
return Preference.PreferenceGroup(
title = stringResource(MR.strings.general_categories),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.action_edit_anime_categories),
subtitle = pluralStringResource(
@ -117,7 +119,7 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(MR.strings.default_anime_category),
subtitle = selectedAnimeCategory?.visualName
?: stringResource(MR.strings.default_category_summary),
entries = animeIds.zip(animeLabels).toMap(),
entries = animeIds.zip(animeLabels).toMap().toImmutableMap(),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.action_edit_manga_categories),
@ -131,9 +133,8 @@ object SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.defaultMangaCategory(),
title = stringResource(MR.strings.default_manga_category),
subtitle = selectedCategory?.visualName
?: stringResource(MR.strings.default_category_summary),
entries = mangaIds.zip(mangaLabels).toMap(),
subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
entries = mangaIds.zip(mangaLabels).toMap().toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.categorizedDisplaySettings(),
@ -222,11 +223,11 @@ object SettingsLibraryScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_library_update),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = autoUpdateIntervalPref,
title = stringResource(MR.strings.pref_library_update_interval),
entries = mapOf(
entries = persistentMapOf(
0 to stringResource(MR.strings.update_never),
12 to stringResource(MR.strings.update_12hour),
24 to stringResource(MR.strings.update_24hour),
@ -245,7 +246,7 @@ object SettingsLibraryScreen : SearchableSettings {
enabled = autoUpdateInterval > 0,
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
entries = mapOf(
entries = persistentMapOf(
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
DEVICE_CHARGING to stringResource(MR.strings.charging),
@ -284,18 +285,12 @@ object SettingsLibraryScreen : SearchableSettings {
),
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateItemRestrictions(),
title = stringResource(MR.strings.pref_library_update_manga_restriction),
entries = mapOf(
ENTRY_HAS_UNVIEWED to stringResource(
MR.strings.pref_update_only_completely_read,
),
title = stringResource(MR.strings.pref_library_update_smart_update),
entries = persistentMapOf(
ENTRY_HAS_UNVIEWED to stringResource(MR.strings.pref_update_only_completely_read),
ENTRY_NON_VIEWED to stringResource(MR.strings.pref_update_only_started),
ENTRY_NON_COMPLETED to stringResource(
MR.strings.pref_update_only_non_completed,
),
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(
MR.strings.pref_update_only_in_release_period,
),
ENTRY_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
),
),
Preference.PreferenceItem.SwitchPreference(
@ -312,41 +307,33 @@ object SettingsLibraryScreen : SearchableSettings {
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_chapter_swipe),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeChapterStartAction(),
title = stringResource(MR.strings.pref_chapter_swipe_start),
entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(
MR.strings.action_disable,
),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(
MR.strings.action_bookmark,
),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(
MR.strings.action_mark_as_read,
),
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
MR.strings.action_download,
),
entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
stringResource(MR.strings.action_bookmark),
LibraryPreferences.ChapterSwipeAction.ToggleRead to
stringResource(MR.strings.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download),
),
),
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeChapterEndAction(),
title = stringResource(MR.strings.pref_chapter_swipe_end),
entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(
MR.strings.action_disable,
),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(
MR.strings.action_bookmark,
),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(
MR.strings.action_mark_as_read,
),
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
MR.strings.action_download,
),
entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
stringResource(MR.strings.action_bookmark),
LibraryPreferences.ChapterSwipeAction.ToggleRead to
stringResource(MR.strings.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download),
),
),
),
@ -359,41 +346,33 @@ object SettingsLibraryScreen : SearchableSettings {
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_episode_swipe),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeEpisodeStartAction(),
title = stringResource(MR.strings.pref_episode_swipe_start),
entries = mapOf(
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(
MR.strings.action_disable,
),
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(
MR.strings.action_bookmark_episode,
),
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource(
MR.strings.action_mark_as_seen,
),
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
MR.strings.action_download,
),
entries = persistentMapOf(
LibraryPreferences.EpisodeSwipeAction.Disabled to
stringResource(MR.strings.disabled),
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
stringResource(MR.strings.action_bookmark_episode),
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
stringResource(MR.strings.action_mark_as_seen),
LibraryPreferences.EpisodeSwipeAction.Download to
stringResource(MR.strings.action_download),
),
),
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeEpisodeEndAction(),
title = stringResource(MR.strings.pref_episode_swipe_end),
entries = mapOf(
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(
MR.strings.action_disable,
),
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(
MR.strings.action_bookmark_episode,
),
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource(
MR.strings.action_mark_as_seen,
),
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
MR.strings.action_download,
),
entries = persistentMapOf(
LibraryPreferences.EpisodeSwipeAction.Disabled to
stringResource(MR.strings.disabled),
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
stringResource(MR.strings.action_bookmark_episode),
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
stringResource(MR.strings.action_mark_as_seen),
LibraryPreferences.EpisodeSwipeAction.Download to
stringResource(MR.strings.action_download),
),
),
),

View file

@ -186,18 +186,18 @@ object SettingsMainScreen : Screen() {
icon = Icons.Outlined.CollectionsBookmark,
screen = SettingsLibraryScreen,
),
Item(
titleRes = MR.strings.pref_category_reader,
subtitleRes = MR.strings.pref_reader_summary,
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen,
),
Item(
titleRes = MR.strings.pref_category_player,
subtitleRes = MR.strings.pref_player_summary,
icon = Icons.Outlined.PlayCircleOutline,
screen = SettingsPlayerScreen,
),
Item(
titleRes = MR.strings.pref_category_reader,
subtitleRes = MR.strings.pref_reader_summary,
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen,
),
Item(
titleRes = MR.strings.pref_category_downloads,
subtitleRes = MR.strings.pref_downloads_summary,

View file

@ -33,7 +33,10 @@ import eu.kanade.tachiyomi.ui.player.VLC_PLAYER
import eu.kanade.tachiyomi.ui.player.WEB_VIDEO_CASTER
import eu.kanade.tachiyomi.ui.player.X_PLAYER
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.i18n.stringResource
@ -57,7 +60,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = playerPreferences.progressPreference(),
title = stringResource(MR.strings.pref_progress_mark_as_seen),
entries = mapOf(
entries = persistentMapOf(
1.00F to stringResource(MR.strings.pref_progress_100),
0.95F to stringResource(MR.strings.pref_progress_95),
0.90F to stringResource(MR.strings.pref_progress_90),
@ -91,7 +94,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_internal_player),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = playerFullscreen,
title = stringResource(MR.strings.pref_player_fullscreen),
@ -118,7 +121,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_volume_brightness),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = enableVolumeBrightnessGestures,
title = stringResource(MR.strings.enable_volume_brightness_gestures),
@ -144,11 +147,11 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_player_orientation),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = defaultPlayerOrientationType,
title = stringResource(MR.strings.pref_default_player_orientation),
entries = mapOf(
entries = persistentMapOf(
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR to stringResource(
MR.strings.rotation_free,
),
@ -179,7 +182,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = defaultPlayerOrientationPortrait,
title = stringResource(MR.strings.pref_default_portrait_orientation),
entries = mapOf(
entries = persistentMapOf(
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT to stringResource(
MR.strings.rotation_portrait,
),
@ -194,7 +197,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = defaultPlayerOrientationLandscape,
title = stringResource(MR.strings.pref_default_landscape_orientation),
entries = mapOf(
entries = persistentMapOf(
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE to stringResource(
MR.strings.rotation_landscape,
),
@ -241,7 +244,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_player_seeking),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = enableHorizontalSeekGesture,
title = stringResource(MR.strings.enable_horizontal_seek_gesture),
@ -254,7 +257,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = skipLengthPreference,
title = stringResource(MR.strings.pref_skip_length),
entries = mapOf(
entries = persistentMapOf(
30 to stringResource(MR.strings.pref_skip_30),
20 to stringResource(MR.strings.pref_skip_20),
10 to stringResource(MR.strings.pref_skip_10),
@ -293,7 +296,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = waitingTimeAniSkip,
title = stringResource(MR.strings.pref_waiting_time_aniskip),
entries = mapOf(
entries = persistentMapOf(
5 to stringResource(MR.strings.pref_waiting_time_aniskip_5),
6 to stringResource(MR.strings.pref_waiting_time_aniskip_6),
7 to stringResource(MR.strings.pref_waiting_time_aniskip_7),
@ -318,7 +321,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_pip),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = enablePip,
title = stringResource(MR.strings.pref_enable_pip),
@ -364,7 +367,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_external_player),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = alwaysUseExternalPlayer,
title = stringResource(MR.strings.pref_always_use_external_player),
@ -372,7 +375,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = externalPlayerPreference,
title = stringResource(MR.strings.pref_external_player_preference),
entries = mapOf("" to "None") + packageNamesMap,
entries = (mapOf("" to "None") + packageNamesMap).toPersistentMap(),
),
),
)

View file

@ -10,6 +10,9 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
@ -31,12 +34,13 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPref.defaultReadingMode(),
title = stringResource(MR.strings.pref_viewer_type),
entries = ReadingMode.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) },
.associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPref.doubleTapAnimSpeed(),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
entries = mapOf(
entries = persistentMapOf(
1 to stringResource(MR.strings.double_tap_anim_speed_0),
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
@ -82,17 +86,18 @@ object SettingsReaderScreen : SearchableSettings {
val fullscreen by fullscreenPref.collectAsState()
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_display),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.defaultOrientationType(),
title = stringResource(MR.strings.pref_rotation_type),
entries = ReaderOrientation.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) },
.associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerTheme(),
title = stringResource(MR.strings.pref_reader_theme),
entries = mapOf(
entries = persistentMapOf(
1 to stringResource(MR.strings.black_background),
2 to stringResource(MR.strings.gray_background),
0 to stringResource(MR.strings.white_background),
@ -126,7 +131,7 @@ object SettingsReaderScreen : SearchableSettings {
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_reading),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipRead(),
title = stringResource(MR.strings.pref_skip_read_chapters),
@ -165,29 +170,26 @@ object SettingsReaderScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pager_viewer),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) }
.toMap(),
.toMap()
.toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pagerNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = mapOf(
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none),
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(
MR.strings.tapping_inverted_horizontal,
),
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(
MR.strings.tapping_inverted_vertical,
),
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
MR.strings.tapping_inverted_both,
),
),
entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
ReaderPreferences.TappingInvertMode.VERTICAL,
ReaderPreferences.TappingInvertMode.BOTH,
)
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
enabled = navMode != 5,
),
Preference.PreferenceItem.ListPreference(
@ -195,14 +197,16 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_image_scale_type),
entries = ReaderPreferences.ImageScaleType
.mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap(),
.toMap()
.toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.zoomStart(),
title = stringResource(MR.strings.pref_zoom_start),
entries = ReaderPreferences.ZoomStart
.mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap(),
.toMap()
.toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBorders(),
@ -265,29 +269,26 @@ object SettingsReaderScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.webtoon_viewer),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) }
.toMap(),
.toMap()
.toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = mapOf(
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none),
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(
MR.strings.tapping_inverted_horizontal,
),
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(
MR.strings.tapping_inverted_vertical,
),
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
MR.strings.tapping_inverted_both,
),
),
entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
ReaderPreferences.TappingInvertMode.VERTICAL,
ReaderPreferences.TappingInvertMode.BOTH,
)
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
enabled = navMode != 5,
),
Preference.PreferenceItem.SliderPreference(
@ -304,19 +305,11 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerHideThreshold(),
title = stringResource(MR.strings.pref_hide_threshold),
entries = mapOf(
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(
MR.strings.pref_highest,
),
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(
MR.strings.pref_high,
),
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(
MR.strings.pref_low,
),
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(
MR.strings.pref_lowest,
),
entries = persistentMapOf(
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
),
),
Preference.PreferenceItem.SwitchPreference(
@ -365,7 +358,7 @@ object SettingsReaderScreen : SearchableSettings {
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_reader_navigation),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readWithVolumeKeysPref,
title = stringResource(MR.strings.pref_read_with_volume_keys),
@ -383,7 +376,7 @@ object SettingsReaderScreen : SearchableSettings {
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_reader_actions),
preferenceItems = listOf(
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readWithLongTap(),
title = stringResource(MR.strings.pref_read_with_long_tap),

Some files were not shown because too many files have changed in this diff Show more