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 acknowledge that:
- I have updated: - 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 - All extensions
- I have gone through the FAQ (https://aniyomi.org/docs/faq/general) and troubleshooting guide (https://aniyomi.org/docs/guides/troubleshooting/) - 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 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 - I will fill out the title and the information in this template

View file

@ -2,10 +2,13 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: ⚠️ Anime extension/source issue - name: ⚠️ Anime extension/source issue
url: https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose 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 - name: 📦 Aniyomi extensions
url: https://aniyomi.org/extensions/ url: https://aniyomi.org/extensions/
about: Anime extensions and sources about: Anime extensions and sources
- name: 🧑‍💻 Aniyomi help discord - name: 🧑‍💻 Aniyomi help discord
url: https://discord.gg/F32UjdJZrR url: https://discord.gg/F32UjdJZrR
about: Common questions are answered here 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 label: Aniyomi version
description: You can find your Aniyomi version in **More → About**. description: You can find your Aniyomi version in **More → About**.
placeholder: | placeholder: |
Example: "0.12.3.10" Example: "0.15.2.4"
validations: validations:
required: true required: true
@ -93,11 +93,13 @@ body:
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true 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 required: true
- label: I have gone through the [FAQ](https://aniyomi.org/docs/faq/general) and [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/). - label: I have gone through the [FAQ](https://aniyomi.org/docs/faq/general) and [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/).
required: true 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 required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true

View file

@ -30,9 +30,9 @@ body:
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true 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 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 required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View file

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

View file

@ -66,6 +66,8 @@ jobs:
alias: ${{ secrets.ALIAS }} alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "34.0.0"
- name: Clean up build artifacts - name: Clean up build artifacts
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'aniyomiorg/aniyomi' 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) | | [![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 # ![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
Features include: Features include:
* Watching anime from [a variety of sources](https://github.com/aniyomiorg/aniyomi-extensions) * Watching videos
* Everything you know and love about Tachiyomi: * View images
* Online reading from a variety of sources * Local reading/watching of downloaded content
* Local reading of downloaded content
* A configurable reader with multiple viewers, reading directions and other settings. * 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/) * 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 * Categories to organize your library
* Light and dark themes * Light and dark themes
* Schedule updating your library for new chapters * Create backups locally to read/watch offline or to your desired cloud service
* Create backups locally to read offline or to your desired cloud service
## Download ## Download
Get the app from the [releases page](https://github.com/aniyomiorg/aniyomi/releases). Get the app from the [releases page](https://github.com/aniyomiorg/aniyomi/releases).

View file

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

View file

@ -50,7 +50,7 @@
##---------------Begin: proguard configuration for kotlinx.serialization ---------- ##---------------Begin: proguard configuration for kotlinx.serialization ----------
-keepattributes *Annotation*, InnerClasses -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 # kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** { -keepclassmembers class kotlinx.serialization.json.** {

View file

@ -8,7 +8,9 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Storage --> <!-- 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 --> <!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <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.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<!-- To view extension packages in API 30+ --> <!-- 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.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" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
@ -45,13 +51,64 @@
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/Theme.Tachiyomi.SplashScreen" android:theme="@style/Theme.Tachiyomi.SplashScreen">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </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 --> <!--suppress AndroidDomInspection -->
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
@ -59,17 +116,17 @@
</activity> </activity>
<activity <activity
android:process=":error_handler"
android:name=".crash.CrashActivity" android:name=".crash.CrashActivity"
android:exported="false" /> android:exported="false"
android:process=":error_handler" />
<activity <activity
android:name=".ui.deeplink.anime.DeepLinkAnimeActivity" android:name=".ui.deeplink.anime.DeepLinkAnimeActivity"
android:launchMode="singleTask" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_global_anime_search" android:label="@string/action_global_anime_search"
android:exported="true"> android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" /> <action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -93,10 +150,10 @@
<activity <activity
android:name=".ui.deeplink.manga.DeepLinkMangaActivity" android:name=".ui.deeplink.manga.DeepLinkMangaActivity"
android:launchMode="singleTask" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_global_manga_search" android:label="@string/action_global_manga_search"
android:exported="true"> android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" /> <action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -124,13 +181,14 @@
<activity <activity
android:name=".ui.reader.ReaderActivity" android:name=".ui.reader.ReaderActivity"
android:launchMode="singleTask" android:exported="false"
android:exported="false"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" /> <action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter> </intent-filter>
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION" <meta-data
android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions" /> android:resource="@xml/s_pen_actions" />
</activity> </activity>
<activity <activity
@ -151,8 +209,8 @@
</activity> </activity>
<activity <activity
android:name=".ui.security.UnlockActivity" android:name=".ui.security.UnlockActivity"
android:theme="@style/Theme.Tachiyomi" android:exported="false"
android:exported="false" /> android:theme="@style/Theme.Tachiyomi" />
<activity <activity
android:name=".ui.webview.WebViewActivity" android:name=".ui.webview.WebViewActivity"
@ -161,39 +219,31 @@
<activity <activity
android:name=".extension.manga.util.MangaExtensionInstallActivity" 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 <activity
android:name=".extension.anime.util.AnimeExtensionInstallActivity" 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 <activity
android:name=".ui.setting.track.TrackLoginActivity" 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> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="aniyomi"/>
<data android:host="myanimelist-auth" />
<data android:host="anilist-auth" /> <data android:host="anilist-auth" />
<data android:host="bangumi-auth" /> <data android:host="bangumi-auth" />
<data android:host="myanimelist-auth"/>
<data android:host="shikimori-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:host="simkl-auth"/>
<data android:scheme="aniyomi"/>
</intent-filter> </intent-filter>
</activity> </activity>
@ -201,17 +251,15 @@
android:name=".data.notification.NotificationReceiver" android:name=".data.notification.NotificationReceiver"
android:exported="false" /> android:exported="false" />
<service
android:name=".extension.util.ExtensionInstallService"
android:exported="false" />
<service <service
android:name=".extension.manga.util.MangaExtensionInstallService" android:name=".extension.manga.util.MangaExtensionInstallService"
android:exported="false" /> android:exported="false"
android:foregroundServiceType="shortService" />
<service <service
android:name=".extension.anime.util.AnimeExtensionInstallService" android:name=".extension.anime.util.AnimeExtensionInstallService"
android:exported="false" /> android:exported="false"
android:foregroundServiceType="shortService" />
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false" android:enabled="false"
@ -239,9 +287,9 @@
<provider <provider
android:name="rikka.shizuku.ShizukuProvider" android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku" android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true" android:enabled="true"
android:exported="true" android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" /> android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data <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.SetExcludedScanlators
import eu.kanade.domain.entries.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.entries.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.entries.manga.interactor.UpdateManga 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.GetAnimeExtensionLanguages
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionRepos
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionSources import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionSources
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionsByType 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.GetExtensionSources
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionLanguages 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.GetMangaExtensionsByType
import eu.kanade.domain.extension.manga.interactor.TrustMangaExtension
import eu.kanade.domain.items.chapter.interactor.GetAvailableScanlators import eu.kanade.domain.items.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.items.chapter.interactor.SetReadStatus import eu.kanade.domain.items.chapter.interactor.SetReadStatus
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource 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.category.manga.repository.MangaCategoryRepository
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
import tachiyomi.domain.entries.anime.interactor.GetAnime 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.GetAnimeFavorites
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime 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.NetworkToLocalAnime
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
import tachiyomi.domain.entries.anime.repository.AnimeRepository 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.GetDuplicateLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetManga 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.GetMangaFavorites
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
@ -322,5 +330,14 @@ class DomainModule : InjektModule {
addFactory { ToggleLanguage(get()) } addFactory { ToggleLanguage(get()) }
addFactory { ToggleMangaSource(get()) } addFactory { ToggleMangaSource(get()) }
addFactory { ToggleMangaSourcePin(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) fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
enum class ExtensionInstaller(val titleRes: StringResource) { enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
LEGACY(MR.strings.ext_installer_legacy), LEGACY(MR.strings.ext_installer_legacy, true),
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller), PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
SHIZUKU(MR.strings.ext_installer_shizuku), SHIZUKU(MR.strings.ext_installer_shizuku, false),
PRIVATE(MR.strings.ext_installer_private), PRIVATE(MR.strings.ext_installer_private, false),
} }
} }

View file

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

View file

@ -81,9 +81,9 @@ class UpdateManga(
dateTime: ZonedDateTime = ZonedDateTime.now(), dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = mangaFetchInterval.getWindow(dateTime), window: Pair<Long, Long> = mangaFetchInterval.getWindow(dateTime),
): Boolean { ): Boolean {
return mangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window) return mangaRepository.updateManga(
?.let { mangaRepository.updateManga(it) } mangaFetchInterval.toMangaUpdate(manga, dateTime, window),
?: false )
} }
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { 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.domain.items.chapter.service.ChapterRecognition
import tachiyomi.source.local.entries.manga.isLocal import tachiyomi.source.local.entries.manga.isLocal
import java.lang.Long.max import java.lang.Long.max
import java.time.Instant
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.TreeSet import java.util.TreeSet
@ -57,6 +56,7 @@ class SyncChaptersWithSource(
} }
val now = ZonedDateTime.now() val now = ZonedDateTime.now()
val nowMillis = now.toInstant().toEpochMilli()
val sourceChapters = rawSourceChapters val sourceChapters = rawSourceChapters
.distinctBy { it.url } .distinctBy { it.url }
@ -67,36 +67,27 @@ class SyncChaptersWithSource(
.copy(mangaId = manga.id, sourceOrder = i.toLong()) .copy(mangaId = manga.id, sourceOrder = i.toLong())
} }
// Chapters from db.
val dbChapters = getChaptersByMangaId.await(manga.id) val dbChapters = getChaptersByMangaId.await(manga.id)
// Chapters from the source not in db. val newChapters = mutableListOf<Chapter>()
val toAdd = mutableListOf<Chapter>() val updatedChapters = mutableListOf<Chapter>()
val removedChapters = dbChapters.filterNot { dbChapter ->
// Chapters whose metadata have changed.
val toChange = mutableListOf<Chapter>()
// Chapters from the db not in source.
val toDelete = dbChapters.filterNot { dbChapter ->
sourceChapters.any { sourceChapter -> sourceChapters.any { sourceChapter ->
dbChapter.url == sourceChapter.url dbChapter.url == sourceChapter.url
} }
} }
val rightNow = Instant.now().toEpochMilli()
// Used to not set upload date of older chapters // Used to not set upload date of older chapters
// to a higher value than newer chapters // to a higher value than newer chapters
var maxSeenUploadDate = 0L var maxSeenUploadDate = 0L
val sManga = manga.toSManga()
for (sourceChapter in sourceChapters) { for (sourceChapter in sourceChapters) {
var chapter = sourceChapter var chapter = sourceChapter
// Update metadata from source if necessary. // Update metadata from source if necessary.
if (source is HttpSource) { if (source is HttpSource) {
val sChapter = chapter.toSChapter() val sChapter = chapter.toSChapter()
source.prepareNewChapter(sChapter, sManga) source.prepareNewChapter(sChapter, manga.toSManga())
chapter = chapter.copyFromSChapter(sChapter) chapter = chapter.copyFromSChapter(sChapter)
} }
@ -112,13 +103,13 @@ class SyncChaptersWithSource(
if (dbChapter == null) { if (dbChapter == null) {
val toAddChapter = if (chapter.dateUpload == 0L) { 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) chapter.copy(dateUpload = altDateUpload)
} else { } else {
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload) maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
chapter chapter
} }
toAdd.add(toAddChapter) newChapters.add(toAddChapter)
} else { } else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) { if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged( val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(
@ -144,13 +135,13 @@ class SyncChaptersWithSource(
if (chapter.dateUpload != 0L) { if (chapter.dateUpload != 0L) {
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload) 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. // Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) { if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval( updateManga.awaitUpdateFetchInterval(
manga, manga,
@ -167,20 +158,20 @@ class SyncChaptersWithSource(
val deletedReadChapterNumbers = TreeSet<Double>() val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>() val deletedBookmarkedChapterNumbers = TreeSet<Double>()
toDelete.forEach { chapter -> removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber) if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
deletedChapterNumbers.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 } .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 // 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. // Sources MUST return the chapters from most to less recent, which is common.
var itemCount = toAdd.size var itemCount = newChapters.size
var updatedToAdd = toAdd.map { toAddItem -> var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--) var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
@ -199,8 +190,8 @@ class SyncChaptersWithSource(
chapter chapter
} }
if (toDelete.isNotEmpty()) { if (removedChapters.isNotEmpty()) {
val toDeleteIds = toDelete.map { it.id } val toDeleteIds = removedChapters.map { it.id }
chapterRepository.removeChaptersWithIds(toDeleteIds) chapterRepository.removeChaptersWithIds(toDeleteIds)
} }
@ -208,8 +199,8 @@ class SyncChaptersWithSource(
updatedToAdd = chapterRepository.addAllChapters(updatedToAdd) updatedToAdd = chapterRepository.addAllChapters(updatedToAdd)
} }
if (toChange.isNotEmpty()) { if (updatedChapters.isNotEmpty()) {
val chapterUpdates = toChange.map { it.toChapterUpdate() } val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates) updateChapter.awaitAll(chapterUpdates)
} }
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow) 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.domain.items.episode.service.EpisodeRecognition
import tachiyomi.source.local.entries.anime.isLocal import tachiyomi.source.local.entries.anime.isLocal
import java.lang.Long.max import java.lang.Long.max
import java.time.Instant
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.TreeSet import java.util.TreeSet
@ -55,6 +54,7 @@ class SyncEpisodesWithSource(
} }
val now = ZonedDateTime.now() val now = ZonedDateTime.now()
val nowMillis = now.toInstant().toEpochMilli()
val sourceEpisodes = rawSourceEpisodes val sourceEpisodes = rawSourceEpisodes
.distinctBy { it.url } .distinctBy { it.url }
@ -65,36 +65,27 @@ class SyncEpisodesWithSource(
.copy(animeId = anime.id, sourceOrder = i.toLong()) .copy(animeId = anime.id, sourceOrder = i.toLong())
} }
// Episodes from db.
val dbEpisodes = getEpisodesByAnimeId.await(anime.id) val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
// Episodes from the source not in db. val newEpisodes = mutableListOf<Episode>()
val toAdd = mutableListOf<Episode>() val updatedEpisodes = mutableListOf<Episode>()
val removedEpisodes = dbEpisodes.filterNot { dbEpisode ->
// Episodes whose metadata have changed.
val toChange = mutableListOf<Episode>()
// Episodes from the db not in source.
val toDelete = dbEpisodes.filterNot { dbEpisode ->
sourceEpisodes.any { sourceEpisode -> sourceEpisodes.any { sourceEpisode ->
dbEpisode.url == sourceEpisode.url dbEpisode.url == sourceEpisode.url
} }
} }
val rightNow = Instant.now().toEpochMilli()
// Used to not set upload date of older episodes // Used to not set upload date of older episodes
// to a higher value than newer episodes // to a higher value than newer episodes
var maxSeenUploadDate = 0L var maxSeenUploadDate = 0L
val sAnime = anime.toSAnime()
for (sourceEpisode in sourceEpisodes) { for (sourceEpisode in sourceEpisodes) {
var episode = sourceEpisode var episode = sourceEpisode
// Update metadata from source if necessary. // Update metadata from source if necessary.
if (source is AnimeHttpSource) { if (source is AnimeHttpSource) {
val sEpisode = episode.toSEpisode() val sEpisode = episode.toSEpisode()
source.prepareNewEpisode(sEpisode, sAnime) source.prepareNewEpisode(sEpisode, anime.toSAnime())
episode = episode.copyFromSEpisode(sEpisode) episode = episode.copyFromSEpisode(sEpisode)
} }
@ -110,13 +101,13 @@ class SyncEpisodesWithSource(
if (dbEpisode == null) { if (dbEpisode == null) {
val toAddEpisode = if (episode.dateUpload == 0L) { 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) episode.copy(dateUpload = altDateUpload)
} else { } else {
maxSeenUploadDate = max(maxSeenUploadDate, sourceEpisode.dateUpload) maxSeenUploadDate = max(maxSeenUploadDate, sourceEpisode.dateUpload)
episode episode
} }
toAdd.add(toAddEpisode) newEpisodes.add(toAddEpisode)
} else { } else {
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) { if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged( val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(
@ -144,13 +135,13 @@ class SyncEpisodesWithSource(
dateUpload = sourceEpisode.dateUpload, dateUpload = sourceEpisode.dateUpload,
) )
} }
toChange.add(toChangeEpisode) updatedEpisodes.add(toChangeEpisode)
} }
} }
} }
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. // Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { if (newEpisodes.isEmpty() && removedEpisodes.isEmpty() && updatedEpisodes.isEmpty()) {
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) { if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
updateAnime.awaitUpdateFetchInterval( updateAnime.awaitUpdateFetchInterval(
anime, anime,
@ -167,20 +158,20 @@ class SyncEpisodesWithSource(
val deletedSeenEpisodeNumbers = TreeSet<Double>() val deletedSeenEpisodeNumbers = TreeSet<Double>()
val deletedBookmarkedEpisodeNumbers = TreeSet<Double>() val deletedBookmarkedEpisodeNumbers = TreeSet<Double>()
toDelete.forEach { episode -> removedEpisodes.forEach { episode ->
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber) if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
if (episode.bookmark) deletedBookmarkedEpisodeNumbers.add(episode.episodeNumber) if (episode.bookmark) deletedBookmarkedEpisodeNumbers.add(episode.episodeNumber)
deletedEpisodeNumbers.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 } .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 // 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. // Sources MUST return the episodes from most to less recent, which is common.
var itemCount = toAdd.size var itemCount = newEpisodes.size
var updatedToAdd = toAdd.map { toAddItem -> var updatedToAdd = newEpisodes.map { toAddItem ->
var episode = toAddItem.copy(dateFetch = rightNow + itemCount--) var episode = toAddItem.copy(dateFetch = nowMillis + itemCount--)
if (episode.isRecognizedNumber.not() || episode.episodeNumber !in deletedEpisodeNumbers) return@map episode if (episode.isRecognizedNumber.not() || episode.episodeNumber !in deletedEpisodeNumbers) return@map episode
@ -199,8 +190,8 @@ class SyncEpisodesWithSource(
episode episode
} }
if (toDelete.isNotEmpty()) { if (removedEpisodes.isNotEmpty()) {
val toDeleteIds = toDelete.map { it.id } val toDeleteIds = removedEpisodes.map { it.id }
episodeRepository.removeEpisodesWithIds(toDeleteIds) episodeRepository.removeEpisodesWithIds(toDeleteIds)
} }
@ -208,8 +199,8 @@ class SyncEpisodesWithSource(
updatedToAdd = episodeRepository.addAllEpisodes(updatedToAdd) updatedToAdd = episodeRepository.addAllEpisodes(updatedToAdd)
} }
if (toChange.isNotEmpty()) { if (updatedEpisodes.isNotEmpty()) {
val episodeUpdates = toChange.map { it.toEpisodeUpdate() } val episodeUpdates = updatedEpisodes.map { it.toEpisodeUpdate() }
updateEpisode.awaitAll(episodeUpdates) updateEpisode.awaitAll(episodeUpdates)
} }
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow) updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)

View file

@ -37,7 +37,14 @@ class SourcePreferences(
SetMigrateSorting.Direction.ASCENDING, 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 // Mixture Sources

View file

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

View file

@ -28,7 +28,7 @@ class TrackEpisode(
if (tracks.isEmpty()) return@withNonCancellableContext if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track -> tracks.mapNotNull { track ->
val service = trackerManager.get(track.syncId) val service = trackerManager.get(track.trackerId)
if (service == null || !service.isLoggedIn || episodeNumber <= track.lastEpisodeSeen) { if (service == null || !service.isLoggedIn || episodeNumber <= track.lastEpisodeSeen) {
return@mapNotNull null 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.id = id
it.anime_id = animeId it.anime_id = animeId
it.media_id = remoteId it.remote_id = remoteId
it.library_id = libraryId it.library_id = libraryId
it.title = title it.title = title
it.last_episode_seen = lastEpisodeSeen.toFloat() it.last_episode_seen = lastEpisodeSeen.toFloat()
@ -33,14 +33,16 @@ fun DbAnimeTrack.toDomainTrack(idRequired: Boolean = true): AnimeTrack? {
return AnimeTrack( return AnimeTrack(
id = trackId, id = trackId,
animeId = anime_id, animeId = anime_id,
syncId = sync_id.toLong(), trackerId = tracker_id.toLong(),
remoteId = media_id, remoteId = remote_id,
libraryId = library_id, libraryId = library_id,
title = title, title = title,
lastEpisodeSeen = last_episode_seen.toDouble(), lastEpisodeSeen = last_episode_seen.toDouble(),
totalEpisodes = total_episodes.toLong(), totalEpisodes = total_episodes.toLong(),
status = status.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, remoteUrl = tracking_url,
startDate = started_watching_date, startDate = started_watching_date,
finishDate = finished_watching_date, finishDate = finished_watching_date,

View file

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

View file

@ -27,7 +27,7 @@ class TrackChapter(
if (tracks.isEmpty()) return@withNonCancellableContext if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track -> 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) {
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) { if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
return@mapNotNull null 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.id = id
it.manga_id = mangaId it.manga_id = mangaId
it.media_id = remoteId it.remote_id = remoteId
it.library_id = libraryId it.library_id = libraryId
it.title = title it.title = title
it.last_chapter_read = lastChapterRead.toFloat() it.last_chapter_read = lastChapterRead.toFloat()
@ -33,14 +33,16 @@ fun DbMangaTrack.toDomainTrack(idRequired: Boolean = true): MangaTrack? {
return MangaTrack( return MangaTrack(
id = trackId, id = trackId,
mangaId = manga_id, mangaId = manga_id,
syncId = sync_id.toLong(), trackerId = tracker_id.toLong(),
remoteId = media_id, remoteId = remote_id,
libraryId = library_id, libraryId = library_id,
title = title, title = title,
lastChapterRead = last_chapter_read.toDouble(), lastChapterRead = last_chapter_read.toDouble(),
totalChapters = total_chapters.toLong(), totalChapters = total_chapters.toLong(),
status = status.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, remoteUrl = tracking_url,
startDate = started_reading_date, startDate = started_reading_date,
finishDate = finished_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.Tracker
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
class TrackPreferences( class TrackPreferences(
private val preferenceStore: PreferenceStore, 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) { fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean(
trackUsername(sync).set(username) Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"),
trackPassword(sync).set(password) 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) fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
@ -29,12 +42,4 @@ class TrackPreferences(
"show_next_episode_airing_time", "show_next_episode_airing_time",
true, 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 android.os.Build
import eu.kanade.domain.ui.model.AppTheme 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.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
@ -34,6 +36,10 @@ class UiPreferences(
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) 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 { companion object {
fun dateFormat(format: String): DateFormat = when (format) { fun dateFormat(format: String): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT) "" -> DateFormat.getDateInstance(DateFormat.SHORT)

View file

@ -15,6 +15,7 @@ enum class AppTheme(val titleRes: StringResource?) {
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk), MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
MOCHA(MR.strings.theme_mocha), MOCHA(MR.strings.theme_mocha),
SAPPHIRE(MR.strings.theme_sapphire), SAPPHIRE(MR.strings.theme_sapphire),
NORD(MR.strings.theme_nord),
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri), STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
TAKO(MR.strings.theme_tako), TAKO(MR.strings.theme_tako),
TEALTURQUOISE(MR.strings.theme_tealturquoise), 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 modifier = Modifier
.padding( .padding(
start = MaterialTheme.padding.medium, start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.tiny, end = MaterialTheme.padding.extraSmall,
) )
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick), .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.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@ -36,6 +35,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign 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.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeExtensionDetailsScreenModel import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeExtensionDetailsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
@ -65,14 +66,23 @@ fun AnimeExtensionDetailsScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
state: AnimeExtensionDetailsScreenModel.State, state: AnimeExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit,
onClickEnableAll: () -> Unit, onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit, onClickDisableAll: () -> Unit,
onClickClearCookies: () -> Unit, onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> 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( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
@ -82,19 +92,14 @@ fun AnimeExtensionDetailsScreen(
AppBarActions( AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder() actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply { .apply {
if (state.extension?.isUnofficial == false) { if (url != null) {
add( add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.whats_new), title = stringResource(MR.strings.action_open_repo),
icon = Icons.Outlined.History, icon = Icons.AutoMirrored.Outlined.Launch,
onClick = onClickWhatsNew, onClick = {
), uriHandler.openUri(url)
) },
add(
AppBar.Action(
title = stringResource(MR.strings.action_faq_and_guides),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onClickReadme,
), ),
) )
} }
@ -124,7 +129,7 @@ fun AnimeExtensionDetailsScreen(
) { paddingValues -> ) { paddingValues ->
if (state.extension == null) { if (state.extension == null) {
EmptyScreen( EmptyScreen(
stringRes = MR.strings.empty_screen, MR.strings.empty_screen,
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
) )
return@Scaffold return@Scaffold
@ -145,7 +150,7 @@ fun AnimeExtensionDetailsScreen(
private fun AnimeExtensionDetails( private fun AnimeExtensionDetails(
contentPadding: PaddingValues, contentPadding: PaddingValues,
extension: AnimeExtension.Installed, extension: AnimeExtension.Installed,
sources: List<AnimeExtensionSourceItem>, sources: ImmutableList<AnimeExtensionSourceItem>,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
@ -156,12 +161,7 @@ private fun AnimeExtensionDetails(
ScrollbarLazyColumn( ScrollbarLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
when { if (extension.isObsolete) {
extension.isUnofficial ->
item {
WarningBanner(MR.strings.unofficial_anime_extension_message)
}
extension.isObsolete ->
item { item {
WarningBanner(MR.strings.obsolete_extension_message) WarningBanner(MR.strings.obsolete_extension_message)
} }
@ -296,7 +296,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small, top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium, bottom = MaterialTheme.padding.medium,
), ),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) { ) {
OutlinedButton( OutlinedButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),

View file

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

View file

@ -74,9 +74,10 @@ internal fun GlobalSearchContent(
items.forEach { (source, result) -> items.forEach { (source, result) ->
item(key = source.id) { item(key = source.id) {
GlobalSearchResultItem( GlobalSearchResultItem(
title = fromSourceId title = fromSourceId?.let {
?.let { "${source.name}".takeIf { source.id == fromSourceId } } ?: source.name, "${source.name}".takeIf { source.id == fromSourceId }
subtitle = LocaleHelper.getDisplayName(source.lang), } ?: source.name,
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
onClick = { onClickSource(source) }, onClick = { onClickSource(source) },
) { ) {
when (result) { 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.presentation.browse.anime.components.BaseAnimeSourceItem
import eu.kanade.tachiyomi.ui.browse.anime.migration.sources.MigrateAnimeSourceScreenModel import eu.kanade.tachiyomi.ui.browse.anime.migration.sources.MigrateAnimeSourceScreenModel
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.source.anime.model.AnimeSource import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge import tachiyomi.presentation.core.components.Badge
@ -75,7 +76,7 @@ fun MigrateAnimeSourceScreen(
@Composable @Composable
private fun MigrateAnimeSourceList( private fun MigrateAnimeSourceList(
list: List<Pair<AnimeSource, Long>>, list: ImmutableList<Pair<AnimeSource, Long>>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickItem: (AnimeSource) -> Unit, onClickItem: (AnimeSource) -> Unit,
onLongClickItem: (AnimeSource) -> Unit, onLongClickItem: (AnimeSource) -> Unit,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,6 +27,8 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import eu.kanade.core.preference.asToggleableState import eu.kanade.core.preference.asToggleableState
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import tachiyomi.core.preference.CheckboxState import tachiyomi.core.preference.CheckboxState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@ -39,12 +41,12 @@ import kotlin.time.Duration.Companion.seconds
fun CategoryCreateDialog( fun CategoryCreateDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onCreate: (String) -> Unit, onCreate: (String) -> Unit,
categories: List<Category>, categories: ImmutableList<String>,
) { ) {
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.anyWithName(name) } val nameAlreadyExists = remember(name) { categories.contains(name) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -69,10 +71,13 @@ fun CategoryCreateDialog(
}, },
text = { text = {
OutlinedTextField( OutlinedTextField(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier
.focusRequester(focusRequester),
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text(text = stringResource(MR.strings.name)) }, label = {
Text(text = stringResource(MR.strings.name))
},
supportingText = { supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) { val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
MR.strings.error_category_exists MR.strings.error_category_exists
@ -98,14 +103,14 @@ fun CategoryCreateDialog(
fun CategoryRenameDialog( fun CategoryRenameDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onRename: (String) -> Unit, onRename: (String) -> Unit,
categories: List<Category>, categories: ImmutableList<String>,
category: Category, category: String,
) { ) {
var name by remember { mutableStateOf(category.name) } var name by remember { mutableStateOf(category) }
var valueHasChanged by remember { mutableStateOf(false) } var valueHasChanged by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.anyWithName(name) } val nameAlreadyExists = remember(name) { categories.contains(name) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -162,7 +167,7 @@ fun CategoryRenameDialog(
fun CategoryDeleteDialog( fun CategoryDeleteDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
category: Category, category: String,
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -183,7 +188,7 @@ fun CategoryDeleteDialog(
Text(text = stringResource(MR.strings.delete_category)) Text(text = stringResource(MR.strings.delete_category))
}, },
text = { 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 @Composable
fun ChangeCategoryDialog( fun ChangeCategoryDialog(
initialSelection: List<CheckboxState<Category>>, initialSelection: ImmutableList<CheckboxState<Category>>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onEditCategories: () -> Unit, onEditCategories: () -> Unit,
onConfirm: (List<Long>, List<Long>) -> Unit, onConfirm: (List<Long>, List<Long>) -> Unit,
@ -291,7 +296,7 @@ fun ChangeCategoryDialog(
if (index != -1) { if (index != -1) {
val mutableList = selection.toMutableList() val mutableList = selection.toMutableList()
mutableList[index] = it.next() mutableList[index] = it.next()
selection = mutableList.toList() selection = mutableList.toList().toImmutableList()
} }
} }
Row( 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.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -16,11 +17,13 @@ import tachiyomi.presentation.core.util.isScrollingUp
fun CategoryFloatingActionButton( fun CategoryFloatingActionButton(
lazyListState: LazyListState, lazyListState: LazyListState,
onCreate: () -> Unit, onCreate: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_add)) }, text = { Text(text = stringResource(MR.strings.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
onClick = onCreate, onClick = onCreate,
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(), expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
modifier = modifier,
) )
} }

View file

@ -52,7 +52,7 @@ fun CategoryListItem(
), ),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "") Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Text( Text(
text = category.name, text = category.name,
modifier = Modifier modifier = Modifier
@ -64,13 +64,13 @@ fun CategoryListItem(
onClick = { onMoveUp(category) }, onClick = { onMoveUp(category) },
enabled = canMoveUp, enabled = canMoveUp,
) { ) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "") Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
} }
IconButton( IconButton(
onClick = { onMoveDown(category) }, onClick = { onMoveDown(category) },
enabled = canMoveDown, enabled = canMoveDown,
) { ) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "") Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) { 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 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.ColumnScope
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.RadioButtonChecked import androidx.compose.material.icons.outlined.RadioButtonChecked
@ -22,12 +25,17 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
/**
* DropdownMenu but overlaps anchor and has width constraints to better
* match non-Compose implementation.
*/
@Composable @Composable
fun DropdownMenu( fun DropdownMenu(
expanded: Boolean, expanded: Boolean,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(8.dp, (-56).dp), offset: DpOffset = DpOffset(8.dp, (-56).dp),
scrollState: ScrollState = rememberScrollState(),
properties: PopupProperties = PopupProperties(focusable = true), properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
@ -36,6 +44,7 @@ fun DropdownMenu(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp), modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
offset = offset, offset = offset,
scrollState = scrollState,
properties = properties, properties = properties,
content = content, content = content,
) )
@ -45,6 +54,7 @@ fun DropdownMenu(
fun RadioMenuItem( fun RadioMenuItem(
text: @Composable () -> Unit, text: @Composable () -> Unit,
isChecked: Boolean, isChecked: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
DropdownMenuItem( DropdownMenuItem(
@ -64,6 +74,7 @@ fun RadioMenuItem(
) )
} }
}, },
modifier = modifier,
) )
} }
@ -71,10 +82,12 @@ fun RadioMenuItem(
fun NestedMenuItem( fun NestedMenuItem(
text: @Composable () -> Unit, text: @Composable () -> Unit,
children: @Composable ColumnScope.(() -> Unit) -> Unit, children: @Composable ColumnScope.(() -> Unit) -> Unit,
modifier: Modifier = Modifier,
) { ) {
var nestedExpanded by remember { mutableStateOf(false) } var nestedExpanded by remember { mutableStateOf(false) }
val closeMenu = { nestedExpanded = false } val closeMenu = { nestedExpanded = false }
Box {
DropdownMenuItem( DropdownMenuItem(
text = text, text = text,
onClick = { nestedExpanded = true }, onClick = { nestedExpanded = true },
@ -89,7 +102,9 @@ fun NestedMenuItem(
DropdownMenu( DropdownMenu(
expanded = nestedExpanded, expanded = nestedExpanded,
onDismissRequest = closeMenu, onDismissRequest = closeMenu,
modifier = modifier,
) { ) {
children(closeMenu) children(closeMenu)
} }
} }
}

View file

@ -3,7 +3,9 @@ package eu.kanade.presentation.components
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.entries.DownloadAction import eu.kanade.presentation.entries.DownloadAction
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -14,20 +16,24 @@ fun EntryDownloadDropdownMenu(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit, onDownloadClicked: (DownloadAction) -> Unit,
isManga: Boolean, isManga: Boolean,
modifier: Modifier = Modifier,
) { ) {
DropdownMenu( val downloadAmount = if (isManga) MR.plurals.download_amount else MR.plurals.download_amount_anime
expanded = expanded,
onDismissRequest = onDismissRequest,
) {
val downloadAmount = if (isManga) MR.plurals.download_amount_manga else MR.plurals.download_amount_anime
val downloadUnviewed = if (isManga) MR.strings.download_unread else MR.strings.download_unseen 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_1_ITEM to pluralStringResource(downloadAmount, 1, 1),
DownloadAction.NEXT_5_ITEMS to pluralStringResource(downloadAmount, 5, 5), DownloadAction.NEXT_5_ITEMS to pluralStringResource(downloadAmount, 5, 5),
DownloadAction.NEXT_10_ITEMS to pluralStringResource(downloadAmount, 10, 10), DownloadAction.NEXT_10_ITEMS to pluralStringResource(downloadAmount, 10, 10),
DownloadAction.NEXT_25_ITEMS to pluralStringResource(downloadAmount, 25, 25), DownloadAction.NEXT_25_ITEMS to pluralStringResource(downloadAmount, 25, 25),
DownloadAction.UNVIEWED_ITEMS to stringResource(downloadUnviewed), DownloadAction.UNVIEWED_ITEMS to stringResource(downloadUnviewed),
).map { (downloadAction, string) -> )
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
options.map { (downloadAction, string) ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = string) }, text = { Text(text = string) },
onClick = { 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.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.entries.anime.model.episodesFiltered 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.DownloadAction
import eu.kanade.presentation.entries.EntryScreenItem import eu.kanade.presentation.entries.EntryScreenItem
import eu.kanade.presentation.entries.anime.components.AnimeActionRow 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.animesource.model.SAnime
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo 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.AnimeScreenModel
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import tachiyomi.domain.entries.anime.model.Anime 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.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.presentation.core.util.isScrollingUp
import java.text.DateFormat import tachiyomi.source.local.entries.anime.isLocal
import java.util.Date import java.time.Instant
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@Composable @Composable
fun AnimeScreen( fun AnimeScreen(
state: AnimeScreenModel.State.Success, state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
fetchInterval: Int?, nextUpdate: Instant?,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
isTabletUi: Boolean, isTabletUi: Boolean,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
@ -156,16 +154,14 @@ fun AnimeScreen(
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val onSettingsClicked: (() -> Unit)? = { val onSettingsClicked: (() -> Unit)? = {
navigator.push(SourcePreferencesScreen(state.source.id)) navigator.push(AnimeSourcePreferencesScreen(state.source.id))
}.takeIf { state.source is ConfigurableAnimeSource } }.takeIf { state.source is ConfigurableAnimeSource }
if (!isTabletUi) { if (!isTabletUi) {
AnimeScreenSmallImpl( AnimeScreenSmallImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime, nextUpdate = nextUpdate,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction, episodeSwipeEndAction = episodeSwipeEndAction,
showNextEpisodeAirTime = showNextEpisodeAirTime, showNextEpisodeAirTime = showNextEpisodeAirTime,
@ -204,13 +200,11 @@ fun AnimeScreen(
AnimeScreenLargeImpl( AnimeScreenLargeImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime, nextUpdate = nextUpdate,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction, episodeSwipeEndAction = episodeSwipeEndAction,
showNextEpisodeAirTime = showNextEpisodeAirTime, showNextEpisodeAirTime = showNextEpisodeAirTime,
alwaysUseExternalPlayer = alwaysUseExternalPlayer, alwaysUseExternalPlayer = alwaysUseExternalPlayer,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
onEpisodeClicked = onEpisodeClicked, onEpisodeClicked = onEpisodeClicked,
onDownloadEpisode = onDownloadEpisode, onDownloadEpisode = onDownloadEpisode,
@ -249,9 +243,7 @@ fun AnimeScreen(
private fun AnimeScreenSmallImpl( private fun AnimeScreenSmallImpl(
state: AnimeScreenModel.State.Success, state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean, nextUpdate: Instant?,
dateFormat: DateFormat,
fetchInterval: Int?,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
showNextEpisodeAirTime: Boolean, showNextEpisodeAirTime: Boolean,
@ -455,7 +447,7 @@ private fun AnimeScreenSmallImpl(
AnimeActionRow( AnimeActionRow(
favorite = state.anime.favorite, favorite = state.anime.favorite,
trackingCount = state.trackingCount, trackingCount = state.trackingCount,
fetchInterval = fetchInterval, nextUpdate = nextUpdate,
isUserIntervalMode = state.anime.fetchInterval < 0, isUserIntervalMode = state.anime.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked, onWebViewClicked = onWebViewClicked,
@ -526,8 +518,6 @@ private fun AnimeScreenSmallImpl(
anime = state.anime, anime = state.anime,
episodes = listItem, episodes = listItem,
isAnyEpisodeSelected = episodes.fastAny { it.selected }, isAnyEpisodeSelected = episodes.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction, episodeSwipeEndAction = episodeSwipeEndAction,
onEpisodeClicked = onEpisodeClicked, onEpisodeClicked = onEpisodeClicked,
@ -546,9 +536,7 @@ private fun AnimeScreenSmallImpl(
fun AnimeScreenLargeImpl( fun AnimeScreenLargeImpl(
state: AnimeScreenModel.State.Success, state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean, nextUpdate: Instant?,
dateFormat: DateFormat,
fetchInterval: Int?,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
showNextEpisodeAirTime: Boolean, showNextEpisodeAirTime: Boolean,
@ -734,7 +722,7 @@ fun AnimeScreenLargeImpl(
AnimeActionRow( AnimeActionRow(
favorite = state.anime.favorite, favorite = state.anime.favorite,
trackingCount = state.trackingCount, trackingCount = state.trackingCount,
fetchInterval = fetchInterval, nextUpdate = nextUpdate,
isUserIntervalMode = state.anime.fetchInterval < 0, isUserIntervalMode = state.anime.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked, onWebViewClicked = onWebViewClicked,
@ -812,8 +800,6 @@ fun AnimeScreenLargeImpl(
anime = state.anime, anime = state.anime,
episodes = listItem, episodes = listItem,
isAnyEpisodeSelected = episodes.fastAny { it.selected }, isAnyEpisodeSelected = episodes.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction, episodeSwipeEndAction = episodeSwipeEndAction,
onEpisodeClicked = onEpisodeClicked, onEpisodeClicked = onEpisodeClicked,
@ -884,8 +870,6 @@ private fun LazyListScope.sharedEpisodeItems(
anime: Anime, anime: Anime,
episodes: List<EpisodeList>, episodes: List<EpisodeList>,
isAnyEpisodeSelected: Boolean, isAnyEpisodeSelected: Boolean,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
onEpisodeClicked: (Episode, Boolean) -> Unit, onEpisodeClicked: (Episode, Boolean) -> Unit,
@ -904,7 +888,6 @@ private fun LazyListScope.sharedEpisodeItems(
contentType = { EntryScreenItem.ITEM }, contentType = { EntryScreenItem.ITEM },
) { episodeItem -> ) { episodeItem ->
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current
when (episodeItem) { when (episodeItem) {
is EpisodeList.MissingCount -> { is EpisodeList.MissingCount -> {
@ -920,15 +903,7 @@ private fun LazyListScope.sharedEpisodeItems(
} else { } else {
episodeItem.episode.name episodeItem.episode.name
}, },
date = episodeItem.episode.dateUpload date = relativeDateText(episodeItem.episode.dateUpload),
.takeIf { it > 0L }
?.let {
Date(it).toRelativeString(
context,
dateRelativeTime,
dateFormat,
)
},
watchProgress = episodeItem.episode.lastSecondSeen watchProgress = episodeItem.episode.lastSecondSeen
.takeIf { !episodeItem.episode.seen && it > 0L } .takeIf { !episodeItem.episode.seen && it > 0L }
?.let { ?.let {
@ -942,7 +917,7 @@ private fun LazyListScope.sharedEpisodeItems(
seen = episodeItem.episode.seen, seen = episodeItem.episode.seen,
bookmark = episodeItem.episode.bookmark, bookmark = episodeItem.episode.bookmark,
selected = episodeItem.selected, selected = episodeItem.selected,
downloadIndicatorEnabled = !isAnyEpisodeSelected, downloadIndicatorEnabled = !isAnyEpisodeSelected && !anime.isLocal(),
downloadStateProvider = { episodeItem.downloadState }, downloadStateProvider = { episodeItem.downloadState },
downloadProgressProvider = { episodeItem.downloadProgress }, downloadProgressProvider = { episodeItem.downloadProgress },
episodeSwipeStartAction = episodeSwipeStartAction, 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.FlowRow
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
@ -28,7 +29,7 @@ fun DuplicateAnimeDialog(
}, },
confirmButton = { confirmButton = {
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
TextButton( TextButton(
onClick = { onClick = {

View file

@ -21,6 +21,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Download 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.NavigateNext
import androidx.compose.material.icons.outlined.OpenInNew import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.SystemUpdateAlt 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 = {}, onExtDownloadClicked: () -> Unit = {},
onCopyClicked: () -> Unit = {}, onCopyClicked: () -> Unit = {},
onExtPlayerClicked: () -> Unit = {}, onExtPlayerClicked: () -> Unit = {},
onIntPlayerClicked: () -> Unit = {},
) { ) {
val closeMenu = { EpisodeOptionsDialogScreen.onDismissDialog() } val closeMenu = { EpisodeOptionsDialogScreen.onDismissDialog() }
@ -325,6 +339,15 @@ private fun QualityOptions(
closeMenu() 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, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
if (watchProgress != null || scanlator != null) DotSeparatorText()
} }
if (watchProgress != null) { if (watchProgress != null) {
DotSeparatorText()
Text( Text(
text = watchProgress, text = watchProgress,
maxLines = 1, maxLines = 1,
@ -200,6 +200,7 @@ fun AnimeEpisodeListItem(
if (scanlator != null) DotSeparatorText() if (scanlator != null) DotSeparatorText()
} }
if (scanlator != null) { if (scanlator != null) {
DotSeparatorText()
Text( Text(
text = scanlator, text = scanlator,
maxLines = 1, maxLines = 1,
@ -210,19 +211,17 @@ fun AnimeEpisodeListItem(
} }
} }
if (onDownloadClick != null) {
EpisodeDownloadIndicator( EpisodeDownloadIndicator(
enabled = downloadIndicatorEnabled, enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider, downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider, downloadProgressProvider = downloadProgressProvider,
onClick = onDownloadClick, onClick = { onDownloadClick?.invoke(it) },
) )
} }
} }
} }
} }
}
private fun getSwipeAction( private fun getSwipeAction(
action: LibraryPreferences.EpisodeSwipeAction, action: LibraryPreferences.EpisodeSwipeAction,

View file

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

View file

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

View file

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

View file

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

View file

@ -225,7 +225,10 @@ private fun RowScope.Button(
onClick: () -> Unit, onClick: () -> Unit,
content: (@Composable () -> Unit)? = null, 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( Column(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
@ -262,13 +265,13 @@ private fun RowScope.Button(
@Composable @Composable
fun LibraryBottomActionMenu( fun LibraryBottomActionMenu(
visible: Boolean, visible: Boolean,
modifier: Modifier = Modifier,
onChangeCategoryClicked: () -> Unit, onChangeCategoryClicked: () -> Unit,
onMarkAsViewedClicked: () -> Unit, onMarkAsViewedClicked: () -> Unit,
onMarkAsUnviewedClicked: () -> Unit, onMarkAsUnviewedClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?, onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: () -> Unit, onDeleteClicked: () -> Unit,
isManga: Boolean, isManga: Boolean,
modifier: Modifier = Modifier,
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,

View file

@ -20,7 +20,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar 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(), .build(),
) )

View file

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

View file

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

View file

@ -1,23 +1,37 @@
package eu.kanade.presentation.entries.components package eu.kanade.presentation.entries.components
import androidx.compose.foundation.layout.BoxWithConstraints 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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp 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 kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.WheelTextPicker 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 tachiyomi.presentation.core.i18n.stringResource
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
@Composable @Composable
fun DeleteItemsDialog( fun DeleteItemsDialog(
@ -55,21 +69,65 @@ fun DeleteItemsDialog(
@Composable @Composable
fun SetIntervalDialog( fun SetIntervalDialog(
interval: Int, interval: Int,
nextUpdate: Instant?,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onValueChanged: (Int) -> Unit, isManga: Boolean,
onValueChanged: ((Int) -> Unit)? = null,
) { ) {
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) } 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( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.manga_modify_calculated_interval_title)) }, title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
text = { 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( BoxWithConstraints(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
val size = DpSize(width = maxWidth / 2, height = 128.dp) 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 { .map {
if (it == 0) { if (it == 0) {
stringResource(MR.strings.label_default) stringResource(MR.strings.label_default)
@ -85,6 +143,8 @@ fun SetIntervalDialog(
onSelectionChanged = { selectedInterval = it }, onSelectionChanged = { selectedInterval = it },
) )
} }
}
}
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
@ -93,7 +153,7 @@ fun SetIntervalDialog(
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
onValueChanged(selectedInterval) onValueChanged?.invoke(selectedInterval)
onDismissRequest() onDismissRequest()
}) { }) {
Text(text = stringResource(MR.strings.action_ok)) 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.FlowRow
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
@ -28,7 +29,7 @@ fun DuplicateMangaDialog(
}, },
confirmButton = { confirmButton = {
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
TextButton( TextButton(
onClick = { onClick = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,24 +6,20 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.history.anime.components.AnimeHistoryItem import eu.kanade.presentation.history.anime.components.AnimeHistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
@Composable @Composable
@ -33,7 +29,6 @@ fun AnimeHistoryScreen(
onClickCover: (animeId: Long) -> Unit, onClickCover: (animeId: Long) -> Unit,
onClickResume: (animeId: Long, episodeId: Long) -> Unit, onClickResume: (animeId: Long, episodeId: Long) -> Unit,
onDialogChange: (AnimeHistoryScreenModel.Dialog?) -> Unit, onDialogChange: (AnimeHistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
searchQuery: String? = null, searchQuery: String? = null,
) { ) {
Scaffold( Scaffold(
@ -53,17 +48,12 @@ fun AnimeHistoryScreen(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
} else { } else {
AnimeHistoryContent( AnimeHistoryScreenContent(
history = it, history = it,
contentPadding = contentPadding, contentPadding = contentPadding,
onClickCover = { history -> onClickCover(history.animeId) }, onClickCover = { history -> onClickCover(history.animeId) },
onClickResume = { history -> onClickResume(history.animeId, history.episodeId) }, onClickResume = { history -> onClickResume(history.animeId, history.episodeId) },
onClickDelete = { item -> onClickDelete = { item -> onDialogChange(AnimeHistoryScreenModel.Dialog.Delete(item)) },
onDialogChange(
AnimeHistoryScreenModel.Dialog.Delete(item),
)
},
preferences = preferences,
) )
} }
} }
@ -71,17 +61,13 @@ fun AnimeHistoryScreen(
} }
@Composable @Composable
private fun AnimeHistoryContent( private fun AnimeHistoryScreenContent(
history: List<AnimeHistoryUiModel>, history: List<AnimeHistoryUiModel>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickCover: (AnimeHistoryWithRelations) -> Unit, onClickCover: (AnimeHistoryWithRelations) -> Unit,
onClickResume: (AnimeHistoryWithRelations) -> Unit, onClickResume: (AnimeHistoryWithRelations) -> Unit,
onClickDelete: (AnimeHistoryWithRelations) -> Unit, onClickDelete: (AnimeHistoryWithRelations) -> Unit,
preferences: UiPreferences,
) { ) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
@ -97,11 +83,9 @@ private fun AnimeHistoryContent(
) { item -> ) { item ->
when (item) { when (item) {
is AnimeHistoryUiModel.Header -> { is AnimeHistoryUiModel.Header -> {
RelativeDateHeader( ListGroupHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
date = item.date, text = relativeDateText(item.date),
relativeTime = relativeTime,
dateFormat = dateFormat,
) )
} }
is AnimeHistoryUiModel.Item -> { is AnimeHistoryUiModel.Item -> {
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = { _, _ -> run {} }, onClickResume = { _, _ -> run {} },
onDialogChange = {}, 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.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.history.manga.components.MangaHistoryItem import eu.kanade.presentation.history.manga.components.MangaHistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
@Composable @Composable
@ -33,7 +29,6 @@ fun MangaHistoryScreen(
onClickCover: (mangaId: Long) -> Unit, onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (MangaHistoryScreenModel.Dialog?) -> Unit, onDialogChange: (MangaHistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
searchQuery: String? = null, searchQuery: String? = null,
) { ) {
Scaffold( Scaffold(
@ -53,17 +48,12 @@ fun MangaHistoryScreen(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
} else { } else {
MangaHistoryContent( MangaHistoryScreenContent(
history = it, history = it,
contentPadding = contentPadding, contentPadding = contentPadding,
onClickCover = { history -> onClickCover(history.mangaId) }, onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onClickDelete = { item -> onDialogChange(MangaHistoryScreenModel.Dialog.Delete(item)) },
onDialogChange(
MangaHistoryScreenModel.Dialog.Delete(item),
)
},
preferences = preferences,
) )
} }
} }
@ -71,17 +61,13 @@ fun MangaHistoryScreen(
} }
@Composable @Composable
private fun MangaHistoryContent( private fun MangaHistoryScreenContent(
history: List<MangaHistoryUiModel>, history: List<MangaHistoryUiModel>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickCover: (MangaHistoryWithRelations) -> Unit, onClickCover: (MangaHistoryWithRelations) -> Unit,
onClickResume: (MangaHistoryWithRelations) -> Unit, onClickResume: (MangaHistoryWithRelations) -> Unit,
onClickDelete: (MangaHistoryWithRelations) -> Unit, onClickDelete: (MangaHistoryWithRelations) -> Unit,
preferences: UiPreferences,
) { ) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
@ -97,11 +83,9 @@ private fun MangaHistoryContent(
) { item -> ) { item ->
when (item) { when (item) {
is MangaHistoryUiModel.Header -> { is MangaHistoryUiModel.Header -> {
RelativeDateHeader( ListGroupHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
date = item.date, text = relativeDateText(item.date),
relativeTime = relativeTime,
dateFormat = dateFormat,
) )
} }
is MangaHistoryUiModel.Item -> { is MangaHistoryUiModel.Item -> {
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = { _, _ -> run {} }, onClickResume = { _, _ -> run {} },
onDialogChange = {}, 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.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibrarySettingsScreenModel 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 kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@ -74,6 +76,8 @@ private fun ColumnScope.FilterPage(
) { ) {
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedAnime().collectAsState() val filterDownloaded by screenModel.libraryPreferences.filterDownloadedAnime().collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState() val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
val autoUpdateAnimeRestrictions by screenModel.libraryPreferences.autoUpdateItemRestrictions().collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_downloaded), label = stringResource(MR.strings.label_downloaded),
state = if (downloadedOnly) { state = if (downloadedOnly) {
@ -108,6 +112,18 @@ private fun ColumnScope.FilterPage(
state = filterCompleted, state = filterCompleted,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedAnime) }, 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 } val trackers = remember { screenModel.trackers }
when (trackers.size) { when (trackers.size) {

View file

@ -40,6 +40,7 @@ fun LibraryToolbar(
searchQuery: String?, searchQuery: String?,
onSearchQueryChange: (String?) -> Unit, onSearchQueryChange: (String?) -> Unit,
scrollBehavior: TopAppBarScrollBehavior?, scrollBehavior: TopAppBarScrollBehavior?,
navigateUp: (() -> Unit)? = null,
) = when { ) = when {
selectedCount > 0 -> LibrarySelectionToolbar( selectedCount > 0 -> LibrarySelectionToolbar(
selectedCount = selectedCount, selectedCount = selectedCount,
@ -57,6 +58,7 @@ fun LibraryToolbar(
onClickGlobalUpdate = onClickGlobalUpdate, onClickGlobalUpdate = onClickGlobalUpdate,
onClickOpenRandomEntry = onClickOpenRandomEntry, onClickOpenRandomEntry = onClickOpenRandomEntry,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
navigateUp = navigateUp,
) )
} }
@ -71,6 +73,7 @@ private fun LibraryRegularToolbar(
onClickGlobalUpdate: () -> Unit, onClickGlobalUpdate: () -> Unit,
onClickOpenRandomEntry: () -> Unit, onClickOpenRandomEntry: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior?, scrollBehavior: TopAppBarScrollBehavior?,
navigateUp: (() -> Unit)?,
) { ) {
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
SearchToolbar( SearchToolbar(
@ -119,6 +122,7 @@ private fun LibraryRegularToolbar(
) )
}, },
scrollBehavior = scrollBehavior, 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.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.library.manga.MangaLibrarySettingsScreenModel 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 kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@ -74,6 +76,8 @@ private fun ColumnScope.FilterPage(
) { ) {
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedManga().collectAsState() val filterDownloaded by screenModel.libraryPreferences.filterDownloadedManga().collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState() val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateItemRestrictions().collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_downloaded), label = stringResource(MR.strings.label_downloaded),
state = if (downloadedOnly) { state = if (downloadedOnly) {
@ -108,6 +112,18 @@ private fun ColumnScope.FilterPage(
state = filterCompleted, state = filterCompleted,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedManga) }, 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 } val trackers = remember { screenModel.trackers }
when (trackers.size) { 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.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.CloudOff 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.GetApp
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.QueryStats import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Settings 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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import eu.kanade.domain.ui.model.NavStyle
import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.Constants import eu.kanade.tachiyomi.core.Constants
import eu.kanade.tachiyomi.ui.more.DownloadQueueState import eu.kanade.tachiyomi.ui.more.DownloadQueueState
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.injectLazy
@Composable @Composable
fun MoreScreen( fun MoreScreen(
@ -47,6 +44,7 @@ fun MoreScreen(
incognitoMode: Boolean, incognitoMode: Boolean,
onIncognitoModeChange: (Boolean) -> Unit, onIncognitoModeChange: (Boolean) -> Unit,
isFDroid: Boolean, isFDroid: Boolean,
navStyle: NavStyle,
onClickAlt: () -> Unit, onClickAlt: () -> Unit,
onClickDownloadQueue: () -> Unit, onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit, onClickCategories: () -> Unit,
@ -107,23 +105,10 @@ fun MoreScreen(
item { HorizontalDivider() } item { HorizontalDivider() }
val libraryPreferences: LibraryPreferences by injectLazy()
item { 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( TextPreferenceWidget(
title = stringResource(titleRes), title = navStyle.moreTab.options.title,
icon = icon, icon = navStyle.moreIcon,
onPreferenceClick = onClickAlt, onPreferenceClick = onClickAlt,
) )
} }
@ -148,6 +133,7 @@ fun MoreScreen(
}" }"
} }
} }
is DownloadQueueState.Downloading -> { is DownloadQueueState.Downloading -> {
val pending = downloadQueueState.pending val pending = downloadQueueState.pending
pluralStringResource( pluralStringResource(

View file

@ -17,7 +17,7 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle 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 com.halilibo.richtext.ui.string.RichTextStringStyle
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@ -42,7 +42,7 @@ fun NewUpdateScreen(
rejectText = stringResource(MR.strings.action_not_now), rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate, onRejectClick = onRejectUpdate,
) { ) {
Material3RichText( RichText(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = MaterialTheme.padding.large), .padding(vertical = MaterialTheme.padding.large),
@ -59,7 +59,7 @@ fun NewUpdateScreen(
modifier = Modifier.padding(top = MaterialTheme.padding.small), modifier = Modifier.padding(top = MaterialTheme.padding.small),
) { ) {
Text(text = stringResource(MR.strings.update_check_open)) 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) 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 androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
internal class GuidesStep(
private val onRestoreBackup: () -> Unit,
) : OnboardingStep {
override val isComplete: Boolean = true
@Composable @Composable
internal fun GuidesStep( override fun Content() {
onRestoreBackup: () -> Unit,
) {
val handler = LocalUriHandler.current val handler = LocalUriHandler.current
Column( Column(
modifier = Modifier.padding(16.dp), 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))) Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
Button( Button(
@ -36,6 +41,7 @@ internal fun GuidesStep(
} }
HorizontalDivider( HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer, color = MaterialTheme.colorScheme.onPrimaryContainer,
) )
@ -48,6 +54,7 @@ internal fun GuidesStep(
} }
} }
} }
}
const val GETTING_STARTED_URL = "https://aniyomi.org/docs/guides/getting-started" const val GETTING_STARTED_URL = "https://aniyomi.org/docs/guides/getting-started"
@ -57,6 +64,6 @@ private fun GuidesStepPreview() {
TachiyomiTheme { TachiyomiTheme {
GuidesStep( GuidesStep(
onRestoreBackup = {}, onRestoreBackup = {},
) ).Content()
} }
} }

View file

@ -13,15 +13,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.materialSharedAxisX
import soup.compose.material.motion.animation.rememberSlideDistance import soup.compose.material.motion.animation.rememberSlideDistance
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -29,24 +26,21 @@ import tachiyomi.presentation.core.screens.InfoScreen
@Composable @Composable
fun OnboardingScreen( fun OnboardingScreen(
storagePreferences: StoragePreferences,
uiPreferences: UiPreferences,
onComplete: () -> Unit, onComplete: () -> Unit,
onRestoreBackup: () -> Unit, onRestoreBackup: () -> Unit,
) { ) {
val context = LocalContext.current
val slideDistance = rememberSlideDistance() val slideDistance = rememberSlideDistance()
var currentStep by remember { mutableIntStateOf(0) } var currentStep by rememberSaveable { mutableIntStateOf(0) }
val steps: List<@Composable () -> Unit> = remember { val steps = remember {
listOf( listOf(
{ ThemeStep(uiPreferences = uiPreferences) }, ThemeStep(),
{ StorageStep(storagePref = storagePreferences.baseStorageDirectory()) }, StorageStep(),
// TODO: prompt for notification permissions when bumping target to Android 13 PermissionStep(),
{ GuidesStep(onRestoreBackup = onRestoreBackup) }, GuidesStep(onRestoreBackup = onRestoreBackup),
) )
} }
val isLastStep = currentStep == steps.size - 1 val isLastStep = currentStep == steps.lastIndex
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- }) BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
@ -61,17 +55,13 @@ fun OnboardingScreen(
MR.strings.onboarding_action_next MR.strings.onboarding_action_next
}, },
), ),
canAccept = steps[currentStep].isComplete,
onAcceptClick = { onAcceptClick = {
if (isLastStep) { if (isLastStep) {
onComplete() onComplete()
} else {
// TODO: this is kind of janky
if (currentStep == 1 && !storagePreferences.baseStorageDirectory().isSet()) {
context.toast(MR.strings.onboarding_storage_selection_required)
} else { } else {
currentStep++ currentStep++
} }
}
}, },
) { ) {
Box( Box(
@ -91,7 +81,7 @@ fun OnboardingScreen(
}, },
label = "stepContent", 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.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
import eu.kanade.tachiyomi.util.system.isTvBox
import eu.kanade.tachiyomi.util.system.toast 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.i18n.MR
import tachiyomi.presentation.core.components.material.Button import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
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 @Composable
internal fun StorageStep( override fun Content() {
storagePref: Preference<String>,
) {
val context = LocalContext.current val context = LocalContext.current
val handler = LocalUriHandler.current
val isTvBox = isTvBox(LocalContext.current)
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref) val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
Text( Text(
stringResource( 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( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = { onClick = {
@ -49,4 +90,24 @@ internal fun StorageStep(
Text(stringResource(MR.strings.onboarding_storage_action_select)) 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.AppThemeModePreferenceWidget
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
internal class ThemeStep : OnboardingStep {
override val isComplete: Boolean = true
private val uiPreferences: UiPreferences = Injekt.get()
@Composable @Composable
internal fun ThemeStep( override fun Content() {
uiPreferences: UiPreferences,
) {
val themeModePref = uiPreferences.themeMode() val themeModePref = uiPreferences.themeMode()
val themeMode by themeModePref.collectAsState() val themeMode by themeModePref.collectAsState()
@ -38,3 +44,4 @@ internal fun ThemeStep(
) )
} }
} }
}

View file

@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.Tracker
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.core.preference.Preference as PreferenceData import tachiyomi.core.preference.Preference as PreferenceData
@ -64,13 +66,13 @@ sealed class Preference {
val pref: PreferenceData<T>, val pref: PreferenceData<T>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", 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]) }, { v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: T) -> Boolean = { true }, override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
val entries: Map<T, String>, val entries: ImmutableMap<T, String>,
) : PreferenceItem<T>() { ) : PreferenceItem<T>() {
internal fun internalSet(newValue: Any) = pref.set(newValue as T) internal fun internalSet(newValue: Any) = pref.set(newValue as T)
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged( internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(
@ -78,8 +80,8 @@ sealed class Preference {
) )
@Composable @Composable
internal fun internalSubtitleProvider(value: Any?, entries: Map<out Any?, String>) = internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
subtitleProvider(value as T, entries as Map<T, String>) subtitleProvider(value as T, entries as ImmutableMap<T, String>)
} }
/** /**
@ -89,13 +91,13 @@ sealed class Preference {
val value: String, val value: String,
override val title: String, override val title: String,
override val subtitle: String? = "%s", 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]) }, { v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val entries: Map<String, String>, val entries: ImmutableMap<String, String>,
) : PreferenceItem<String>() ) : PreferenceItem<String>()
/** /**
@ -106,7 +108,10 @@ sealed class Preference {
val pref: PreferenceData<Set<String>>, val pref: PreferenceData<Set<String>>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", 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) { val combined = remember(v) {
v.map { e[it] } v.map { e[it] }
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
@ -118,7 +123,7 @@ sealed class Preference {
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true }, override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
val entries: Map<String, String>, val entries: ImmutableMap<String, String>,
) : PreferenceItem<Set<String>>() ) : PreferenceItem<Set<String>>()
/** /**
@ -184,6 +189,6 @@ sealed class Preference {
override val title: String, override val title: String,
override val enabled: Boolean = true, override val enabled: Boolean = true,
val preferenceItems: List<PreferenceItem<out Any>>, val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
) : Preference() ) : 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.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -172,8 +171,8 @@ internal fun PreferenceItem(
) )
} }
is Preference.PreferenceItem.TrackerPreference -> { is Preference.PreferenceItem.TrackerPreference -> {
val uName by Injekt.get<PreferenceStore>() val uName by Injekt.get<TrackPreferences>()
.getString(TrackPreferences.trackUsername(item.tracker.id)) .trackUsername(item.tracker)
.collectAsState() .collectAsState()
item.tracker.run { item.tracker.run {
TrackingPreferenceWidget( TrackingPreferenceWidget(

View file

@ -8,6 +8,7 @@ import eu.kanade.core.preference.asState
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding
import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -47,10 +48,15 @@ object AdvancedPlayerSettingsScreen : SearchableSettings {
postfix = if (mpvInput.asState(scope).value.lines().size > 2) "\n..." else "", 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( Preference.PreferenceItem.ListPreference(
title = context.stringResource(MR.strings.pref_debanding_title), title = context.stringResource(MR.strings.pref_debanding_title),
pref = playerPreferences.videoDebanding(), 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 * Returns a string of categories name for settings subtitle
*/ */
@ReadOnlyComposable @ReadOnlyComposable
@Composable @Composable
fun getCategoriesLabel( fun getCategoriesLabel(

View file

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

View file

@ -1,34 +1,28 @@
package eu.kanade.presentation.more.settings.screen package eu.kanade.presentation.more.settings.screen
import android.app.Activity import android.app.Activity
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat 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.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.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.presentation.more.settings.Preference 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.AppThemeModePreferenceWidget
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
import eu.kanade.tachiyomi.R import kotlinx.collections.immutable.persistentListOf
import eu.kanade.tachiyomi.ui.home.HomeScreen import kotlinx.collections.immutable.toImmutableMap
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import org.xmlpull.v1.XmlPullParser
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
@ -69,7 +63,7 @@ object SettingsAppearanceScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_theme), title = stringResource(MR.strings.pref_category_theme),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.CustomPreference( Preference.PreferenceItem.CustomPreference(
title = stringResource(MR.strings.pref_app_theme), title = stringResource(MR.strings.pref_app_theme),
) { ) {
@ -107,13 +101,8 @@ object SettingsAppearanceScreen : SearchableSettings {
uiPreferences: UiPreferences, uiPreferences: UiPreferences,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
val context = LocalContext.current 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 now = remember { Instant.now().toEpochMilli() }
val dateFormat by uiPreferences.dateFormat().collectAsState() val dateFormat by uiPreferences.dateFormat().collectAsState()
@ -121,67 +110,43 @@ object SettingsAppearanceScreen : SearchableSettings {
UiPreferences.dateFormat(dateFormat).format(now) 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( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_display), title = stringResource(MR.strings.pref_category_display),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.TextPreference(
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,
title = stringResource(MR.strings.pref_app_language), title = stringResource(MR.strings.pref_app_language),
entries = langs, onClick = { navigator.push(AppLanguageScreen()) },
onValueChanged = { newValue ->
currentLanguage = newValue
true
},
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = uiPreferences.tabletUiMode(), pref = uiPreferences.tabletUiMode(),
title = stringResource(MR.strings.pref_tablet_ui_mode), title = stringResource(MR.strings.pref_tablet_ui_mode),
entries = TabletUiMode.entries.associateWith { stringResource(it.titleRes) }, entries = TabletUiMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
onValueChanged = { onValueChanged = {
context.stringResource(MR.strings.requires_app_restart) context.stringResource(MR.strings.requires_app_restart)
true 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( Preference.PreferenceItem.ListPreference(
pref = uiPreferences.dateFormat(), pref = uiPreferences.dateFormat(),
title = stringResource(MR.strings.pref_date_format), title = stringResource(MR.strings.pref_date_format),
@ -189,7 +154,8 @@ object SettingsAppearanceScreen : SearchableSettings {
.associateWith { .associateWith {
val formattedDate = UiPreferences.dateFormat(it).format(now) val formattedDate = UiPreferences.dateFormat(it).format(now)
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)" "${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
}, }
.toImmutableMap(),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.relativeTime(), 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( 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.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity 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.domain.source.service.SourcePreferences
import eu.kanade.presentation.more.settings.Preference 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 eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -23,11 +31,16 @@ object SettingsBrowseScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val sourcePreferences = remember { Injekt.get<SourcePreferences>() } val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
val mangaReposCount by sourcePreferences.mangaExtensionRepos().collectAsState()
val animeReposCount by sourcePreferences.animeExtensionRepos().collectAsState()
return listOf( return listOf(
Preference.PreferenceGroup( Preference.PreferenceGroup(
title = stringResource(MR.strings.label_sources), title = stringResource(MR.strings.label_sources),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.hideInAnimeLibraryItems(), pref = sourcePreferences.hideInAnimeLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_anime_library_items), title = stringResource(MR.strings.pref_hide_in_anime_library_items),
@ -36,11 +49,33 @@ object SettingsBrowseScreen : SearchableSettings {
pref = sourcePreferences.hideInMangaLibraryItems(), pref = sourcePreferences.hideInMangaLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_manga_library_items), 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( Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_nsfw_content), title = stringResource(MR.strings.pref_category_nsfw_content),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.showNsfwSource(), pref = sourcePreferences.showNsfwSource(),
title = stringResource(MR.strings.pref_show_nsfw_source), 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.Context
import android.content.Intent import android.content.Intent
import android.net.Uri 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.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons
import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material3.AlertDialog 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.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.net.toUri import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen 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.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache 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.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.displayablePath
import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences 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.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@ -71,21 +63,35 @@ import uy.kohesive.injekt.api.get
object SettingsDataScreen : SearchableSettings { object SettingsDataScreen : SearchableSettings {
val restorePreferenceKeyString = MR.strings.label_backup
const val HELP_URL = "https://aniyomi.org/docs/faq/storage"
@ReadOnlyComposable @ReadOnlyComposable
@Composable @Composable
override fun getTitleRes() = MR.strings.label_data_storage 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 @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>() val backupPreferences = Injekt.get<BackupPreferences>()
val storagePreferences = Injekt.get<StoragePreferences>() val storagePreferences = Injekt.get<StoragePreferences>()
return listOf( return persistentListOf(
getStorageLocationPref(storagePreferences = storagePreferences), getStorageLocationPref(storagePreferences = storagePreferences),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
getBackupAndRestoreGroup(backupPreferences = backupPreferences), getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(backupPreferences = backupPreferences), getDataGroup(),
) )
} }
@ -107,8 +113,6 @@ object SettingsDataScreen : SearchableSettings {
UniFile.fromUri(context, uri)?.let { UniFile.fromUri(context, uri)?.let {
storageDirPref.set(it.uri.toString()) 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 context = LocalContext.current
val storageDir by storageDirPref.collectAsState() val storageDir by storageDirPref.collectAsState()
if (storageDir == storageDirPref.defaultValue()) { if (!storageDirPref.isSet()) {
return stringResource(MR.strings.no_location_set) return stringResource(MR.strings.no_location_set)
} }
return remember(storageDir) { return remember(storageDir) {
val file = UniFile.fromUri(context, storageDir.toUri()) val file = UniFile.fromUri(context, storageDir.toUri())
file?.filePath ?: file?.uri?.toString() file?.displayablePath
} ?: stringResource(MR.strings.invalid_location, storageDir) } ?: stringResource(MR.strings.invalid_location, storageDir)
} }
@ -153,20 +157,75 @@ object SettingsDataScreen : SearchableSettings {
@Composable @Composable
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup { private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState() 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( return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_backup), title = stringResource(MR.strings.label_backup),
preferenceItems = listOf( preferenceItems = persistentListOf(
// Manual actions // Manual actions
getCreateBackupPref(), Preference.PreferenceItem.CustomPreference(
getRestoreBackupPref(), 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 // Automatic backups
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = backupPreferences.backupInterval(), pref = backupPreferences.backupInterval(),
title = stringResource(MR.strings.pref_backup_interval), title = stringResource(MR.strings.pref_backup_interval),
entries = mapOf( entries = persistentMapOf(
0 to stringResource(MR.strings.off), 0 to stringResource(MR.strings.off),
6 to stringResource(MR.strings.update_6hour), 6 to stringResource(MR.strings.update_6hour),
12 to stringResource(MR.strings.update_12hour), 12 to stringResource(MR.strings.update_12hour),
@ -188,181 +247,44 @@ object SettingsDataScreen : SearchableSettings {
} }
@Composable @Composable
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference { private fun getDataGroup(): Preference.PreferenceGroup {
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 {
val context = LocalContext.current 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 scope = rememberCoroutineScope()
val context = LocalContext.current
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() } val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val backupIntervalPref = backupPreferences.backupInterval()
val backupInterval by backupIntervalPref.collectAsState()
val chapterCache = remember { Injekt.get<ChapterCache>() } val chapterCache = remember { Injekt.get<ChapterCache>() }
val episodeCache = remember { Injekt.get<EpisodeCache>() }
var cacheReadableSizeSema by remember { mutableIntStateOf(0) } var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
val cacheReadableMangaSize = remember(cacheReadableSizeSema) { chapterCache.readableSize } val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
val cacheReadableAnimeSize = remember(cacheReadableSizeSema) { episodeCache.readableSize }
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_data), title = stringResource(MR.strings.pref_storage_usage),
preferenceItems = listOf( preferenceItems = persistentListOf(
getMangaStorageInfoPref(cacheReadableMangaSize), Preference.PreferenceItem.CustomPreference(
getAnimeStorageInfoPref(cacheReadableAnimeSize), title = stringResource(MR.strings.pref_storage_usage),
) {
BasePreferenceWidget(
subcomponent = {
StorageInfo(
modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
)
},
)
},
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_clear_chapter_cache), title = stringResource(MR.strings.pref_clear_chapter_cache),
subtitle = stringResource( subtitle = stringResource(MR.strings.used_cache, cacheReadableSize),
MR.strings.used_cache_both,
cacheReadableAnimeSize,
cacheReadableMangaSize,
),
onClick = { onClick = {
scope.launchNonCancellable { scope.launchNonCancellable {
try { try {
val deletedFiles = chapterCache.clear() + episodeCache.clear() val deletedFiles = chapterCache.clear()
withUIContext { withUIContext {
context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles)) context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles))
cacheReadableSizeSema++ cacheReadableSizeSema++
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) 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(), pref = libraryPreferences.autoClearItemCache(),
title = stringResource(MR.strings.pref_auto_clear_chapter_cache), 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 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.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog 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 kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
import tachiyomi.domain.category.manga.interactor.GetMangaCategories import tachiyomi.domain.category.manga.interactor.GetMangaCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.i18n.MR 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.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
@ -33,23 +51,59 @@ object SettingsDownloadScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetMangaCategories>() } val getMangaCategories = remember { Injekt.get<GetMangaCategories>() }
val allCategories by getCategories.subscribe().collectAsState( val allMangaCategories by getMangaCategories.subscribe().collectAsState(
initial = runBlocking { getCategories.await() }, initial = runBlocking { getMangaCategories.await() },
) )
val getAnimeCategories = remember { Injekt.get<GetAnimeCategories>() } val getAnimeCategories = remember { Injekt.get<GetAnimeCategories>() }
val allAnimeCategories by getAnimeCategories.subscribe().collectAsState( val allAnimeCategories by getAnimeCategories.subscribe().collectAsState(
initial = runBlocking { getAnimeCategories.await() }, initial = runBlocking { getAnimeCategories.await() },
) )
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() } val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
val basePreferences = remember { Injekt.get<BasePreferences>() } 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( return listOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.downloadOnlyOverWifi(), pref = downloadPreferences.downloadOnlyOverWifi(),
title = stringResource(MR.strings.connected_to_wifi), 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( Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.saveChaptersAsCBZ(), pref = downloadPreferences.saveChaptersAsCBZ(),
title = stringResource(MR.strings.save_chapter_as_cbz), title = stringResource(MR.strings.save_chapter_as_cbz),
@ -62,17 +116,18 @@ object SettingsDownloadScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.numberOfDownloads(), pref = downloadPreferences.numberOfDownloads(),
title = stringResource(MR.strings.pref_download_slots), 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)), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_slots_info)),
getDeleteChaptersGroup( getDeleteChaptersGroup(
downloadPreferences = downloadPreferences, downloadPreferences = downloadPreferences,
categories = allCategories, animeCategories = allAnimeCategories,
mangaCategories = allMangaCategories,
), ),
getAutoDownloadGroup( getAutoDownloadGroup(
downloadPreferences = downloadPreferences, downloadPreferences = downloadPreferences,
allCategories = allCategories,
allAnimeCategories = allAnimeCategories, allAnimeCategories = allAnimeCategories,
allMangaCategories = allMangaCategories,
), ),
getDownloadAheadGroup(downloadPreferences = downloadPreferences), getDownloadAheadGroup(downloadPreferences = downloadPreferences),
getExternalDownloaderGroup( getExternalDownloaderGroup(
@ -85,11 +140,12 @@ object SettingsDownloadScreen : SearchableSettings {
@Composable @Composable
private fun getDeleteChaptersGroup( private fun getDeleteChaptersGroup(
downloadPreferences: DownloadPreferences, downloadPreferences: DownloadPreferences,
categories: List<Category>, animeCategories: List<Category>,
mangaCategories: List<Category>,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_delete_chapters), title = stringResource(MR.strings.pref_category_delete_chapters),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.removeAfterMarkedAsRead(), pref = downloadPreferences.removeAfterMarkedAsRead(),
title = stringResource(MR.strings.pref_remove_after_marked_as_read), title = stringResource(MR.strings.pref_remove_after_marked_as_read),
@ -97,7 +153,7 @@ object SettingsDownloadScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.removeAfterReadSlots(), pref = downloadPreferences.removeAfterReadSlots(),
title = stringResource(MR.strings.pref_remove_after_read), title = stringResource(MR.strings.pref_remove_after_read),
entries = mapOf( entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled), -1 to stringResource(MR.strings.disabled),
0 to stringResource(MR.strings.last_read_chapter), 0 to stringResource(MR.strings.last_read_chapter),
1 to stringResource(MR.strings.second_to_last), 1 to stringResource(MR.strings.second_to_last),
@ -110,9 +166,13 @@ object SettingsDownloadScreen : SearchableSettings {
pref = downloadPreferences.removeBookmarkedChapters(), pref = downloadPreferences.removeBookmarkedChapters(),
title = stringResource(MR.strings.pref_remove_bookmarked_chapters), title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
), ),
getExcludedAnimeCategoriesPreference(
downloadPreferences = downloadPreferences,
categories = { animeCategories },
),
getExcludedCategoriesPreference( getExcludedCategoriesPreference(
downloadPreferences = downloadPreferences, downloadPreferences = downloadPreferences,
categories = { categories }, categories = { mangaCategories },
), ),
), ),
) )
@ -126,15 +186,31 @@ object SettingsDownloadScreen : SearchableSettings {
return Preference.PreferenceItem.MultiSelectListPreference( return Preference.PreferenceItem.MultiSelectListPreference(
pref = downloadPreferences.removeExcludeCategories(), pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(MR.strings.pref_remove_exclude_categories_manga), 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 @Composable
private fun getAutoDownloadGroup( private fun getAutoDownloadGroup(
downloadPreferences: DownloadPreferences, downloadPreferences: DownloadPreferences,
allCategories: List<Category>,
allAnimeCategories: List<Category>, allAnimeCategories: List<Category>,
allMangaCategories: List<Category>,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
val downloadNewEpisodesPref = downloadPreferences.downloadNewEpisodes() val downloadNewEpisodesPref = downloadPreferences.downloadNewEpisodes()
val downloadNewEpisodeCategoriesPref = downloadPreferences.downloadNewEpisodeCategories() val downloadNewEpisodeCategoriesPref = downloadPreferences.downloadNewEpisodeCategories()
@ -179,9 +255,9 @@ object SettingsDownloadScreen : SearchableSettings {
TriStateListDialog( TriStateListDialog(
title = stringResource(MR.strings.manga_categories), title = stringResource(MR.strings.manga_categories),
message = stringResource(MR.strings.pref_download_new_categories_details), message = stringResource(MR.strings.pref_download_new_categories_details),
items = allCategories, items = allMangaCategories,
initialChecked = included.mapNotNull { id -> allCategories.find { it.id.toString() == id } }, initialChecked = included.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } },
initialInversed = excluded.mapNotNull { id -> allCategories.find { it.id.toString() == id } }, initialInversed = excluded.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } },
itemLabel = { it.visualName }, itemLabel = { it.visualName },
onDismissRequest = { showDialog = false }, onDismissRequest = { showDialog = false },
onValueChanged = { newIncluded, newExcluded -> onValueChanged = { newIncluded, newExcluded ->
@ -198,7 +274,7 @@ object SettingsDownloadScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_auto_download), title = stringResource(MR.strings.pref_category_auto_download),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadNewEpisodesPref, pref = downloadNewEpisodesPref,
title = stringResource(MR.strings.pref_download_new_episodes), title = stringResource(MR.strings.pref_download_new_episodes),
@ -220,7 +296,7 @@ object SettingsDownloadScreen : SearchableSettings {
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.manga_categories), title = stringResource(MR.strings.manga_categories),
subtitle = getCategoriesLabel( subtitle = getCategoriesLabel(
allCategories = allCategories, allCategories = allMangaCategories,
included = included, included = included,
excluded = excluded, excluded = excluded,
), ),
@ -237,36 +313,32 @@ object SettingsDownloadScreen : SearchableSettings {
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.download_ahead), title = stringResource(MR.strings.download_ahead),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.autoDownloadWhileReading(),
title = stringResource(MR.strings.auto_download_while_reading),
entries = listOf(0, 2, 3, 5, 10).associateWith {
if (it == 0) {
stringResource(MR.strings.disabled)
} else {
pluralStringResource(
MR.plurals.next_unread_chapters,
count = it,
it,
)
}
},
),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.autoDownloadWhileWatching(), pref = downloadPreferences.autoDownloadWhileWatching(),
title = stringResource(MR.strings.auto_download_while_watching), 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) { if (it == 0) {
stringResource(MR.strings.disabled) stringResource(MR.strings.disabled)
} else { } else {
pluralStringResource( pluralStringResource(MR.plurals.next_unseen_episodes, count = it, it)
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( Preference.PreferenceItem.InfoPreference(
stringResource(MR.strings.download_ahead_info), stringResource(MR.strings.download_ahead_info),
@ -299,12 +371,11 @@ object SettingsDownloadScreen : SearchableSettings {
.map { pm.getApplicationLabel(it.applicationInfo).toString() } .map { pm.getApplicationLabel(it.applicationInfo).toString() }
val packageNamesMap: Map<String, String> = val packageNamesMap: Map<String, String> =
packageNames.zip(packageNamesReadable) mapOf("" to "None") + packageNames.zip(packageNamesReadable).toMap()
.toMap()
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_external_downloader), title = stringResource(MR.strings.pref_category_external_downloader),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = useExternalDownloader, pref = useExternalDownloader,
title = stringResource(MR.strings.pref_use_external_downloader), title = stringResource(MR.strings.pref_use_external_downloader),
@ -312,9 +383,58 @@ object SettingsDownloadScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = externalDownloaderPreference, pref = externalDownloaderPreference,
title = stringResource(MR.strings.pref_external_downloader_selection), 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.anime.AnimeLibraryUpdateJob
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
import eu.kanade.tachiyomi.ui.category.CategoriesTab 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.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
@ -66,8 +69,8 @@ object SettingsLibraryScreen : SearchableSettings {
libraryPreferences, libraryPreferences,
), ),
getGlobalUpdateGroup(allCategories, allAnimeCategories, libraryPreferences), getGlobalUpdateGroup(allCategories, allAnimeCategories, libraryPreferences),
getChapterSwipeActionsGroup(libraryPreferences),
getEpisodeSwipeActionsGroup(libraryPreferences), getEpisodeSwipeActionsGroup(libraryPreferences),
getChapterSwipeActionsGroup(libraryPreferences),
) )
} }
@ -78,7 +81,6 @@ object SettingsLibraryScreen : SearchableSettings {
allAnimeCategories: List<Category>, allAnimeCategories: List<Category>,
libraryPreferences: LibraryPreferences, libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
val userAnimeCategoriesCount = allAnimeCategories.filterNot(Category::isSystemCategory).size val userAnimeCategoriesCount = allAnimeCategories.filterNot(Category::isSystemCategory).size
@ -96,13 +98,13 @@ object SettingsLibraryScreen : SearchableSettings {
allAnimeCategories.fastMap { it.id.toInt() } allAnimeCategories.fastMap { it.id.toInt() }
val mangaLabels = listOf(stringResource(MR.strings.default_category_summary)) + 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)) + val animeLabels = listOf(stringResource(MR.strings.default_category_summary)) +
allAnimeCategories.fastMap { it.visualName(context) } allAnimeCategories.fastMap { it.visualName }
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.general_categories), title = stringResource(MR.strings.general_categories),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.action_edit_anime_categories), title = stringResource(MR.strings.action_edit_anime_categories),
subtitle = pluralStringResource( subtitle = pluralStringResource(
@ -117,7 +119,7 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(MR.strings.default_anime_category), title = stringResource(MR.strings.default_anime_category),
subtitle = selectedAnimeCategory?.visualName subtitle = selectedAnimeCategory?.visualName
?: stringResource(MR.strings.default_category_summary), ?: stringResource(MR.strings.default_category_summary),
entries = animeIds.zip(animeLabels).toMap(), entries = animeIds.zip(animeLabels).toMap().toImmutableMap(),
), ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.action_edit_manga_categories), title = stringResource(MR.strings.action_edit_manga_categories),
@ -131,9 +133,8 @@ object SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.defaultMangaCategory(), pref = libraryPreferences.defaultMangaCategory(),
title = stringResource(MR.strings.default_manga_category), title = stringResource(MR.strings.default_manga_category),
subtitle = selectedCategory?.visualName subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
?: stringResource(MR.strings.default_category_summary), entries = mangaIds.zip(mangaLabels).toMap().toImmutableMap(),
entries = mangaIds.zip(mangaLabels).toMap(),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.categorizedDisplaySettings(), pref = libraryPreferences.categorizedDisplaySettings(),
@ -222,11 +223,11 @@ object SettingsLibraryScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_library_update), title = stringResource(MR.strings.pref_category_library_update),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = autoUpdateIntervalPref, pref = autoUpdateIntervalPref,
title = stringResource(MR.strings.pref_library_update_interval), title = stringResource(MR.strings.pref_library_update_interval),
entries = mapOf( entries = persistentMapOf(
0 to stringResource(MR.strings.update_never), 0 to stringResource(MR.strings.update_never),
12 to stringResource(MR.strings.update_12hour), 12 to stringResource(MR.strings.update_12hour),
24 to stringResource(MR.strings.update_24hour), 24 to stringResource(MR.strings.update_24hour),
@ -245,7 +246,7 @@ object SettingsLibraryScreen : SearchableSettings {
enabled = autoUpdateInterval > 0, enabled = autoUpdateInterval > 0,
title = stringResource(MR.strings.pref_library_update_restriction), title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions), subtitle = stringResource(MR.strings.restrictions),
entries = mapOf( entries = persistentMapOf(
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi), DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered), DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
DEVICE_CHARGING to stringResource(MR.strings.charging), DEVICE_CHARGING to stringResource(MR.strings.charging),
@ -284,18 +285,12 @@ object SettingsLibraryScreen : SearchableSettings {
), ),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateItemRestrictions(), pref = libraryPreferences.autoUpdateItemRestrictions(),
title = stringResource(MR.strings.pref_library_update_manga_restriction), title = stringResource(MR.strings.pref_library_update_smart_update),
entries = mapOf( entries = persistentMapOf(
ENTRY_HAS_UNVIEWED to stringResource( ENTRY_HAS_UNVIEWED to stringResource(MR.strings.pref_update_only_completely_read),
MR.strings.pref_update_only_completely_read,
),
ENTRY_NON_VIEWED to stringResource(MR.strings.pref_update_only_started), ENTRY_NON_VIEWED to stringResource(MR.strings.pref_update_only_started),
ENTRY_NON_COMPLETED to stringResource( ENTRY_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
MR.strings.pref_update_only_non_completed, ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
),
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(
MR.strings.pref_update_only_in_release_period,
),
), ),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
@ -312,41 +307,33 @@ object SettingsLibraryScreen : SearchableSettings {
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_chapter_swipe), title = stringResource(MR.strings.pref_chapter_swipe),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeChapterStartAction(), pref = libraryPreferences.swipeChapterStartAction(),
title = stringResource(MR.strings.pref_chapter_swipe_start), title = stringResource(MR.strings.pref_chapter_swipe_start),
entries = mapOf( entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource( LibraryPreferences.ChapterSwipeAction.Disabled to
MR.strings.action_disable, stringResource(MR.strings.disabled),
), LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource( stringResource(MR.strings.action_bookmark),
MR.strings.action_bookmark, LibraryPreferences.ChapterSwipeAction.ToggleRead to
), stringResource(MR.strings.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource( LibraryPreferences.ChapterSwipeAction.Download to
MR.strings.action_mark_as_read, stringResource(MR.strings.action_download),
),
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
MR.strings.action_download,
),
), ),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeChapterEndAction(), pref = libraryPreferences.swipeChapterEndAction(),
title = stringResource(MR.strings.pref_chapter_swipe_end), title = stringResource(MR.strings.pref_chapter_swipe_end),
entries = mapOf( entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource( LibraryPreferences.ChapterSwipeAction.Disabled to
MR.strings.action_disable, stringResource(MR.strings.disabled),
), LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource( stringResource(MR.strings.action_bookmark),
MR.strings.action_bookmark, LibraryPreferences.ChapterSwipeAction.ToggleRead to
), stringResource(MR.strings.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource( LibraryPreferences.ChapterSwipeAction.Download to
MR.strings.action_mark_as_read, stringResource(MR.strings.action_download),
),
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
MR.strings.action_download,
),
), ),
), ),
), ),
@ -359,41 +346,33 @@ object SettingsLibraryScreen : SearchableSettings {
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_episode_swipe), title = stringResource(MR.strings.pref_episode_swipe),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeEpisodeStartAction(), pref = libraryPreferences.swipeEpisodeStartAction(),
title = stringResource(MR.strings.pref_episode_swipe_start), title = stringResource(MR.strings.pref_episode_swipe_start),
entries = mapOf( entries = persistentMapOf(
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource( LibraryPreferences.EpisodeSwipeAction.Disabled to
MR.strings.action_disable, stringResource(MR.strings.disabled),
), LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource( stringResource(MR.strings.action_bookmark_episode),
MR.strings.action_bookmark_episode, LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
), stringResource(MR.strings.action_mark_as_seen),
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource( LibraryPreferences.EpisodeSwipeAction.Download to
MR.strings.action_mark_as_seen, stringResource(MR.strings.action_download),
),
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
MR.strings.action_download,
),
), ),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeEpisodeEndAction(), pref = libraryPreferences.swipeEpisodeEndAction(),
title = stringResource(MR.strings.pref_episode_swipe_end), title = stringResource(MR.strings.pref_episode_swipe_end),
entries = mapOf( entries = persistentMapOf(
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource( LibraryPreferences.EpisodeSwipeAction.Disabled to
MR.strings.action_disable, stringResource(MR.strings.disabled),
), LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource( stringResource(MR.strings.action_bookmark_episode),
MR.strings.action_bookmark_episode, LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
), stringResource(MR.strings.action_mark_as_seen),
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource( LibraryPreferences.EpisodeSwipeAction.Download to
MR.strings.action_mark_as_seen, stringResource(MR.strings.action_download),
),
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
MR.strings.action_download,
),
), ),
), ),
), ),

View file

@ -186,18 +186,18 @@ object SettingsMainScreen : Screen() {
icon = Icons.Outlined.CollectionsBookmark, icon = Icons.Outlined.CollectionsBookmark,
screen = SettingsLibraryScreen, screen = SettingsLibraryScreen,
), ),
Item(
titleRes = MR.strings.pref_category_reader,
subtitleRes = MR.strings.pref_reader_summary,
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen,
),
Item( Item(
titleRes = MR.strings.pref_category_player, titleRes = MR.strings.pref_category_player,
subtitleRes = MR.strings.pref_player_summary, subtitleRes = MR.strings.pref_player_summary,
icon = Icons.Outlined.PlayCircleOutline, icon = Icons.Outlined.PlayCircleOutline,
screen = SettingsPlayerScreen, screen = SettingsPlayerScreen,
), ),
Item(
titleRes = MR.strings.pref_category_reader,
subtitleRes = MR.strings.pref_reader_summary,
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen,
),
Item( Item(
titleRes = MR.strings.pref_category_downloads, titleRes = MR.strings.pref_category_downloads,
subtitleRes = MR.strings.pref_downloads_summary, 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.WEB_VIDEO_CASTER
import eu.kanade.tachiyomi.ui.player.X_PLAYER import eu.kanade.tachiyomi.ui.player.X_PLAYER
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences 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.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -57,7 +60,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = playerPreferences.progressPreference(), pref = playerPreferences.progressPreference(),
title = stringResource(MR.strings.pref_progress_mark_as_seen), title = stringResource(MR.strings.pref_progress_mark_as_seen),
entries = mapOf( entries = persistentMapOf(
1.00F to stringResource(MR.strings.pref_progress_100), 1.00F to stringResource(MR.strings.pref_progress_100),
0.95F to stringResource(MR.strings.pref_progress_95), 0.95F to stringResource(MR.strings.pref_progress_95),
0.90F to stringResource(MR.strings.pref_progress_90), 0.90F to stringResource(MR.strings.pref_progress_90),
@ -91,7 +94,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_internal_player), title = stringResource(MR.strings.pref_category_internal_player),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = playerFullscreen, pref = playerFullscreen,
title = stringResource(MR.strings.pref_player_fullscreen), title = stringResource(MR.strings.pref_player_fullscreen),
@ -118,7 +121,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_volume_brightness), title = stringResource(MR.strings.pref_category_volume_brightness),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = enableVolumeBrightnessGestures, pref = enableVolumeBrightnessGestures,
title = stringResource(MR.strings.enable_volume_brightness_gestures), title = stringResource(MR.strings.enable_volume_brightness_gestures),
@ -144,11 +147,11 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_player_orientation), title = stringResource(MR.strings.pref_category_player_orientation),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = defaultPlayerOrientationType, pref = defaultPlayerOrientationType,
title = stringResource(MR.strings.pref_default_player_orientation), title = stringResource(MR.strings.pref_default_player_orientation),
entries = mapOf( entries = persistentMapOf(
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR to stringResource( ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR to stringResource(
MR.strings.rotation_free, MR.strings.rotation_free,
), ),
@ -179,7 +182,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = defaultPlayerOrientationPortrait, pref = defaultPlayerOrientationPortrait,
title = stringResource(MR.strings.pref_default_portrait_orientation), title = stringResource(MR.strings.pref_default_portrait_orientation),
entries = mapOf( entries = persistentMapOf(
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT to stringResource( ActivityInfo.SCREEN_ORIENTATION_PORTRAIT to stringResource(
MR.strings.rotation_portrait, MR.strings.rotation_portrait,
), ),
@ -194,7 +197,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = defaultPlayerOrientationLandscape, pref = defaultPlayerOrientationLandscape,
title = stringResource(MR.strings.pref_default_landscape_orientation), title = stringResource(MR.strings.pref_default_landscape_orientation),
entries = mapOf( entries = persistentMapOf(
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE to stringResource( ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE to stringResource(
MR.strings.rotation_landscape, MR.strings.rotation_landscape,
), ),
@ -241,7 +244,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_player_seeking), title = stringResource(MR.strings.pref_category_player_seeking),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = enableHorizontalSeekGesture, pref = enableHorizontalSeekGesture,
title = stringResource(MR.strings.enable_horizontal_seek_gesture), title = stringResource(MR.strings.enable_horizontal_seek_gesture),
@ -254,7 +257,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = skipLengthPreference, pref = skipLengthPreference,
title = stringResource(MR.strings.pref_skip_length), title = stringResource(MR.strings.pref_skip_length),
entries = mapOf( entries = persistentMapOf(
30 to stringResource(MR.strings.pref_skip_30), 30 to stringResource(MR.strings.pref_skip_30),
20 to stringResource(MR.strings.pref_skip_20), 20 to stringResource(MR.strings.pref_skip_20),
10 to stringResource(MR.strings.pref_skip_10), 10 to stringResource(MR.strings.pref_skip_10),
@ -293,7 +296,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = waitingTimeAniSkip, pref = waitingTimeAniSkip,
title = stringResource(MR.strings.pref_waiting_time_aniskip), title = stringResource(MR.strings.pref_waiting_time_aniskip),
entries = mapOf( entries = persistentMapOf(
5 to stringResource(MR.strings.pref_waiting_time_aniskip_5), 5 to stringResource(MR.strings.pref_waiting_time_aniskip_5),
6 to stringResource(MR.strings.pref_waiting_time_aniskip_6), 6 to stringResource(MR.strings.pref_waiting_time_aniskip_6),
7 to stringResource(MR.strings.pref_waiting_time_aniskip_7), 7 to stringResource(MR.strings.pref_waiting_time_aniskip_7),
@ -318,7 +321,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_pip), title = stringResource(MR.strings.pref_category_pip),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = enablePip, pref = enablePip,
title = stringResource(MR.strings.pref_enable_pip), title = stringResource(MR.strings.pref_enable_pip),
@ -364,7 +367,7 @@ object SettingsPlayerScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_external_player), title = stringResource(MR.strings.pref_category_external_player),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = alwaysUseExternalPlayer, pref = alwaysUseExternalPlayer,
title = stringResource(MR.strings.pref_always_use_external_player), title = stringResource(MR.strings.pref_always_use_external_player),
@ -372,7 +375,7 @@ object SettingsPlayerScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = externalPlayerPreference, pref = externalPlayerPreference,
title = stringResource(MR.strings.pref_external_player_preference), 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.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode 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.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
@ -31,12 +34,13 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPref.defaultReadingMode(), pref = readerPref.defaultReadingMode(),
title = stringResource(MR.strings.pref_viewer_type), title = stringResource(MR.strings.pref_viewer_type),
entries = ReadingMode.entries.drop(1) entries = ReadingMode.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) }, .associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPref.doubleTapAnimSpeed(), pref = readerPref.doubleTapAnimSpeed(),
title = stringResource(MR.strings.pref_double_tap_anim_speed), title = stringResource(MR.strings.pref_double_tap_anim_speed),
entries = mapOf( entries = persistentMapOf(
1 to stringResource(MR.strings.double_tap_anim_speed_0), 1 to stringResource(MR.strings.double_tap_anim_speed_0),
500 to stringResource(MR.strings.double_tap_anim_speed_normal), 500 to stringResource(MR.strings.double_tap_anim_speed_normal),
250 to stringResource(MR.strings.double_tap_anim_speed_fast), 250 to stringResource(MR.strings.double_tap_anim_speed_fast),
@ -82,17 +86,18 @@ object SettingsReaderScreen : SearchableSettings {
val fullscreen by fullscreenPref.collectAsState() val fullscreen by fullscreenPref.collectAsState()
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_display), title = stringResource(MR.strings.pref_category_display),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.defaultOrientationType(), pref = readerPreferences.defaultOrientationType(),
title = stringResource(MR.strings.pref_rotation_type), title = stringResource(MR.strings.pref_rotation_type),
entries = ReaderOrientation.entries.drop(1) entries = ReaderOrientation.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) }, .associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerTheme(), pref = readerPreferences.readerTheme(),
title = stringResource(MR.strings.pref_reader_theme), title = stringResource(MR.strings.pref_reader_theme),
entries = mapOf( entries = persistentMapOf(
1 to stringResource(MR.strings.black_background), 1 to stringResource(MR.strings.black_background),
2 to stringResource(MR.strings.gray_background), 2 to stringResource(MR.strings.gray_background),
0 to stringResource(MR.strings.white_background), 0 to stringResource(MR.strings.white_background),
@ -126,7 +131,7 @@ object SettingsReaderScreen : SearchableSettings {
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup { private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_reading), title = stringResource(MR.strings.pref_category_reading),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipRead(), pref = readerPreferences.skipRead(),
title = stringResource(MR.strings.pref_skip_read_chapters), title = stringResource(MR.strings.pref_skip_read_chapters),
@ -165,29 +170,26 @@ object SettingsReaderScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pager_viewer), title = stringResource(MR.strings.pager_viewer),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = navModePref, pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav), title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) } .mapIndexed { index, it -> index to stringResource(it) }
.toMap(), .toMap()
.toImmutableMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pagerNavInverted(), pref = readerPreferences.pagerNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted), title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = mapOf( entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none), ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource( ReaderPreferences.TappingInvertMode.HORIZONTAL,
MR.strings.tapping_inverted_horizontal, ReaderPreferences.TappingInvertMode.VERTICAL,
), ReaderPreferences.TappingInvertMode.BOTH,
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource( )
MR.strings.tapping_inverted_vertical, .associateWith { stringResource(it.titleRes) }
), .toImmutableMap(),
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
MR.strings.tapping_inverted_both,
),
),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
@ -195,14 +197,16 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_image_scale_type), title = stringResource(MR.strings.pref_image_scale_type),
entries = ReaderPreferences.ImageScaleType entries = ReaderPreferences.ImageScaleType
.mapIndexed { index, it -> index + 1 to stringResource(it) } .mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap(), .toMap()
.toImmutableMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.zoomStart(), pref = readerPreferences.zoomStart(),
title = stringResource(MR.strings.pref_zoom_start), title = stringResource(MR.strings.pref_zoom_start),
entries = ReaderPreferences.ZoomStart entries = ReaderPreferences.ZoomStart
.mapIndexed { index, it -> index + 1 to stringResource(it) } .mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap(), .toMap()
.toImmutableMap(),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBorders(), pref = readerPreferences.cropBorders(),
@ -265,29 +269,26 @@ object SettingsReaderScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.webtoon_viewer), title = stringResource(MR.strings.webtoon_viewer),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = navModePref, pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav), title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) } .mapIndexed { index, it -> index to stringResource(it) }
.toMap(), .toMap()
.toImmutableMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonNavInverted(), pref = readerPreferences.webtoonNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted), title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = mapOf( entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none), ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource( ReaderPreferences.TappingInvertMode.HORIZONTAL,
MR.strings.tapping_inverted_horizontal, ReaderPreferences.TappingInvertMode.VERTICAL,
), ReaderPreferences.TappingInvertMode.BOTH,
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource( )
MR.strings.tapping_inverted_vertical, .associateWith { stringResource(it.titleRes) }
), .toImmutableMap(),
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
MR.strings.tapping_inverted_both,
),
),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
@ -304,19 +305,11 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerHideThreshold(), pref = readerPreferences.readerHideThreshold(),
title = stringResource(MR.strings.pref_hide_threshold), title = stringResource(MR.strings.pref_hide_threshold),
entries = mapOf( entries = persistentMapOf(
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource( ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
MR.strings.pref_highest, ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
), ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource( ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
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( Preference.PreferenceItem.SwitchPreference(
@ -365,7 +358,7 @@ object SettingsReaderScreen : SearchableSettings {
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState() val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_reader_navigation), title = stringResource(MR.strings.pref_reader_navigation),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readWithVolumeKeysPref, pref = readWithVolumeKeysPref,
title = stringResource(MR.strings.pref_read_with_volume_keys), title = stringResource(MR.strings.pref_read_with_volume_keys),
@ -383,7 +376,7 @@ object SettingsReaderScreen : SearchableSettings {
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup { private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_reader_actions), title = stringResource(MR.strings.pref_reader_actions),
preferenceItems = listOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readWithLongTap(), pref = readerPreferences.readWithLongTap(),
title = stringResource(MR.strings.pref_read_with_long_tap), 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