mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 20:27:06 +03:00
Merge branch 'master' into controls_refactor
This commit is contained in:
commit
40d8028c4a
657 changed files with 20215 additions and 16660 deletions
5
.github/ISSUE_TEMPLATE.md
vendored
5
.github/ISSUE_TEMPLATE.md
vendored
|
@ -3,10 +3,11 @@
|
|||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.12.3.10)
|
||||
- To the latest version of the app (stable is v0.15.2.4)
|
||||
- All extensions
|
||||
- I have gone through the FAQ (https://aniyomi.org/docs/faq/general) and troubleshooting guide (https://aniyomi.org/docs/guides/troubleshooting/)
|
||||
- If this is an issue with an anime extension, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi-extensions
|
||||
- If this is an issue with an official anime extension, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi-extensions
|
||||
- If this is an issue with an official manga extension and this issue can be replicated in the Tachiyomi app, that I should be opening an issue in https://github.com/tachiyomiorg/extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
|
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -2,10 +2,13 @@ blank_issues_enabled: false
|
|||
contact_links:
|
||||
- name: ⚠️ Anime extension/source issue
|
||||
url: https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose
|
||||
about: Issues and requests for extensions and sources should be opened in the aniyomi-extensions repository instead
|
||||
about: Issues and requests for official extensions and sources should be opened in the aniyomi-extensions repository instead
|
||||
- name: 📦 Aniyomi extensions
|
||||
url: https://aniyomi.org/extensions/
|
||||
about: Anime extensions and sources
|
||||
- name: 🧑💻 Aniyomi help discord
|
||||
url: https://discord.gg/F32UjdJZrR
|
||||
about: Common questions are answered here
|
||||
- name: 🖥️ Aniyomi website
|
||||
url: https://aniyomi.org/
|
||||
about: Guides, troubleshooting, and answers to common questions
|
||||
|
|
8
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
8
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
|
@ -52,7 +52,7 @@ body:
|
|||
label: Aniyomi version
|
||||
description: You can find your Aniyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.12.3.10"
|
||||
Example: "0.15.2.4"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
@ -93,11 +93,13 @@ body:
|
|||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
|
||||
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: If this is an issue with an official manga extension and this issue can be replicated in the Tachiyomi app, that I should be opening an issue in [Tachiyomi's extensions repository](https://github.com/tachiyomiorg/extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://aniyomi.org/docs/faq/general) and [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
|
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
|
@ -30,9 +30,9 @@ body:
|
|||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
|
||||
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
|
6
.github/workflows/build_pull_request.yml
vendored
6
.github/workflows/build_pull_request.yml
vendored
|
@ -3,8 +3,10 @@ on:
|
|||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'i18n/src/main/res/**/strings.xml'
|
||||
- 'i18n/src/main/res/**/strings-aniyomi.xml'
|
||||
- 'i18n/src/commonMain/resources/**/strings-aniyomi.xml'
|
||||
- 'i18n/src/commonMain/resources/**/strings.xml'
|
||||
- 'i18n/src/commonMain/resources/**/plurals-aniyomi.xml'
|
||||
- 'i18n/src/commonMain/resources/**/plurals.xml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
|
|
2
.github/workflows/build_push.yml
vendored
2
.github/workflows/build_push.yml
vendored
|
@ -66,6 +66,8 @@ jobs:
|
|||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
|
||||
- name: Clean up build artifacts
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'aniyomiorg/aniyomi'
|
||||
|
|
22
README.md
22
README.md
|
@ -2,23 +2,21 @@
|
|||
|-------|-----------|-------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------|---------|
|
||||
| [![CI](https://github.com/aniyomiorg/aniyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/aniyomiorg/aniyomi/actions/workflows/build_push.yml) | [![latest preview build](https://img.shields.io/github/v/release/aniyomiorg/aniyomi-preview.svg?maxAge=3600&label=download)](https://github.com/aniyomiorg/aniyomi-preview/releases) | [![CodeFactor](https://www.codefactor.io/repository/github/aniyomiorg/aniyomi/badge)](https://www.codefactor.io/repository/github/aniyomiorg/aniyomi) | [![stable release](https://img.shields.io/github/release/aniyomiorg/aniyomi.svg?maxAge=3600&label=download)](https://github.com/aniyomiorg/aniyomi/releases) | [![Translation status](https://hosted.weblate.org/widgets/aniyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/aniyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/841701076242530374?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/F32UjdJZrR) |
|
||||
|
||||
|
||||
# ![app icon](.github/readme-images/app-icon.png)Aniyomi
|
||||
Aniyomi is an unofficial fork of the free and open source manga reader [Tachiyomi](https://github.com/tachiyomiorg/tachiyomi) that adds anime capabilities! For Android 6.0 and above.
|
||||
Aniyomi is a video player and image viewer for Android 6.0 and above.
|
||||
|
||||
## Features
|
||||
|
||||
Features include:
|
||||
* Watching anime from [a variety of sources](https://github.com/aniyomiorg/aniyomi-extensions)
|
||||
* Everything you know and love about Tachiyomi:
|
||||
* Online reading from a variety of sources
|
||||
* Local reading of downloaded content
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
||||
* Categories to organize your library
|
||||
* Light and dark themes
|
||||
* Schedule updating your library for new chapters
|
||||
* Create backups locally to read offline or to your desired cloud service
|
||||
* Watching videos
|
||||
* View images
|
||||
* Local reading/watching of downloaded content
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* A configurable player built on mpv-android with multiple options and settings
|
||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
||||
* Categories to organize your library
|
||||
* Light and dark themes
|
||||
* Create backups locally to read/watch offline or to your desired cloud service
|
||||
|
||||
## Download
|
||||
Get the app from the [releases page](https://github.com/aniyomiorg/aniyomi/releases).
|
||||
|
|
|
@ -20,8 +20,8 @@ android {
|
|||
defaultConfig {
|
||||
applicationId = "xyz.jmir.tachiyomi.mi"
|
||||
|
||||
versionCode = 113
|
||||
versionName = "0.14.7"
|
||||
versionCode = 121
|
||||
versionName = "0.15.2.4"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
|
@ -130,6 +130,7 @@ android {
|
|||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
buildConfig = true
|
||||
|
||||
// Disable some unused things
|
||||
aidl = false
|
||||
|
@ -253,7 +254,7 @@ dependencies {
|
|||
implementation(libs.logcat)
|
||||
|
||||
// Crash reports
|
||||
implementation(libs.acra.http)
|
||||
implementation(libs.bundles.acra)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -50,7 +50,7 @@
|
|||
|
||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||
-dontnote kotlinx.serialization.** # core serialization annotations
|
||||
|
||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||
-keepclassmembers class kotlinx.serialization.json.** {
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
@ -20,11 +22,15 @@
|
|||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
|
@ -45,13 +51,64 @@
|
|||
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
||||
android:exported="true">
|
||||
android:theme="@style/Theme.Tachiyomi.SplashScreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deep link to add manga repos -->
|
||||
<intent-filter android:label="@string/action_add_repo">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="tachiyomi" />
|
||||
<data android:host="add-repo" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deep link to add manga repos -->
|
||||
<intent-filter android:label="@string/action_add_repo">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="aniyomi" />
|
||||
<data android:host="add-repo" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Open backup files -->
|
||||
<intent-filter android:label="@string/pref_restore_backup">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="*/*" />
|
||||
<!--
|
||||
Work around Android's ugly primitive PatternMatcher
|
||||
implementation that can't cope with finding a . early in
|
||||
the path unless it's explicitly matched.
|
||||
|
||||
See https://stackoverflow.com/a/31028507
|
||||
-->
|
||||
<data android:pathPattern=".*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
|
||||
</intent-filter>
|
||||
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
|
@ -59,17 +116,17 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:process=":error_handler"
|
||||
android:name=".crash.CrashActivity"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:process=":error_handler" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.deeplink.anime.DeepLinkAnimeActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:exported="true"
|
||||
android:label="@string/action_global_anime_search"
|
||||
android:exported="true">
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||
|
@ -93,10 +150,10 @@
|
|||
|
||||
<activity
|
||||
android:name=".ui.deeplink.manga.DeepLinkMangaActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:exported="true"
|
||||
android:label="@string/action_global_manga_search"
|
||||
android:exported="true">
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||
|
@ -124,14 +181,15 @@
|
|||
|
||||
<activity
|
||||
android:name=".ui.reader.ReaderActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/s_pen_actions"/>
|
||||
<meta-data
|
||||
android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/s_pen_actions" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.player.PlayerActivity"
|
||||
|
@ -151,8 +209,8 @@
|
|||
</activity>
|
||||
<activity
|
||||
android:name=".ui.security.UnlockActivity"
|
||||
android:theme="@style/Theme.Tachiyomi"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Tachiyomi" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
|
@ -161,39 +219,31 @@
|
|||
|
||||
<activity
|
||||
android:name=".extension.manga.util.MangaExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<activity
|
||||
android:name=".extension.anime.util.AnimeExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.setting.track.TrackLoginActivity"
|
||||
android:label="@string/track_activity_name"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:label="@string/track_activity_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="anilist-auth"/>
|
||||
<data android:host="bangumi-auth"/>
|
||||
<data android:host="myanimelist-auth"/>
|
||||
<data android:host="shikimori-auth"/>
|
||||
|
||||
<data android:scheme="tachiyomi"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="simkl-auth"/>
|
||||
<data android:scheme="aniyomi"/>
|
||||
|
||||
<data android:host="myanimelist-auth" />
|
||||
<data android:host="anilist-auth" />
|
||||
<data android:host="bangumi-auth" />
|
||||
<data android:host="shikimori-auth" />
|
||||
<data android:host="simkl-auth"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
@ -201,17 +251,15 @@
|
|||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="shortService" />
|
||||
|
||||
<service
|
||||
android:name=".extension.anime.util.AnimeExtensionInstallService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="shortService" />
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
|
@ -239,9 +287,9 @@
|
|||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
android:multiprocess="false"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:multiprocess="false"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
|
||||
<meta-data
|
||||
|
|
|
@ -8,12 +8,20 @@ import eu.kanade.domain.entries.manga.interactor.GetExcludedScanlators
|
|||
import eu.kanade.domain.entries.manga.interactor.SetExcludedScanlators
|
||||
import eu.kanade.domain.entries.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.extension.anime.interactor.CreateAnimeExtensionRepo
|
||||
import eu.kanade.domain.extension.anime.interactor.DeleteAnimeExtensionRepo
|
||||
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionLanguages
|
||||
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionRepos
|
||||
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionSources
|
||||
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionsByType
|
||||
import eu.kanade.domain.extension.anime.interactor.TrustAnimeExtension
|
||||
import eu.kanade.domain.extension.manga.interactor.CreateMangaExtensionRepo
|
||||
import eu.kanade.domain.extension.manga.interactor.DeleteMangaExtensionRepo
|
||||
import eu.kanade.domain.extension.manga.interactor.GetExtensionSources
|
||||
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionLanguages
|
||||
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionRepos
|
||||
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionsByType
|
||||
import eu.kanade.domain.extension.manga.interactor.TrustMangaExtension
|
||||
import eu.kanade.domain.items.chapter.interactor.GetAvailableScanlators
|
||||
import eu.kanade.domain.items.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource
|
||||
|
@ -84,19 +92,19 @@ import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory
|
|||
import tachiyomi.domain.category.manga.repository.MangaCategoryRepository
|
||||
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeByUrlAndSourceId
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
|
||||
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.GetMangaByUrlAndSourceId
|
||||
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||
import tachiyomi.domain.entries.manga.interactor.GetAnimeByUrlAndSourceId
|
||||
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetMangaByUrlAndSourceId
|
||||
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
|
||||
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
||||
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||
|
@ -322,5 +330,14 @@ class DomainModule : InjektModule {
|
|||
addFactory { ToggleLanguage(get()) }
|
||||
addFactory { ToggleMangaSource(get()) }
|
||||
addFactory { ToggleMangaSourcePin(get()) }
|
||||
addFactory { TrustAnimeExtension(get()) }
|
||||
addFactory { TrustMangaExtension(get()) }
|
||||
|
||||
addFactory { CreateMangaExtensionRepo(get()) }
|
||||
addFactory { DeleteMangaExtensionRepo(get()) }
|
||||
addFactory { GetMangaExtensionRepos(get()) }
|
||||
addFactory { CreateAnimeExtensionRepo(get()) }
|
||||
addFactory { DeleteAnimeExtensionRepo(get()) }
|
||||
addFactory { GetAnimeExtensionRepos(get()) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,10 +35,10 @@ class BasePreferences(
|
|||
|
||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||
|
||||
enum class ExtensionInstaller(val titleRes: StringResource) {
|
||||
LEGACY(MR.strings.ext_installer_legacy),
|
||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),
|
||||
SHIZUKU(MR.strings.ext_installer_shizuku),
|
||||
PRIVATE(MR.strings.ext_installer_private),
|
||||
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||
LEGACY(MR.strings.ext_installer_legacy, true),
|
||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
|
||||
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||
PRIVATE(MR.strings.ext_installer_private, false),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,9 +81,9 @@ class UpdateAnime(
|
|||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
window: Pair<Long, Long> = animeFetchInterval.getWindow(dateTime),
|
||||
): Boolean {
|
||||
return animeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
|
||||
?.let { animeRepository.updateAnime(it) }
|
||||
?: false
|
||||
return animeRepository.updateAnime(
|
||||
animeFetchInterval.toAnimeUpdate(anime, dateTime, window),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {
|
||||
|
|
|
@ -81,9 +81,9 @@ class UpdateManga(
|
|||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
window: Pair<Long, Long> = mangaFetchInterval.getWindow(dateTime),
|
||||
): Boolean {
|
||||
return mangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||
?.let { mangaRepository.updateManga(it) }
|
||||
?: false
|
||||
return mangaRepository.updateManga(
|
||||
mangaFetchInterval.toMangaUpdate(manga, dateTime, window),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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()
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -22,7 +22,6 @@ import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
|||
import tachiyomi.domain.items.chapter.service.ChapterRecognition
|
||||
import tachiyomi.source.local.entries.manga.isLocal
|
||||
import java.lang.Long.max
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TreeSet
|
||||
|
||||
|
@ -57,6 +56,7 @@ class SyncChaptersWithSource(
|
|||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val nowMillis = now.toInstant().toEpochMilli()
|
||||
|
||||
val sourceChapters = rawSourceChapters
|
||||
.distinctBy { it.url }
|
||||
|
@ -67,36 +67,27 @@ class SyncChaptersWithSource(
|
|||
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
||||
}
|
||||
|
||||
// Chapters from db.
|
||||
val dbChapters = getChaptersByMangaId.await(manga.id)
|
||||
|
||||
// Chapters from the source not in db.
|
||||
val toAdd = mutableListOf<Chapter>()
|
||||
|
||||
// Chapters whose metadata have changed.
|
||||
val toChange = mutableListOf<Chapter>()
|
||||
|
||||
// Chapters from the db not in source.
|
||||
val toDelete = dbChapters.filterNot { dbChapter ->
|
||||
val newChapters = mutableListOf<Chapter>()
|
||||
val updatedChapters = mutableListOf<Chapter>()
|
||||
val removedChapters = dbChapters.filterNot { dbChapter ->
|
||||
sourceChapters.any { sourceChapter ->
|
||||
dbChapter.url == sourceChapter.url
|
||||
}
|
||||
}
|
||||
|
||||
val rightNow = Instant.now().toEpochMilli()
|
||||
|
||||
// Used to not set upload date of older chapters
|
||||
// to a higher value than newer chapters
|
||||
var maxSeenUploadDate = 0L
|
||||
|
||||
val sManga = manga.toSManga()
|
||||
for (sourceChapter in sourceChapters) {
|
||||
var chapter = sourceChapter
|
||||
|
||||
// Update metadata from source if necessary.
|
||||
if (source is HttpSource) {
|
||||
val sChapter = chapter.toSChapter()
|
||||
source.prepareNewChapter(sChapter, sManga)
|
||||
source.prepareNewChapter(sChapter, manga.toSManga())
|
||||
chapter = chapter.copyFromSChapter(sChapter)
|
||||
}
|
||||
|
||||
|
@ -112,13 +103,13 @@ class SyncChaptersWithSource(
|
|||
|
||||
if (dbChapter == null) {
|
||||
val toAddChapter = if (chapter.dateUpload == 0L) {
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
|
||||
chapter.copy(dateUpload = altDateUpload)
|
||||
} else {
|
||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
||||
chapter
|
||||
}
|
||||
toAdd.add(toAddChapter)
|
||||
newChapters.add(toAddChapter)
|
||||
} else {
|
||||
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
|
||||
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(
|
||||
|
@ -144,13 +135,13 @@ class SyncChaptersWithSource(
|
|||
if (chapter.dateUpload != 0L) {
|
||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||
}
|
||||
toChange.add(toChangeChapter)
|
||||
updatedChapters.add(toChangeChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
|
||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga,
|
||||
|
@ -167,20 +158,20 @@ class SyncChaptersWithSource(
|
|||
val deletedReadChapterNumbers = TreeSet<Double>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
||||
|
||||
toDelete.forEach { chapter ->
|
||||
removedChapters.forEach { chapter ->
|
||||
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
||||
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
||||
deletedChapterNumbers.add(chapter.chapterNumber)
|
||||
}
|
||||
|
||||
val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
|
||||
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
|
||||
.associate { it.chapterNumber to it.dateFetch }
|
||||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// Sources MUST return the chapters from most to less recent, which is common.
|
||||
var itemCount = toAdd.size
|
||||
var updatedToAdd = toAdd.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
||||
var itemCount = newChapters.size
|
||||
var updatedToAdd = newChapters.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||
|
||||
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
||||
|
||||
|
@ -199,8 +190,8 @@ class SyncChaptersWithSource(
|
|||
chapter
|
||||
}
|
||||
|
||||
if (toDelete.isNotEmpty()) {
|
||||
val toDeleteIds = toDelete.map { it.id }
|
||||
if (removedChapters.isNotEmpty()) {
|
||||
val toDeleteIds = removedChapters.map { it.id }
|
||||
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
||||
}
|
||||
|
||||
|
@ -208,8 +199,8 @@ class SyncChaptersWithSource(
|
|||
updatedToAdd = chapterRepository.addAllChapters(updatedToAdd)
|
||||
}
|
||||
|
||||
if (toChange.isNotEmpty()) {
|
||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||
if (updatedChapters.isNotEmpty()) {
|
||||
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||
|
|
|
@ -21,7 +21,6 @@ import tachiyomi.domain.items.episode.repository.EpisodeRepository
|
|||
import tachiyomi.domain.items.episode.service.EpisodeRecognition
|
||||
import tachiyomi.source.local.entries.anime.isLocal
|
||||
import java.lang.Long.max
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TreeSet
|
||||
|
||||
|
@ -55,6 +54,7 @@ class SyncEpisodesWithSource(
|
|||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val nowMillis = now.toInstant().toEpochMilli()
|
||||
|
||||
val sourceEpisodes = rawSourceEpisodes
|
||||
.distinctBy { it.url }
|
||||
|
@ -65,36 +65,27 @@ class SyncEpisodesWithSource(
|
|||
.copy(animeId = anime.id, sourceOrder = i.toLong())
|
||||
}
|
||||
|
||||
// Episodes from db.
|
||||
val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
|
||||
|
||||
// Episodes from the source not in db.
|
||||
val toAdd = mutableListOf<Episode>()
|
||||
|
||||
// Episodes whose metadata have changed.
|
||||
val toChange = mutableListOf<Episode>()
|
||||
|
||||
// Episodes from the db not in source.
|
||||
val toDelete = dbEpisodes.filterNot { dbEpisode ->
|
||||
val newEpisodes = mutableListOf<Episode>()
|
||||
val updatedEpisodes = mutableListOf<Episode>()
|
||||
val removedEpisodes = dbEpisodes.filterNot { dbEpisode ->
|
||||
sourceEpisodes.any { sourceEpisode ->
|
||||
dbEpisode.url == sourceEpisode.url
|
||||
}
|
||||
}
|
||||
|
||||
val rightNow = Instant.now().toEpochMilli()
|
||||
|
||||
// Used to not set upload date of older episodes
|
||||
// to a higher value than newer episodes
|
||||
var maxSeenUploadDate = 0L
|
||||
|
||||
val sAnime = anime.toSAnime()
|
||||
for (sourceEpisode in sourceEpisodes) {
|
||||
var episode = sourceEpisode
|
||||
|
||||
// Update metadata from source if necessary.
|
||||
if (source is AnimeHttpSource) {
|
||||
val sEpisode = episode.toSEpisode()
|
||||
source.prepareNewEpisode(sEpisode, sAnime)
|
||||
source.prepareNewEpisode(sEpisode, anime.toSAnime())
|
||||
episode = episode.copyFromSEpisode(sEpisode)
|
||||
}
|
||||
|
||||
|
@ -110,13 +101,13 @@ class SyncEpisodesWithSource(
|
|||
|
||||
if (dbEpisode == null) {
|
||||
val toAddEpisode = if (episode.dateUpload == 0L) {
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
|
||||
episode.copy(dateUpload = altDateUpload)
|
||||
} else {
|
||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceEpisode.dateUpload)
|
||||
episode
|
||||
}
|
||||
toAdd.add(toAddEpisode)
|
||||
newEpisodes.add(toAddEpisode)
|
||||
} else {
|
||||
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
|
||||
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(
|
||||
|
@ -144,13 +135,13 @@ class SyncEpisodesWithSource(
|
|||
dateUpload = sourceEpisode.dateUpload,
|
||||
)
|
||||
}
|
||||
toChange.add(toChangeEpisode)
|
||||
updatedEpisodes.add(toChangeEpisode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||
if (newEpisodes.isEmpty() && removedEpisodes.isEmpty() && updatedEpisodes.isEmpty()) {
|
||||
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
|
||||
updateAnime.awaitUpdateFetchInterval(
|
||||
anime,
|
||||
|
@ -167,20 +158,20 @@ class SyncEpisodesWithSource(
|
|||
val deletedSeenEpisodeNumbers = TreeSet<Double>()
|
||||
val deletedBookmarkedEpisodeNumbers = TreeSet<Double>()
|
||||
|
||||
toDelete.forEach { episode ->
|
||||
removedEpisodes.forEach { episode ->
|
||||
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
|
||||
if (episode.bookmark) deletedBookmarkedEpisodeNumbers.add(episode.episodeNumber)
|
||||
deletedEpisodeNumbers.add(episode.episodeNumber)
|
||||
}
|
||||
|
||||
val deletedEpisodeNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
|
||||
val deletedEpisodeNumberDateFetchMap = removedEpisodes.sortedByDescending { it.dateFetch }
|
||||
.associate { it.episodeNumber to it.dateFetch }
|
||||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// Sources MUST return the episodes from most to less recent, which is common.
|
||||
var itemCount = toAdd.size
|
||||
var updatedToAdd = toAdd.map { toAddItem ->
|
||||
var episode = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
||||
var itemCount = newEpisodes.size
|
||||
var updatedToAdd = newEpisodes.map { toAddItem ->
|
||||
var episode = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||
|
||||
if (episode.isRecognizedNumber.not() || episode.episodeNumber !in deletedEpisodeNumbers) return@map episode
|
||||
|
||||
|
@ -199,8 +190,8 @@ class SyncEpisodesWithSource(
|
|||
episode
|
||||
}
|
||||
|
||||
if (toDelete.isNotEmpty()) {
|
||||
val toDeleteIds = toDelete.map { it.id }
|
||||
if (removedEpisodes.isNotEmpty()) {
|
||||
val toDeleteIds = removedEpisodes.map { it.id }
|
||||
episodeRepository.removeEpisodesWithIds(toDeleteIds)
|
||||
}
|
||||
|
||||
|
@ -208,8 +199,8 @@ class SyncEpisodesWithSource(
|
|||
updatedToAdd = episodeRepository.addAllEpisodes(updatedToAdd)
|
||||
}
|
||||
|
||||
if (toChange.isNotEmpty()) {
|
||||
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
|
||||
if (updatedEpisodes.isNotEmpty()) {
|
||||
val episodeUpdates = updatedEpisodes.map { it.toEpisodeUpdate() }
|
||||
updateEpisode.awaitAll(episodeUpdates)
|
||||
}
|
||||
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)
|
||||
|
|
|
@ -37,7 +37,14 @@ class SourcePreferences(
|
|||
SetMigrateSorting.Direction.ASCENDING,
|
||||
)
|
||||
|
||||
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
|
||||
fun animeExtensionRepos() = preferenceStore.getStringSet("anime_extension_repos", emptySet())
|
||||
|
||||
fun mangaExtensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||
|
||||
fun trustedExtensions() = preferenceStore.getStringSet(
|
||||
Preference.appStateKey("trusted_extensions"),
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
// Mixture Sources
|
||||
|
||||
|
|
|
@ -25,14 +25,14 @@ class RefreshAnimeTracks(
|
|||
suspend fun await(animeId: Long): List<Pair<Tracker?, Throwable>> {
|
||||
return supervisorScope {
|
||||
return@supervisorScope getTracks.await(animeId)
|
||||
.map { it to trackerManager.get(it.syncId) }
|
||||
.map { it to trackerManager.get(it.trackerId) }
|
||||
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||
.map { (track, service) ->
|
||||
async {
|
||||
return@async try {
|
||||
val updatedTrack = service!!.animeService.refresh(track.toDbTrack())
|
||||
insertTrack.await(updatedTrack.toDomainTrack()!!)
|
||||
syncEpisodeProgressWithTrack.await(animeId, track, service.animeService)
|
||||
val updatedTrack = service!!.animeService.refresh(track.toDbTrack()).toDomainTrack()!!
|
||||
insertTrack.await(updatedTrack)
|
||||
syncEpisodeProgressWithTrack.await(animeId, updatedTrack, service.animeService)
|
||||
null
|
||||
} catch (e: Throwable) {
|
||||
service to e
|
||||
|
|
|
@ -28,7 +28,7 @@ class TrackEpisode(
|
|||
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||
|
||||
tracks.mapNotNull { track ->
|
||||
val service = trackerManager.get(track.syncId)
|
||||
val service = trackerManager.get(track.trackerId)
|
||||
if (service == null || !service.isLoggedIn || episodeNumber <= track.lastEpisodeSeen) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
|
|
@ -13,10 +13,10 @@ fun AnimeTrack.copyPersonalFrom(other: AnimeTrack): AnimeTrack {
|
|||
)
|
||||
}
|
||||
|
||||
fun AnimeTrack.toDbTrack(): DbAnimeTrack = DbAnimeTrack.create(syncId).also {
|
||||
fun AnimeTrack.toDbTrack(): DbAnimeTrack = DbAnimeTrack.create(trackerId).also {
|
||||
it.id = id
|
||||
it.anime_id = animeId
|
||||
it.media_id = remoteId
|
||||
it.remote_id = remoteId
|
||||
it.library_id = libraryId
|
||||
it.title = title
|
||||
it.last_episode_seen = lastEpisodeSeen.toFloat()
|
||||
|
@ -33,14 +33,16 @@ fun DbAnimeTrack.toDomainTrack(idRequired: Boolean = true): AnimeTrack? {
|
|||
return AnimeTrack(
|
||||
id = trackId,
|
||||
animeId = anime_id,
|
||||
syncId = sync_id.toLong(),
|
||||
remoteId = media_id,
|
||||
trackerId = tracker_id.toLong(),
|
||||
remoteId = remote_id,
|
||||
libraryId = library_id,
|
||||
title = title,
|
||||
lastEpisodeSeen = last_episode_seen.toDouble(),
|
||||
totalEpisodes = total_episodes.toLong(),
|
||||
status = status.toLong(),
|
||||
score = score.toDouble(),
|
||||
// Jank workaround due to precision issues while converting
|
||||
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
|
||||
score = score.toString().toDouble(),
|
||||
remoteUrl = tracking_url,
|
||||
startDate = started_watching_date,
|
||||
finishDate = finished_watching_date,
|
||||
|
|
|
@ -25,14 +25,14 @@ class RefreshMangaTracks(
|
|||
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
|
||||
return supervisorScope {
|
||||
return@supervisorScope getTracks.await(mangaId)
|
||||
.map { it to trackerManager.get(it.syncId) }
|
||||
.map { it to trackerManager.get(it.trackerId) }
|
||||
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||
.map { (track, service) ->
|
||||
async {
|
||||
return@async try {
|
||||
val updatedTrack = service!!.mangaService.refresh(track.toDbTrack())
|
||||
insertTrack.await(updatedTrack.toDomainTrack()!!)
|
||||
syncChapterProgressWithTrack.await(mangaId, track, service.mangaService)
|
||||
val updatedTrack = service!!.mangaService.refresh(track.toDbTrack()).toDomainTrack()!!
|
||||
insertTrack.await(updatedTrack)
|
||||
syncChapterProgressWithTrack.await(mangaId, updatedTrack, service.mangaService)
|
||||
null
|
||||
} catch (e: Throwable) {
|
||||
service to e
|
||||
|
|
|
@ -27,7 +27,7 @@ class TrackChapter(
|
|||
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||
|
||||
tracks.mapNotNull { track ->
|
||||
val service = trackerManager.get(track.syncId)
|
||||
val service = trackerManager.get(track.trackerId)
|
||||
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
||||
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
||||
return@mapNotNull null
|
||||
|
|
|
@ -13,10 +13,10 @@ fun MangaTrack.copyPersonalFrom(other: MangaTrack): MangaTrack {
|
|||
)
|
||||
}
|
||||
|
||||
fun MangaTrack.toDbTrack(): DbMangaTrack = DbMangaTrack.create(syncId).also {
|
||||
fun MangaTrack.toDbTrack(): DbMangaTrack = DbMangaTrack.create(trackerId).also {
|
||||
it.id = id
|
||||
it.manga_id = mangaId
|
||||
it.media_id = remoteId
|
||||
it.remote_id = remoteId
|
||||
it.library_id = libraryId
|
||||
it.title = title
|
||||
it.last_chapter_read = lastChapterRead.toFloat()
|
||||
|
@ -33,14 +33,16 @@ fun DbMangaTrack.toDomainTrack(idRequired: Boolean = true): MangaTrack? {
|
|||
return MangaTrack(
|
||||
id = trackId,
|
||||
mangaId = manga_id,
|
||||
syncId = sync_id.toLong(),
|
||||
remoteId = media_id,
|
||||
trackerId = tracker_id.toLong(),
|
||||
remoteId = remote_id,
|
||||
libraryId = library_id,
|
||||
title = title,
|
||||
lastChapterRead = last_chapter_read.toDouble(),
|
||||
totalChapters = total_chapters.toLong(),
|
||||
status = status.toLong(),
|
||||
score = score.toDouble(),
|
||||
// Jank workaround due to precision issues while converting
|
||||
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
|
||||
score = score.toString().toDouble(),
|
||||
remoteUrl = tracking_url,
|
||||
startDate = started_reading_date,
|
||||
finishDate = finished_reading_date,
|
||||
|
|
|
@ -2,22 +2,35 @@ package eu.kanade.domain.track.service
|
|||
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import tachiyomi.core.preference.Preference
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class TrackPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
|
||||
fun trackUsername(tracker: Tracker) = preferenceStore.getString(
|
||||
Preference.privateKey("pref_mangasync_username_${tracker.id}"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
|
||||
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
|
||||
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun setCredentials(sync: Tracker, username: String, password: String) {
|
||||
trackUsername(sync).set(username)
|
||||
trackPassword(sync).set(password)
|
||||
fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean(
|
||||
Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"),
|
||||
false,
|
||||
)
|
||||
|
||||
fun setCredentials(tracker: Tracker, username: String, password: String) {
|
||||
trackUsername(tracker).set(username)
|
||||
trackPassword(tracker).set(password)
|
||||
trackAuthExpired(tracker).set(false)
|
||||
}
|
||||
|
||||
fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
|
||||
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
|
||||
|
||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
|
||||
|
@ -29,12 +42,4 @@ class TrackPreferences(
|
|||
"show_next_episode_airing_time",
|
||||
true,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
|
||||
|
||||
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
||||
|
||||
private fun trackToken(syncId: Long) = "track_token_$syncId"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package eu.kanade.domain.ui
|
|||
|
||||
import android.os.Build
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.domain.ui.model.NavStyle
|
||||
import eu.kanade.domain.ui.model.StartScreen
|
||||
import eu.kanade.domain.ui.model.TabletUiMode
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
|
@ -34,6 +36,10 @@ class UiPreferences(
|
|||
|
||||
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||
|
||||
fun startScreen() = preferenceStore.getEnum("start_screen", StartScreen.ANIME)
|
||||
|
||||
fun navStyle() = preferenceStore.getEnum("bottom_rail_nav_style", NavStyle.MOVE_HISTORY_TO_MORE)
|
||||
|
||||
companion object {
|
||||
fun dateFormat(format: String): DateFormat = when (format) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
|
|
|
@ -15,6 +15,7 @@ enum class AppTheme(val titleRes: StringResource?) {
|
|||
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
||||
MOCHA(MR.strings.theme_mocha),
|
||||
SAPPHIRE(MR.strings.theme_sapphire),
|
||||
NORD(MR.strings.theme_nord),
|
||||
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
||||
TAKO(MR.strings.theme_tako),
|
||||
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
||||
|
|
48
app/src/main/java/eu/kanade/domain/ui/model/NavStyle.kt
Normal file
48
app/src/main/java/eu/kanade/domain/ui/model/NavStyle.kt
Normal 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) }
|
||||
}
|
||||
}
|
18
app/src/main/java/eu/kanade/domain/ui/model/StartScreen.kt
Normal file
18
app/src/main/java/eu/kanade/domain/ui/model/StartScreen.kt
Normal 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()),
|
||||
}
|
|
@ -39,7 +39,7 @@ fun GlobalSearchResultItem(
|
|||
modifier = Modifier
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.tiny,
|
||||
end = MaterialTheme.padding.extraSmall,
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
|
|
|
@ -16,8 +16,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
|
@ -36,6 +35,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -52,6 +52,7 @@ import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
|||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeExtensionDetailsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
|
@ -65,14 +66,23 @@ fun AnimeExtensionDetailsScreen(
|
|||
navigateUp: () -> Unit,
|
||||
state: AnimeExtensionDetailsScreenModel.State,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickWhatsNew: () -> Unit,
|
||||
onClickReadme: () -> Unit,
|
||||
onClickEnableAll: () -> Unit,
|
||||
onClickDisableAll: () -> Unit,
|
||||
onClickClearCookies: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = remember(state.extension) {
|
||||
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
||||
regex.find(state.extension?.repoUrl.orEmpty())
|
||||
?.let {
|
||||
val (user, repo) = it.destructured
|
||||
"https://github.com/$user/$repo"
|
||||
}
|
||||
?: state.extension?.repoUrl
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
|
@ -82,19 +92,14 @@ fun AnimeExtensionDetailsScreen(
|
|||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
if (state.extension?.isUnofficial == false) {
|
||||
if (url != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.whats_new),
|
||||
icon = Icons.Outlined.History,
|
||||
onClick = onClickWhatsNew,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_faq_and_guides),
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
onClick = onClickReadme,
|
||||
title = stringResource(MR.strings.action_open_repo),
|
||||
icon = Icons.AutoMirrored.Outlined.Launch,
|
||||
onClick = {
|
||||
uriHandler.openUri(url)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -124,7 +129,7 @@ fun AnimeExtensionDetailsScreen(
|
|||
) { paddingValues ->
|
||||
if (state.extension == null) {
|
||||
EmptyScreen(
|
||||
stringRes = MR.strings.empty_screen,
|
||||
MR.strings.empty_screen,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
|
@ -145,7 +150,7 @@ fun AnimeExtensionDetailsScreen(
|
|||
private fun AnimeExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
extension: AnimeExtension.Installed,
|
||||
sources: List<AnimeExtensionSourceItem>,
|
||||
sources: ImmutableList<AnimeExtensionSourceItem>,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
|
@ -156,15 +161,10 @@ private fun AnimeExtensionDetails(
|
|||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
when {
|
||||
extension.isUnofficial ->
|
||||
item {
|
||||
WarningBanner(MR.strings.unofficial_anime_extension_message)
|
||||
}
|
||||
extension.isObsolete ->
|
||||
item {
|
||||
WarningBanner(MR.strings.obsolete_extension_message)
|
||||
}
|
||||
if (extension.isObsolete) {
|
||||
item {
|
||||
WarningBanner(MR.strings.obsolete_extension_message)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
|
@ -296,7 +296,7 @@ private fun DetailsHeader(
|
|||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.presentation.browse.anime
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
@ -37,16 +38,20 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.core.util.fastDistinctBy
|
||||
import eu.kanade.presentation.browse.BaseBrowseItem
|
||||
import eu.kanade.presentation.browse.anime.components.AnimeExtensionIcon
|
||||
import eu.kanade.presentation.browse.manga.ExtensionHeader
|
||||
import eu.kanade.presentation.browse.manga.ExtensionTrustDialog
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
|
@ -65,7 +70,7 @@ fun AnimeExtensionScreen(
|
|||
searchQuery: String?,
|
||||
onLongClickItem: (AnimeExtension) -> Unit,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||
onClickItemWebView: (AnimeExtension.Available) -> Unit,
|
||||
onOpenWebView: (AnimeExtension.Available) -> Unit,
|
||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||
onUninstallExtension: (AnimeExtension) -> Unit,
|
||||
onUpdateExtension: (AnimeExtension.Installed) -> Unit,
|
||||
|
@ -98,7 +103,7 @@ fun AnimeExtensionScreen(
|
|||
contentPadding = contentPadding,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onOpenWebView = onOpenWebView,
|
||||
onInstallExtension = onInstallExtension,
|
||||
onUninstallExtension = onUninstallExtension,
|
||||
onUpdateExtension = onUpdateExtension,
|
||||
|
@ -116,7 +121,7 @@ private fun AnimeExtensionContent(
|
|||
state: AnimeExtensionsScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (AnimeExtension) -> Unit,
|
||||
onClickItemWebView: (AnimeExtension.Available) -> Unit,
|
||||
onOpenWebView: (AnimeExtension.Available) -> Unit,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||
onUninstallExtension: (AnimeExtension) -> Unit,
|
||||
|
@ -125,11 +130,24 @@ private fun AnimeExtensionContent(
|
|||
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var trustState by remember { mutableStateOf<AnimeExtension.Untrusted?>(null) }
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
||||
item(key = "extension-permissions-warning") {
|
||||
WarningBanner(
|
||||
textRes = MR.strings.ext_permission_install_apps_warning,
|
||||
modifier = Modifier.clickable {
|
||||
context.launchRequestPackageInstallsPermission()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.items.forEach { (header, items) ->
|
||||
item(
|
||||
contentType = "header",
|
||||
|
@ -168,7 +186,7 @@ private fun AnimeExtensionContent(
|
|||
}
|
||||
|
||||
items(
|
||||
items = items,
|
||||
items = items.fastDistinctBy { it.hashCode() },
|
||||
contentType = { "item" },
|
||||
key = { "extension-${it.hashCode()}" },
|
||||
) { item ->
|
||||
|
@ -183,8 +201,14 @@ private fun AnimeExtensionContent(
|
|||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemSecondaryAction = {
|
||||
when (it) {
|
||||
is AnimeExtension.Available -> onOpenWebView(it)
|
||||
is AnimeExtension.Installed -> onOpenExtension(it)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
is AnimeExtension.Available -> onInstallExtension(it)
|
||||
|
@ -227,10 +251,10 @@ private fun AnimeExtensionItem(
|
|||
item: AnimeExtensionUiModel.Item,
|
||||
onClickItem: (AnimeExtension) -> Unit,
|
||||
onLongClickItem: (AnimeExtension) -> Unit,
|
||||
onClickItemWebView: (AnimeExtension.Available) -> Unit,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||
onClickItemAction: (AnimeExtension) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItemSecondaryAction: (AnimeExtension) -> Unit,
|
||||
) {
|
||||
val (extension, installStep) = item
|
||||
BaseBrowseItem(
|
||||
|
@ -271,9 +295,9 @@ private fun AnimeExtensionItem(
|
|||
AnimeExtensionItemActions(
|
||||
extension = extension,
|
||||
installStep = installStep,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = onClickItemAction,
|
||||
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -303,7 +327,7 @@ private fun AnimeExtensionItemContent(
|
|||
// Won't look good but it's not like we can ellipsize overflowing content
|
||||
FlowRow(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
if (extension is AnimeExtension.Installed && extension.lang.isNotEmpty()) {
|
||||
|
@ -323,7 +347,6 @@ private fun AnimeExtensionItemContent(
|
|||
|
||||
val warning = when {
|
||||
extension is AnimeExtension.Untrusted -> MR.strings.ext_untrusted
|
||||
extension is AnimeExtension.Installed && extension.isUnofficial -> MR.strings.ext_unofficial
|
||||
extension is AnimeExtension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
||||
extension.isNsfw -> MR.strings.ext_nsfw_short
|
||||
else -> null
|
||||
|
@ -358,15 +381,15 @@ private fun AnimeExtensionItemActions(
|
|||
extension: AnimeExtension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItemWebView: (AnimeExtension.Available) -> Unit = {},
|
||||
onClickItemCancel: (AnimeExtension) -> Unit = {},
|
||||
onClickItemAction: (AnimeExtension) -> Unit = {},
|
||||
onClickItemSecondaryAction: (AnimeExtension) -> Unit = {},
|
||||
) {
|
||||
val isIdle = installStep.isCompleted()
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
when {
|
||||
!isIdle -> {
|
||||
|
@ -388,6 +411,13 @@ private fun AnimeExtensionItemActions(
|
|||
installStep == InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is AnimeExtension.Installed -> {
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.hasUpdate) {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
|
@ -396,13 +426,6 @@ private fun AnimeExtensionItemActions(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
}
|
||||
is AnimeExtension.Untrusted -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
|
@ -415,7 +438,7 @@ private fun AnimeExtensionItemActions(
|
|||
is AnimeExtension.Available -> {
|
||||
if (extension.sources.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = { onClickItemWebView(extension) },
|
||||
onClick = { onClickItemSecondaryAction(extension) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Public,
|
||||
|
|
|
@ -74,9 +74,10 @@ internal fun GlobalSearchContent(
|
|||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = fromSourceId
|
||||
?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
title = fromSourceId?.let {
|
||||
"▶ ${source.name}".takeIf { source.id == fromSourceId }
|
||||
} ?: source.name,
|
||||
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
when (result) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import eu.kanade.presentation.browse.anime.components.AnimeSourceIcon
|
|||
import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.migration.sources.MigrateAnimeSourceScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.Badge
|
||||
|
@ -75,7 +76,7 @@ fun MigrateAnimeSourceScreen(
|
|||
|
||||
@Composable
|
||||
private fun MigrateAnimeSourceList(
|
||||
list: List<Pair<AnimeSource, Long>>,
|
||||
list: ImmutableList<Pair<AnimeSource, Long>>,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (AnimeSource) -> Unit,
|
||||
onLongClickItem: (AnimeSource) -> Unit,
|
||||
|
|
|
@ -38,7 +38,7 @@ fun GlobalAnimeSearchCardRow(
|
|||
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
items(titles) {
|
||||
val title by getAnime(it)
|
||||
|
|
|
@ -74,9 +74,10 @@ internal fun GlobalSearchContent(
|
|||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = fromSourceId
|
||||
?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
title = fromSourceId?.let {
|
||||
"▶ ${source.name}".takeIf { source.id == fromSourceId }
|
||||
} ?: source.name,
|
||||
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
when (result) {
|
||||
|
|
|
@ -16,8 +16,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
|
@ -38,6 +37,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -53,6 +53,7 @@ import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
|||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaExtensionDetailsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
|
@ -62,18 +63,27 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
|
||||
@Composable
|
||||
fun ExtensionDetailsScreen(
|
||||
fun MangaExtensionDetailsScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: MangaExtensionDetailsScreenModel.State,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickWhatsNew: () -> Unit,
|
||||
onClickReadme: () -> Unit,
|
||||
onClickEnableAll: () -> Unit,
|
||||
onClickDisableAll: () -> Unit,
|
||||
onClickClearCookies: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = remember(state.extension) {
|
||||
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
||||
regex.find(state.extension?.repoUrl.orEmpty())
|
||||
?.let {
|
||||
val (user, repo) = it.destructured
|
||||
"https://github.com/$user/$repo"
|
||||
}
|
||||
?: state.extension?.repoUrl
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
|
@ -83,19 +93,14 @@ fun ExtensionDetailsScreen(
|
|||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
if (state.extension?.isUnofficial == false) {
|
||||
if (url != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.whats_new),
|
||||
icon = Icons.Outlined.History,
|
||||
onClick = onClickWhatsNew,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_faq_and_guides),
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
onClick = onClickReadme,
|
||||
title = stringResource(MR.strings.action_open_repo),
|
||||
icon = Icons.AutoMirrored.Outlined.Launch,
|
||||
onClick = {
|
||||
uriHandler.openUri(url)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -125,7 +130,7 @@ fun ExtensionDetailsScreen(
|
|||
) { paddingValues ->
|
||||
if (state.extension == null) {
|
||||
EmptyScreen(
|
||||
stringRes = MR.strings.empty_screen,
|
||||
MR.strings.empty_screen,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
|
@ -146,7 +151,7 @@ fun ExtensionDetailsScreen(
|
|||
private fun ExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
extension: MangaExtension.Installed,
|
||||
sources: List<MangaExtensionSourceItem>,
|
||||
sources: ImmutableList<MangaExtensionSourceItem>,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
|
@ -157,15 +162,10 @@ private fun ExtensionDetails(
|
|||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
when {
|
||||
extension.isUnofficial ->
|
||||
item {
|
||||
WarningBanner(MR.strings.unofficial_extension_message)
|
||||
}
|
||||
extension.isObsolete ->
|
||||
item {
|
||||
WarningBanner(MR.strings.obsolete_extension_message)
|
||||
}
|
||||
if (extension.isObsolete) {
|
||||
item {
|
||||
WarningBanner(MR.strings.obsolete_extension_message)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
|
@ -295,7 +295,7 @@ private fun DetailsHeader(
|
|||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.presentation.browse.manga
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
@ -40,14 +41,18 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.core.util.fastDistinctBy
|
||||
import eu.kanade.presentation.browse.BaseBrowseItem
|
||||
import eu.kanade.presentation.browse.manga.components.MangaExtensionIcon
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
|
@ -67,7 +72,7 @@ fun MangaExtensionScreen(
|
|||
searchQuery: String?,
|
||||
onLongClickItem: (MangaExtension) -> Unit,
|
||||
onClickItemCancel: (MangaExtension) -> Unit,
|
||||
onClickItemWebView: (MangaExtension.Available) -> Unit,
|
||||
onOpenWebView: (MangaExtension.Available) -> Unit,
|
||||
onInstallExtension: (MangaExtension.Available) -> Unit,
|
||||
onUninstallExtension: (MangaExtension) -> Unit,
|
||||
onUpdateExtension: (MangaExtension.Installed) -> Unit,
|
||||
|
@ -100,7 +105,7 @@ fun MangaExtensionScreen(
|
|||
contentPadding = contentPadding,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onOpenWebView = onOpenWebView,
|
||||
onInstallExtension = onInstallExtension,
|
||||
onUninstallExtension = onUninstallExtension,
|
||||
onUpdateExtension = onUpdateExtension,
|
||||
|
@ -118,7 +123,7 @@ private fun ExtensionContent(
|
|||
state: MangaExtensionsScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (MangaExtension) -> Unit,
|
||||
onClickItemWebView: (MangaExtension.Available) -> Unit,
|
||||
onOpenWebView: (MangaExtension.Available) -> Unit,
|
||||
onClickItemCancel: (MangaExtension) -> Unit,
|
||||
onInstallExtension: (MangaExtension.Available) -> Unit,
|
||||
onUninstallExtension: (MangaExtension) -> Unit,
|
||||
|
@ -127,11 +132,24 @@ private fun ExtensionContent(
|
|||
onOpenExtension: (MangaExtension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var trustState by remember { mutableStateOf<MangaExtension.Untrusted?>(null) }
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
||||
item(key = "extension-permissions-warning") {
|
||||
WarningBanner(
|
||||
textRes = MR.strings.ext_permission_install_apps_warning,
|
||||
modifier = Modifier.clickable {
|
||||
context.launchRequestPackageInstallsPermission()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.items.forEach { (header, items) ->
|
||||
item(
|
||||
contentType = "header",
|
||||
|
@ -170,7 +188,7 @@ private fun ExtensionContent(
|
|||
}
|
||||
|
||||
items(
|
||||
items = items,
|
||||
items = items.fastDistinctBy { it.hashCode() },
|
||||
contentType = { "item" },
|
||||
key = { "extension-${it.hashCode()}" },
|
||||
) { item ->
|
||||
|
@ -185,7 +203,13 @@ private fun ExtensionContent(
|
|||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onClickItemSecondaryAction = {
|
||||
when (it) {
|
||||
is MangaExtension.Available -> onOpenWebView(it)
|
||||
is MangaExtension.Installed -> onOpenExtension(it)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
|
@ -229,9 +253,9 @@ private fun ExtensionItem(
|
|||
item: MangaExtensionUiModel.Item,
|
||||
onClickItem: (MangaExtension) -> Unit,
|
||||
onLongClickItem: (MangaExtension) -> Unit,
|
||||
onClickItemWebView: (MangaExtension.Available) -> Unit,
|
||||
onClickItemCancel: (MangaExtension) -> Unit,
|
||||
onClickItemAction: (MangaExtension) -> Unit,
|
||||
onClickItemSecondaryAction: (MangaExtension) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (extension, installStep) = item
|
||||
|
@ -273,9 +297,9 @@ private fun ExtensionItem(
|
|||
ExtensionItemActions(
|
||||
extension = extension,
|
||||
installStep = installStep,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = onClickItemAction,
|
||||
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -305,7 +329,7 @@ private fun ExtensionItemContent(
|
|||
// Won't look good but it's not like we can ellipsize overflowing content
|
||||
FlowRow(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
if (extension is MangaExtension.Installed && extension.lang.isNotEmpty()) {
|
||||
|
@ -325,7 +349,6 @@ private fun ExtensionItemContent(
|
|||
|
||||
val warning = when {
|
||||
extension is MangaExtension.Untrusted -> MR.strings.ext_untrusted
|
||||
extension is MangaExtension.Installed && extension.isUnofficial -> MR.strings.ext_unofficial
|
||||
extension is MangaExtension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
||||
extension.isNsfw -> MR.strings.ext_nsfw_short
|
||||
else -> null
|
||||
|
@ -360,15 +383,15 @@ private fun ExtensionItemActions(
|
|||
extension: MangaExtension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItemWebView: (MangaExtension.Available) -> Unit = {},
|
||||
onClickItemCancel: (MangaExtension) -> Unit = {},
|
||||
onClickItemAction: (MangaExtension) -> Unit = {},
|
||||
onClickItemSecondaryAction: (MangaExtension) -> Unit = {},
|
||||
) {
|
||||
val isIdle = installStep.isCompleted()
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
when {
|
||||
!isIdle -> {
|
||||
|
@ -390,6 +413,13 @@ private fun ExtensionItemActions(
|
|||
installStep == InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is MangaExtension.Installed -> {
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.hasUpdate) {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
|
@ -398,13 +428,6 @@ private fun ExtensionItemActions(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
}
|
||||
is MangaExtension.Untrusted -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
|
@ -417,7 +440,7 @@ private fun ExtensionItemActions(
|
|||
is MangaExtension.Available -> {
|
||||
if (extension.sources.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = { onClickItemWebView(extension) },
|
||||
onClick = { onClickItemSecondaryAction(extension) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Public,
|
||||
|
|
|
@ -26,6 +26,7 @@ import eu.kanade.presentation.browse.manga.components.BaseMangaSourceItem
|
|||
import eu.kanade.presentation.browse.manga.components.MangaSourceIcon
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.migration.sources.MigrateMangaSourceScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import tachiyomi.domain.source.manga.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.Badge
|
||||
|
@ -75,7 +76,7 @@ fun MigrateMangaSourceScreen(
|
|||
|
||||
@Composable
|
||||
private fun MigrateSourceList(
|
||||
list: List<Pair<Source, Long>>,
|
||||
list: ImmutableList<Pair<Source, Long>>,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source) -> Unit,
|
||||
onLongClickItem: (Source) -> Unit,
|
||||
|
|
|
@ -38,7 +38,7 @@ fun GlobalMangaSearchCardRow(
|
|||
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
items(titles) {
|
||||
val title by getManga(it)
|
||||
|
|
|
@ -27,6 +27,8 @@ import androidx.compose.ui.focus.FocusRequester
|
|||
import androidx.compose.ui.focus.focusRequester
|
||||
import eu.kanade.core.preference.asToggleableState
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.core.preference.CheckboxState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
|
@ -39,12 +41,12 @@ import kotlin.time.Duration.Companion.seconds
|
|||
fun CategoryCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: (String) -> Unit,
|
||||
categories: List<Category>,
|
||||
categories: ImmutableList<String>,
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
||||
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
@ -69,10 +71,13 @@ fun CategoryCreateDialog(
|
|||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text(text = stringResource(MR.strings.name)) },
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.name))
|
||||
},
|
||||
supportingText = {
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||
MR.strings.error_category_exists
|
||||
|
@ -98,14 +103,14 @@ fun CategoryCreateDialog(
|
|||
fun CategoryRenameDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onRename: (String) -> Unit,
|
||||
categories: List<Category>,
|
||||
category: Category,
|
||||
categories: ImmutableList<String>,
|
||||
category: String,
|
||||
) {
|
||||
var name by remember { mutableStateOf(category.name) }
|
||||
var name by remember { mutableStateOf(category) }
|
||||
var valueHasChanged by remember { mutableStateOf(false) }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
||||
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
@ -162,7 +167,7 @@ fun CategoryRenameDialog(
|
|||
fun CategoryDeleteDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
category: Category,
|
||||
category: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
@ -183,7 +188,7 @@ fun CategoryDeleteDialog(
|
|||
Text(text = stringResource(MR.strings.delete_category))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.delete_category_confirmation, category.name))
|
||||
Text(text = stringResource(MR.strings.delete_category_confirmation, category))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -219,7 +224,7 @@ fun CategorySortAlphabeticallyDialog(
|
|||
|
||||
@Composable
|
||||
fun ChangeCategoryDialog(
|
||||
initialSelection: List<CheckboxState<Category>>,
|
||||
initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onEditCategories: () -> Unit,
|
||||
onConfirm: (List<Long>, List<Long>) -> Unit,
|
||||
|
@ -291,7 +296,7 @@ fun ChangeCategoryDialog(
|
|||
if (index != -1) {
|
||||
val mutableList = selection.toMutableList()
|
||||
mutableList[index] = it.next()
|
||||
selection = mutableList.toList()
|
||||
selection = mutableList.toList().toImmutableList()
|
||||
}
|
||||
}
|
||||
Row(
|
||||
|
@ -325,7 +330,3 @@ fun ChangeCategoryDialog(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<Category>.anyWithName(name: String): Boolean {
|
||||
return any { name == it.name }
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.material.icons.outlined.Add
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@ -16,11 +17,13 @@ import tachiyomi.presentation.core.util.isScrollingUp
|
|||
fun CategoryFloatingActionButton(
|
||||
lazyListState: LazyListState,
|
||||
onCreate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
|
||||
onClick = onCreate,
|
||||
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ fun CategoryListItem(
|
|||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Text(
|
||||
text = category.name,
|
||||
modifier = Modifier
|
||||
|
@ -64,13 +64,13 @@ fun CategoryListItem(
|
|||
onClick = { onMoveUp(category) },
|
||||
enabled = canMoveUp,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onMoveDown(category) },
|
||||
enabled = canMoveDown,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onRename) {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
||||
|
@ -22,12 +25,17 @@ import tachiyomi.i18n.MR
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
||||
|
||||
/**
|
||||
* DropdownMenu but overlaps anchor and has width constraints to better
|
||||
* match non-Compose implementation.
|
||||
*/
|
||||
@Composable
|
||||
fun DropdownMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
offset: DpOffset = DpOffset(8.dp, (-56).dp),
|
||||
scrollState: ScrollState = rememberScrollState(),
|
||||
properties: PopupProperties = PopupProperties(focusable = true),
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
|
@ -36,6 +44,7 @@ fun DropdownMenu(
|
|||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
|
||||
offset = offset,
|
||||
scrollState = scrollState,
|
||||
properties = properties,
|
||||
content = content,
|
||||
)
|
||||
|
@ -45,6 +54,7 @@ fun DropdownMenu(
|
|||
fun RadioMenuItem(
|
||||
text: @Composable () -> Unit,
|
||||
isChecked: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
|
@ -64,6 +74,7 @@ fun RadioMenuItem(
|
|||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -71,25 +82,29 @@ fun RadioMenuItem(
|
|||
fun NestedMenuItem(
|
||||
text: @Composable () -> Unit,
|
||||
children: @Composable ColumnScope.(() -> Unit) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var nestedExpanded by remember { mutableStateOf(false) }
|
||||
val closeMenu = { nestedExpanded = false }
|
||||
|
||||
DropdownMenuItem(
|
||||
text = text,
|
||||
onClick = { nestedExpanded = true },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
Box {
|
||||
DropdownMenuItem(
|
||||
text = text,
|
||||
onClick = { nestedExpanded = true },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = nestedExpanded,
|
||||
onDismissRequest = closeMenu,
|
||||
) {
|
||||
children(closeMenu)
|
||||
DropdownMenu(
|
||||
expanded = nestedExpanded,
|
||||
onDismissRequest = closeMenu,
|
||||
modifier = modifier,
|
||||
) {
|
||||
children(closeMenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ package eu.kanade.presentation.components
|
|||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.entries.DownloadAction
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@ -14,20 +16,24 @@ fun EntryDownloadDropdownMenu(
|
|||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
isManga: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val downloadAmount = if (isManga) MR.plurals.download_amount else MR.plurals.download_amount_anime
|
||||
val downloadUnviewed = if (isManga) MR.strings.download_unread else MR.strings.download_unseen
|
||||
val options = persistentListOf(
|
||||
DownloadAction.NEXT_1_ITEM to pluralStringResource(downloadAmount, 1, 1),
|
||||
DownloadAction.NEXT_5_ITEMS to pluralStringResource(downloadAmount, 5, 5),
|
||||
DownloadAction.NEXT_10_ITEMS to pluralStringResource(downloadAmount, 10, 10),
|
||||
DownloadAction.NEXT_25_ITEMS to pluralStringResource(downloadAmount, 25, 25),
|
||||
DownloadAction.UNVIEWED_ITEMS to stringResource(downloadUnviewed),
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
) {
|
||||
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
|
||||
listOfNotNull(
|
||||
DownloadAction.NEXT_1_ITEM to pluralStringResource(downloadAmount, 1, 1),
|
||||
DownloadAction.NEXT_5_ITEMS to pluralStringResource(downloadAmount, 5, 5),
|
||||
DownloadAction.NEXT_10_ITEMS to pluralStringResource(downloadAmount, 10, 10),
|
||||
DownloadAction.NEXT_25_ITEMS to pluralStringResource(downloadAmount, 25, 25),
|
||||
DownloadAction.UNVIEWED_ITEMS to stringResource(downloadUnviewed),
|
||||
).map { (downloadAction, string) ->
|
||||
options.map { (downloadAction, string) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = string) },
|
||||
onClick = {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
|
@ -53,6 +53,7 @@ import androidx.compose.ui.util.fastMap
|
|||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.entries.anime.model.episodesFiltered
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.entries.DownloadAction
|
||||
import eu.kanade.presentation.entries.EntryScreenItem
|
||||
import eu.kanade.presentation.entries.anime.components.AnimeActionRow
|
||||
|
@ -70,10 +71,9 @@ import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
|||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
|
||||
import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeSourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel
|
||||
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
|
@ -90,17 +90,15 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||
import tachiyomi.presentation.core.util.isScrollingUp
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import tachiyomi.source.local.entries.anime.isLocal
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Composable
|
||||
fun AnimeScreen(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fetchInterval: Int?,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
nextUpdate: Instant?,
|
||||
isTabletUi: Boolean,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
|
@ -156,16 +154,14 @@ fun AnimeScreen(
|
|||
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val onSettingsClicked: (() -> Unit)? = {
|
||||
navigator.push(SourcePreferencesScreen(state.source.id))
|
||||
navigator.push(AnimeSourcePreferencesScreen(state.source.id))
|
||||
}.takeIf { state.source is ConfigurableAnimeSource }
|
||||
|
||||
if (!isTabletUi) {
|
||||
AnimeScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||
|
@ -204,13 +200,11 @@ fun AnimeScreen(
|
|||
AnimeScreenLargeImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
nextUpdate = nextUpdate,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
onBackClicked = onBackClicked,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onDownloadEpisode = onDownloadEpisode,
|
||||
|
@ -249,9 +243,7 @@ fun AnimeScreen(
|
|||
private fun AnimeScreenSmallImpl(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
showNextEpisodeAirTime: Boolean,
|
||||
|
@ -455,7 +447,7 @@ private fun AnimeScreenSmallImpl(
|
|||
AnimeActionRow(
|
||||
favorite = state.anime.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
isUserIntervalMode = state.anime.fetchInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
|
@ -526,8 +518,6 @@ private fun AnimeScreenSmallImpl(
|
|||
anime = state.anime,
|
||||
episodes = listItem,
|
||||
isAnyEpisodeSelected = episodes.fastAny { it.selected },
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
|
@ -546,9 +536,7 @@ private fun AnimeScreenSmallImpl(
|
|||
fun AnimeScreenLargeImpl(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
showNextEpisodeAirTime: Boolean,
|
||||
|
@ -734,7 +722,7 @@ fun AnimeScreenLargeImpl(
|
|||
AnimeActionRow(
|
||||
favorite = state.anime.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
isUserIntervalMode = state.anime.fetchInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
|
@ -812,8 +800,6 @@ fun AnimeScreenLargeImpl(
|
|||
anime = state.anime,
|
||||
episodes = listItem,
|
||||
isAnyEpisodeSelected = episodes.fastAny { it.selected },
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
|
@ -884,8 +870,6 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
anime: Anime,
|
||||
episodes: List<EpisodeList>,
|
||||
isAnyEpisodeSelected: Boolean,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||
|
@ -904,7 +888,6 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
contentType = { EntryScreenItem.ITEM },
|
||||
) { episodeItem ->
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
|
||||
when (episodeItem) {
|
||||
is EpisodeList.MissingCount -> {
|
||||
|
@ -920,15 +903,7 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
} else {
|
||||
episodeItem.episode.name
|
||||
},
|
||||
date = episodeItem.episode.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
date = relativeDateText(episodeItem.episode.dateUpload),
|
||||
watchProgress = episodeItem.episode.lastSecondSeen
|
||||
.takeIf { !episodeItem.episode.seen && it > 0L }
|
||||
?.let {
|
||||
|
@ -942,7 +917,7 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
seen = episodeItem.episode.seen,
|
||||
bookmark = episodeItem.episode.bookmark,
|
||||
selected = episodeItem.selected,
|
||||
downloadIndicatorEnabled = !isAnyEpisodeSelected,
|
||||
downloadIndicatorEnabled = !isAnyEpisodeSelected && !anime.isLocal(),
|
||||
downloadStateProvider = { episodeItem.downloadState },
|
||||
downloadProgressProvider = { episodeItem.downloadProgress },
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
|
|
|
@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
|
@ -28,7 +29,7 @@ fun DuplicateAnimeDialog(
|
|||
},
|
||||
confirmButton = {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Input
|
||||
import androidx.compose.material.icons.outlined.NavigateNext
|
||||
import androidx.compose.material.icons.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.SystemUpdateAlt
|
||||
|
@ -256,6 +257,18 @@ private fun VideoList(
|
|||
)
|
||||
}
|
||||
},
|
||||
onIntPlayerClicked = {
|
||||
scope.launch {
|
||||
MainActivity.startPlayerActivity(
|
||||
context,
|
||||
anime.id,
|
||||
episode.id,
|
||||
false,
|
||||
selectedVideo,
|
||||
videoList,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -289,6 +302,7 @@ private fun QualityOptions(
|
|||
onExtDownloadClicked: () -> Unit = {},
|
||||
onCopyClicked: () -> Unit = {},
|
||||
onExtPlayerClicked: () -> Unit = {},
|
||||
onIntPlayerClicked: () -> Unit = {},
|
||||
) {
|
||||
val closeMenu = { EpisodeOptionsDialogScreen.onDismissDialog() }
|
||||
|
||||
|
@ -325,6 +339,15 @@ private fun QualityOptions(
|
|||
closeMenu()
|
||||
},
|
||||
)
|
||||
|
||||
ClickableRow(
|
||||
text = stringResource(MR.strings.action_play_internally),
|
||||
icon = Icons.Outlined.Input,
|
||||
onClick = {
|
||||
onIntPlayerClicked()
|
||||
closeMenu()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -188,9 +188,9 @@ fun AnimeEpisodeListItem(
|
|||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (watchProgress != null || scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (watchProgress != null) {
|
||||
DotSeparatorText()
|
||||
Text(
|
||||
text = watchProgress,
|
||||
maxLines = 1,
|
||||
|
@ -200,6 +200,7 @@ fun AnimeEpisodeListItem(
|
|||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (scanlator != null) {
|
||||
DotSeparatorText()
|
||||
Text(
|
||||
text = scanlator,
|
||||
maxLines = 1,
|
||||
|
@ -210,15 +211,13 @@ fun AnimeEpisodeListItem(
|
|||
}
|
||||
}
|
||||
|
||||
if (onDownloadClick != null) {
|
||||
EpisodeDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = onDownloadClick,
|
||||
)
|
||||
}
|
||||
EpisodeDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = { onDownloadClick?.invoke(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
@ -87,14 +88,14 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import kotlin.math.absoluteValue
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||
|
||||
@Composable
|
||||
fun AnimeInfoBox(
|
||||
modifier: Modifier = Modifier,
|
||||
isTabletUi: Boolean,
|
||||
appBarPadding: Dp,
|
||||
title: String,
|
||||
|
@ -106,6 +107,7 @@ fun AnimeInfoBox(
|
|||
status: Long,
|
||||
onCoverClick: () -> Unit,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
// Backdrop
|
||||
|
@ -164,10 +166,9 @@ fun AnimeInfoBox(
|
|||
|
||||
@Composable
|
||||
fun AnimeActionRow(
|
||||
modifier: Modifier = Modifier,
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
isUserIntervalMode: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
|
@ -175,9 +176,20 @@ fun AnimeActionRow(
|
|||
onTrackingClicked: () -> Unit,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
|
||||
// TODO: show something better when using custom interval
|
||||
val nextUpdateDays = remember(nextUpdate) {
|
||||
return@remember if (nextUpdate != null) {
|
||||
val now = Instant.now()
|
||||
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
AnimeActionButton(
|
||||
title = if (favorite) {
|
||||
|
@ -190,18 +202,20 @@ fun AnimeActionRow(
|
|||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
||||
AnimeActionButton(
|
||||
title = pluralStringResource(
|
||||
AnimeActionButton(
|
||||
title = when (nextUpdateDays) {
|
||||
null -> stringResource(MR.strings.not_applicable)
|
||||
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
|
||||
else -> pluralStringResource(
|
||||
MR.plurals.day,
|
||||
count = fetchInterval.absoluteValue,
|
||||
fetchInterval.absoluteValue,
|
||||
),
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onEditIntervalClicked,
|
||||
)
|
||||
}
|
||||
count = nextUpdateDays,
|
||||
nextUpdateDays,
|
||||
)
|
||||
},
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = { onEditIntervalClicked?.invoke() },
|
||||
)
|
||||
AnimeActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
stringResource(MR.strings.manga_tracking_tab)
|
||||
|
@ -227,12 +241,12 @@ fun AnimeActionRow(
|
|||
|
||||
@Composable
|
||||
fun ExpandableAnimeDescription(
|
||||
modifier: Modifier = Modifier,
|
||||
defaultExpandState: Boolean,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
onTagSearch: (String) -> Unit,
|
||||
onCopyTagToClipboard: (tag: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
val (expanded, onExpanded) = rememberSaveable {
|
||||
|
@ -288,7 +302,7 @@ fun ExpandableAnimeDescription(
|
|||
if (expanded) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
|
@ -304,7 +318,7 @@ fun ExpandableAnimeDescription(
|
|||
} else {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
|
@ -407,15 +421,15 @@ private fun AnimeAndSourceTitlesSmall(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeContentInfo(
|
||||
private fun ColumnScope.AnimeContentInfo(
|
||||
title: String,
|
||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
status: Long,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Text(
|
||||
|
@ -439,7 +453,7 @@ private fun AnimeContentInfo(
|
|||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
@ -470,7 +484,7 @@ private fun AnimeContentInfo(
|
|||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
@ -20,8 +20,8 @@ import tachiyomi.presentation.core.components.material.padding
|
|||
|
||||
@Composable
|
||||
fun BaseAnimeListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
anime: Anime,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItem: () -> Unit = {},
|
||||
onClickCover: () -> Unit = onClickItem,
|
||||
cover: @Composable RowScope.() -> Unit = { defaultCover(anime, onClickCover) },
|
||||
|
|
|
@ -46,10 +46,10 @@ enum class EpisodeDownloadAction {
|
|||
@Composable
|
||||
fun EpisodeDownloadIndicator(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
downloadStateProvider: () -> AnimeDownload.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
onClick: (EpisodeDownloadAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (val downloadState = downloadStateProvider()) {
|
||||
AnimeDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
||||
|
@ -106,10 +106,10 @@ private fun NotDownloadedIndicator(
|
|||
@Composable
|
||||
private fun DownloadingIndicator(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
downloadState: AnimeDownload.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
onClick: (EpisodeDownloadAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
|
|
|
@ -2,13 +2,24 @@ package eu.kanade.presentation.entries.components
|
|||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun DotSeparatorText() {
|
||||
Text(text = " • ")
|
||||
fun DotSeparatorText(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = " • ",
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DotSeparatorNoSpaceText() {
|
||||
Text(text = "•")
|
||||
fun DotSeparatorNoSpaceText(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = "•",
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -225,7 +225,10 @@ private fun RowScope.Button(
|
|||
onClick: () -> Unit,
|
||||
content: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
|
||||
val animatedWeight by animateFloatAsState(
|
||||
targetValue = if (toConfirm) 2f else 1f,
|
||||
label = "weight",
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
|
@ -262,13 +265,13 @@ private fun RowScope.Button(
|
|||
@Composable
|
||||
fun LibraryBottomActionMenu(
|
||||
visible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onChangeCategoryClicked: () -> Unit,
|
||||
onMarkAsViewedClicked: () -> Unit,
|
||||
onMarkAsUnviewedClicked: () -> Unit,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onDeleteClicked: () -> Unit,
|
||||
isManga: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
|
|
|
@ -20,7 +20,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
|
@ -161,6 +160,14 @@ fun EntryToolbar(
|
|||
),
|
||||
)
|
||||
}
|
||||
if (onClickSettings != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.settings),
|
||||
onClick = onClickSettings,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
|
|
|
@ -22,8 +22,8 @@ enum class ItemCover(val ratio: Float) {
|
|||
|
||||
@Composable
|
||||
operator fun invoke(
|
||||
modifier: Modifier = Modifier,
|
||||
data: Any?,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String = "",
|
||||
shape: Shape = MaterialTheme.shapes.extraSmall,
|
||||
onClick: (() -> Unit)? = null,
|
||||
|
|
|
@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
|
@ -23,16 +24,17 @@ fun ItemHeader(
|
|||
missingItemsCount: Int,
|
||||
onClick: () -> Unit,
|
||||
isManga: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
Text(
|
||||
text = if (itemCount == null) {
|
||||
|
|
|
@ -1,23 +1,37 @@
|
|||
package eu.kanade.presentation.entries.components
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun DeleteItemsDialog(
|
||||
|
@ -55,35 +69,81 @@ fun DeleteItemsDialog(
|
|||
@Composable
|
||||
fun SetIntervalDialog(
|
||||
interval: Int,
|
||||
nextUpdate: Instant?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onValueChanged: (Int) -> Unit,
|
||||
isManga: Boolean,
|
||||
onValueChanged: ((Int) -> Unit)? = null,
|
||||
) {
|
||||
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
||||
|
||||
val nextUpdateDays = remember(nextUpdate) {
|
||||
return@remember if (nextUpdate != null) {
|
||||
val now = Instant.now()
|
||||
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(MR.strings.manga_modify_calculated_interval_title)) },
|
||||
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
|
||||
text = {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||
val items = (0..28)
|
||||
.map {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.label_default)
|
||||
Column {
|
||||
if (nextUpdateDays != null && nextUpdateDays >= 0 && interval >= 0) {
|
||||
Text(
|
||||
stringResource(
|
||||
if (isManga) {
|
||||
MR.strings.manga_interval_expected_update
|
||||
} else {
|
||||
MR.strings.anime_interval_expected_update
|
||||
},
|
||||
pluralStringResource(
|
||||
MR.plurals.day,
|
||||
count = nextUpdateDays,
|
||||
nextUpdateDays,
|
||||
),
|
||||
pluralStringResource(
|
||||
MR.plurals.day,
|
||||
count = interval.absoluteValue,
|
||||
interval.absoluteValue,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(MaterialTheme.padding.small))
|
||||
}
|
||||
|
||||
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
||||
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||
val maxInterval = if (isManga) {
|
||||
MangaFetchInterval.MAX_INTERVAL
|
||||
} else {
|
||||
it.toString()
|
||||
AnimeFetchInterval.MAX_INTERVAL
|
||||
}
|
||||
val items = (0..maxInterval)
|
||||
.map {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.label_default)
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
.toImmutableList()
|
||||
WheelTextPicker(
|
||||
items = items,
|
||||
size = size,
|
||||
startIndex = selectedInterval,
|
||||
onSelectionChanged = { selectedInterval = it },
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
WheelTextPicker(
|
||||
items = items,
|
||||
size = size,
|
||||
startIndex = selectedInterval,
|
||||
onSelectionChanged = { selectedInterval = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
|
@ -93,7 +153,7 @@ fun SetIntervalDialog(
|
|||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onValueChanged(selectedInterval)
|
||||
onValueChanged?.invoke(selectedInterval)
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
|
|
|
@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
|
@ -28,7 +29,7 @@ fun DuplicateMangaDialog(
|
|||
},
|
||||
confirmButton = {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
|
|
@ -49,6 +49,7 @@ import androidx.compose.ui.util.fastAny
|
|||
import androidx.compose.ui.util.fastMap
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.entries.DownloadAction
|
||||
import eu.kanade.presentation.entries.EntryScreenItem
|
||||
import eu.kanade.presentation.entries.components.EntryBottomActionMenu
|
||||
|
@ -67,7 +68,6 @@ import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo
|
|||
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.entries.manga.ChapterList
|
||||
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
|
@ -83,16 +83,14 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||
import tachiyomi.presentation.core.util.isScrollingUp
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import tachiyomi.source.local.entries.manga.isLocal
|
||||
import java.time.Instant
|
||||
|
||||
@Composable
|
||||
fun MangaScreen(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fetchInterval: Int?,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
nextUpdate: Instant?,
|
||||
isTabletUi: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -152,9 +150,7 @@ fun MangaScreen(
|
|||
MangaScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onBackClicked = onBackClicked,
|
||||
|
@ -190,11 +186,9 @@ fun MangaScreen(
|
|||
MangaScreenLargeImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
onBackClicked = onBackClicked,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
|
@ -231,9 +225,7 @@ fun MangaScreen(
|
|||
private fun MangaScreenSmallImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
@ -425,7 +417,7 @@ private fun MangaScreenSmallImpl(
|
|||
MangaActionRow(
|
||||
favorite = state.manga.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
isUserIntervalMode = state.manga.fetchInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
|
@ -469,8 +461,6 @@ private fun MangaScreenSmallImpl(
|
|||
manga = state.manga,
|
||||
chapters = listItem,
|
||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onChapterClicked = onChapterClicked,
|
||||
|
@ -488,9 +478,7 @@ private fun MangaScreenSmallImpl(
|
|||
fun MangaScreenLargeImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
@ -670,7 +658,7 @@ fun MangaScreenLargeImpl(
|
|||
MangaActionRow(
|
||||
favorite = state.manga.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
isUserIntervalMode = state.manga.fetchInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
|
@ -721,8 +709,6 @@ fun MangaScreenLargeImpl(
|
|||
manga = state.manga,
|
||||
chapters = listItem,
|
||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onChapterClicked = onChapterClicked,
|
||||
|
@ -775,7 +761,7 @@ private fun SharedMangaBottomActionMenu(
|
|||
onDeleteClicked = {
|
||||
onMultiDeleteClicked(selected.fastMap { it.chapter })
|
||||
}.takeIf {
|
||||
onDownloadChapter != null && selected.fastAny { it.downloadState == MangaDownload.State.DOWNLOADED }
|
||||
selected.fastAny { it.downloadState == MangaDownload.State.DOWNLOADED }
|
||||
},
|
||||
isManga = true,
|
||||
)
|
||||
|
@ -785,8 +771,6 @@ private fun LazyListScope.sharedChapterItems(
|
|||
manga: Manga,
|
||||
chapters: List<ChapterList>,
|
||||
isAnyChapterSelected: Boolean,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
|
@ -805,7 +789,6 @@ private fun LazyListScope.sharedChapterItems(
|
|||
contentType = { EntryScreenItem.ITEM },
|
||||
) { item ->
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
|
||||
when (item) {
|
||||
is ChapterList.MissingCount -> {
|
||||
|
@ -821,15 +804,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||
} else {
|
||||
item.chapter.name
|
||||
},
|
||||
date = item.chapter.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
date = relativeDateText(item.chapter.dateUpload),
|
||||
readProgress = item.chapter.lastPageRead
|
||||
.takeIf { !item.chapter.read && it > 0L }
|
||||
?.let {
|
||||
|
@ -842,7 +817,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||
read = item.chapter.read,
|
||||
bookmark = item.chapter.bookmark,
|
||||
selected = item.selected,
|
||||
downloadIndicatorEnabled = !isAnyChapterSelected,
|
||||
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
|
||||
downloadStateProvider = { item.downloadState },
|
||||
downloadProgressProvider = { item.downloadProgress },
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
|
|
|
@ -20,8 +20,8 @@ import tachiyomi.presentation.core.components.material.padding
|
|||
|
||||
@Composable
|
||||
fun BaseMangaListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
manga: Manga,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItem: () -> Unit = {},
|
||||
onClickCover: () -> Unit = onClickItem,
|
||||
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
|
||||
|
|
|
@ -45,10 +45,10 @@ enum class ChapterDownloadAction {
|
|||
@Composable
|
||||
fun ChapterDownloadIndicator(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
downloadStateProvider: () -> MangaDownload.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
onClick: (ChapterDownloadAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (val downloadState = downloadStateProvider()) {
|
||||
MangaDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
||||
|
@ -105,10 +105,10 @@ private fun NotDownloadedIndicator(
|
|||
@Composable
|
||||
private fun DownloadingIndicator(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
downloadState: MangaDownload.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
onClick: (ChapterDownloadAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
|
|
|
@ -186,9 +186,9 @@ fun MangaChapterListItem(
|
|||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (readProgress != null || scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (readProgress != null) {
|
||||
DotSeparatorText()
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
|
@ -197,6 +197,7 @@ fun MangaChapterListItem(
|
|||
)
|
||||
}
|
||||
if (scanlator != null) {
|
||||
DotSeparatorText()
|
||||
Text(
|
||||
text = scanlator,
|
||||
maxLines = 1,
|
||||
|
@ -207,15 +208,13 @@ fun MangaChapterListItem(
|
|||
}
|
||||
}
|
||||
|
||||
if (onDownloadClick != null) {
|
||||
ChapterDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = onDownloadClick,
|
||||
)
|
||||
}
|
||||
ChapterDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = { onDownloadClick?.invoke(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
@ -87,7 +88,8 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import kotlin.math.absoluteValue
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||
|
@ -166,7 +168,7 @@ fun MangaInfoBox(
|
|||
fun MangaActionRow(
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
isUserIntervalMode: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
|
@ -178,6 +180,16 @@ fun MangaActionRow(
|
|||
) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
|
||||
// TODO: show something better when using custom interval
|
||||
val nextUpdateDays = remember(nextUpdate) {
|
||||
return@remember if (nextUpdate != null) {
|
||||
val now = Instant.now()
|
||||
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
MangaActionButton(
|
||||
title = if (favorite) {
|
||||
|
@ -190,18 +202,20 @@ fun MangaActionRow(
|
|||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
||||
MangaActionButton(
|
||||
title = pluralStringResource(
|
||||
MangaActionButton(
|
||||
title = when (nextUpdateDays) {
|
||||
null -> stringResource(MR.strings.not_applicable)
|
||||
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
|
||||
else -> pluralStringResource(
|
||||
MR.plurals.day,
|
||||
count = fetchInterval.absoluteValue,
|
||||
fetchInterval.absoluteValue,
|
||||
),
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onEditIntervalClicked,
|
||||
)
|
||||
}
|
||||
count = nextUpdateDays,
|
||||
nextUpdateDays,
|
||||
)
|
||||
},
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = { onEditIntervalClicked?.invoke() },
|
||||
)
|
||||
MangaActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
stringResource(MR.strings.manga_tracking_tab)
|
||||
|
@ -287,7 +301,7 @@ fun ExpandableMangaDescription(
|
|||
if (expanded) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
|
@ -303,7 +317,7 @@ fun ExpandableMangaDescription(
|
|||
} else {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
|
@ -406,7 +420,7 @@ private fun MangaAndSourceTitlesSmall(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaContentInfo(
|
||||
private fun ColumnScope.MangaContentInfo(
|
||||
title: String,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
|
@ -438,7 +452,7 @@ private fun MangaContentInfo(
|
|||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
@ -469,7 +483,7 @@ private fun MangaContentInfo(
|
|||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.presentation.history
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -11,10 +12,10 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.random.Random
|
||||
|
||||
|
@ -32,7 +33,7 @@ fun HistoryDeleteDialog(
|
|||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
val subtitle = if (isManga) {
|
||||
MR.strings.dialog_with_checkbox_remove_description
|
||||
|
@ -42,7 +43,11 @@ fun HistoryDeleteDialog(
|
|||
Text(text = stringResource(subtitle))
|
||||
|
||||
LabeledCheckbox(
|
||||
label = stringResource(MR.strings.dialog_with_checkbox_reset),
|
||||
label = if (isManga) {
|
||||
stringResource(MR.strings.dialog_with_checkbox_reset)
|
||||
} else {
|
||||
stringResource(MR.strings.dialog_with_checkbox_reset_anime)
|
||||
},
|
||||
checked = removeEverything,
|
||||
onCheckedChange = { removeEverything = it },
|
||||
)
|
|
@ -6,24 +6,20 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.components.RelativeDateHeader
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.history.anime.components.AnimeHistoryItem
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
|
||||
import tachiyomi.core.preference.InMemoryPreferenceStore
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
|
@ -33,7 +29,6 @@ fun AnimeHistoryScreen(
|
|||
onClickCover: (animeId: Long) -> Unit,
|
||||
onClickResume: (animeId: Long, episodeId: Long) -> Unit,
|
||||
onDialogChange: (AnimeHistoryScreenModel.Dialog?) -> Unit,
|
||||
preferences: UiPreferences = Injekt.get(),
|
||||
searchQuery: String? = null,
|
||||
) {
|
||||
Scaffold(
|
||||
|
@ -53,17 +48,12 @@ fun AnimeHistoryScreen(
|
|||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
} else {
|
||||
AnimeHistoryContent(
|
||||
AnimeHistoryScreenContent(
|
||||
history = it,
|
||||
contentPadding = contentPadding,
|
||||
onClickCover = { history -> onClickCover(history.animeId) },
|
||||
onClickResume = { history -> onClickResume(history.animeId, history.episodeId) },
|
||||
onClickDelete = { item ->
|
||||
onDialogChange(
|
||||
AnimeHistoryScreenModel.Dialog.Delete(item),
|
||||
)
|
||||
},
|
||||
preferences = preferences,
|
||||
onClickDelete = { item -> onDialogChange(AnimeHistoryScreenModel.Dialog.Delete(item)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -71,17 +61,13 @@ fun AnimeHistoryScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeHistoryContent(
|
||||
private fun AnimeHistoryScreenContent(
|
||||
history: List<AnimeHistoryUiModel>,
|
||||
contentPadding: PaddingValues,
|
||||
onClickCover: (AnimeHistoryWithRelations) -> Unit,
|
||||
onClickResume: (AnimeHistoryWithRelations) -> Unit,
|
||||
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
||||
preferences: UiPreferences,
|
||||
) {
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
|
@ -97,11 +83,9 @@ private fun AnimeHistoryContent(
|
|||
) { item ->
|
||||
when (item) {
|
||||
is AnimeHistoryUiModel.Header -> {
|
||||
RelativeDateHeader(
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
text = relativeDateText(item.date),
|
||||
)
|
||||
}
|
||||
is AnimeHistoryUiModel.Item -> {
|
||||
|
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
|
|||
onClickCover = {},
|
||||
onClickResume = { _, _ -> run {} },
|
||||
onDialogChange = {},
|
||||
preferences = UiPreferences(
|
||||
InMemoryPreferenceStore(
|
||||
sequenceOf(
|
||||
InMemoryPreferenceStore.InMemoryPreference(
|
||||
key = "relative_time_v2",
|
||||
data = false,
|
||||
defaultValue = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,24 +6,20 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.components.RelativeDateHeader
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.history.manga.components.MangaHistoryItem
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
|
||||
import tachiyomi.core.preference.InMemoryPreferenceStore
|
||||
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
|
@ -33,7 +29,6 @@ fun MangaHistoryScreen(
|
|||
onClickCover: (mangaId: Long) -> Unit,
|
||||
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
||||
onDialogChange: (MangaHistoryScreenModel.Dialog?) -> Unit,
|
||||
preferences: UiPreferences = Injekt.get(),
|
||||
searchQuery: String? = null,
|
||||
) {
|
||||
Scaffold(
|
||||
|
@ -53,17 +48,12 @@ fun MangaHistoryScreen(
|
|||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
} else {
|
||||
MangaHistoryContent(
|
||||
MangaHistoryScreenContent(
|
||||
history = it,
|
||||
contentPadding = contentPadding,
|
||||
onClickCover = { history -> onClickCover(history.mangaId) },
|
||||
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
||||
onClickDelete = { item ->
|
||||
onDialogChange(
|
||||
MangaHistoryScreenModel.Dialog.Delete(item),
|
||||
)
|
||||
},
|
||||
preferences = preferences,
|
||||
onClickDelete = { item -> onDialogChange(MangaHistoryScreenModel.Dialog.Delete(item)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -71,17 +61,13 @@ fun MangaHistoryScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaHistoryContent(
|
||||
private fun MangaHistoryScreenContent(
|
||||
history: List<MangaHistoryUiModel>,
|
||||
contentPadding: PaddingValues,
|
||||
onClickCover: (MangaHistoryWithRelations) -> Unit,
|
||||
onClickResume: (MangaHistoryWithRelations) -> Unit,
|
||||
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
||||
preferences: UiPreferences,
|
||||
) {
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
|
@ -97,11 +83,9 @@ private fun MangaHistoryContent(
|
|||
) { item ->
|
||||
when (item) {
|
||||
is MangaHistoryUiModel.Header -> {
|
||||
RelativeDateHeader(
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
text = relativeDateText(item.date),
|
||||
)
|
||||
}
|
||||
is MangaHistoryUiModel.Item -> {
|
||||
|
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
|
|||
onClickCover = {},
|
||||
onClickResume = { _, _ -> run {} },
|
||||
onDialogChange = {},
|
||||
preferences = UiPreferences(
|
||||
InMemoryPreferenceStore(
|
||||
sequenceOf(
|
||||
InMemoryPreferenceStore.InMemoryPreference(
|
||||
key = "relative_time_v2",
|
||||
data = false,
|
||||
defaultValue = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ import androidx.compose.ui.platform.LocalConfiguration
|
|||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibrarySettingsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.preference.TriState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
|
@ -74,6 +76,8 @@ private fun ColumnScope.FilterPage(
|
|||
) {
|
||||
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedAnime().collectAsState()
|
||||
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
|
||||
val autoUpdateAnimeRestrictions by screenModel.libraryPreferences.autoUpdateItemRestrictions().collectAsState()
|
||||
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_downloaded),
|
||||
state = if (downloadedOnly) {
|
||||
|
@ -108,6 +112,18 @@ private fun ColumnScope.FilterPage(
|
|||
state = filterCompleted,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedAnime) },
|
||||
)
|
||||
// TODO: re-enable when custom intervals are ready for stable
|
||||
if (
|
||||
(isDevFlavor || isPreviewBuildType) &&
|
||||
LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in autoUpdateAnimeRestrictions
|
||||
) {
|
||||
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_interval_custom),
|
||||
state = filterIntervalCustom,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
|
||||
)
|
||||
}
|
||||
|
||||
val trackers = remember { screenModel.trackers }
|
||||
when (trackers.size) {
|
||||
|
|
|
@ -40,6 +40,7 @@ fun LibraryToolbar(
|
|||
searchQuery: String?,
|
||||
onSearchQueryChange: (String?) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior?,
|
||||
navigateUp: (() -> Unit)? = null,
|
||||
) = when {
|
||||
selectedCount > 0 -> LibrarySelectionToolbar(
|
||||
selectedCount = selectedCount,
|
||||
|
@ -57,6 +58,7 @@ fun LibraryToolbar(
|
|||
onClickGlobalUpdate = onClickGlobalUpdate,
|
||||
onClickOpenRandomEntry = onClickOpenRandomEntry,
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigateUp = navigateUp,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -71,6 +73,7 @@ private fun LibraryRegularToolbar(
|
|||
onClickGlobalUpdate: () -> Unit,
|
||||
onClickOpenRandomEntry: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior?,
|
||||
navigateUp: (() -> Unit)?,
|
||||
) {
|
||||
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
|
||||
SearchToolbar(
|
||||
|
@ -119,6 +122,7 @@ private fun LibraryRegularToolbar(
|
|||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigateUp = navigateUp,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ import androidx.compose.ui.platform.LocalConfiguration
|
|||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.library.manga.MangaLibrarySettingsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.preference.TriState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
|
@ -74,6 +76,8 @@ private fun ColumnScope.FilterPage(
|
|||
) {
|
||||
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedManga().collectAsState()
|
||||
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
|
||||
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateItemRestrictions().collectAsState()
|
||||
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_downloaded),
|
||||
state = if (downloadedOnly) {
|
||||
|
@ -108,6 +112,18 @@ private fun ColumnScope.FilterPage(
|
|||
state = filterCompleted,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedManga) },
|
||||
)
|
||||
// TODO: re-enable when custom intervals are ready for stable
|
||||
if (
|
||||
(isDevFlavor || isPreviewBuildType) &&
|
||||
LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions
|
||||
) {
|
||||
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_interval_custom),
|
||||
state = filterIntervalCustom,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
|
||||
)
|
||||
}
|
||||
|
||||
val trackers = remember { screenModel.trackers }
|
||||
when (trackers.size) {
|
||||
|
|
|
@ -12,9 +12,7 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.CloudOff
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.material.icons.outlined.GetApp
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.QueryStats
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
|
@ -25,19 +23,18 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import eu.kanade.domain.ui.model.NavStyle
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.Constants
|
||||
import eu.kanade.tachiyomi.ui.more.DownloadQueueState
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@Composable
|
||||
fun MoreScreen(
|
||||
|
@ -47,6 +44,7 @@ fun MoreScreen(
|
|||
incognitoMode: Boolean,
|
||||
onIncognitoModeChange: (Boolean) -> Unit,
|
||||
isFDroid: Boolean,
|
||||
navStyle: NavStyle,
|
||||
onClickAlt: () -> Unit,
|
||||
onClickDownloadQueue: () -> Unit,
|
||||
onClickCategories: () -> Unit,
|
||||
|
@ -107,23 +105,10 @@ fun MoreScreen(
|
|||
|
||||
item { HorizontalDivider() }
|
||||
|
||||
val libraryPreferences: LibraryPreferences by injectLazy()
|
||||
|
||||
item {
|
||||
val bottomNavStyle = libraryPreferences.bottomNavStyle().get()
|
||||
val titleRes = when (bottomNavStyle) {
|
||||
0 -> MR.strings.label_recent_manga
|
||||
1 -> MR.strings.label_recent_updates
|
||||
else -> MR.strings.label_manga
|
||||
}
|
||||
val icon = when (bottomNavStyle) {
|
||||
0 -> Icons.Outlined.History
|
||||
1 -> ImageVector.vectorResource(id = R.drawable.ic_updates_outline_24dp)
|
||||
else -> Icons.Outlined.CollectionsBookmark
|
||||
}
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(titleRes),
|
||||
icon = icon,
|
||||
title = navStyle.moreTab.options.title,
|
||||
icon = navStyle.moreIcon,
|
||||
onPreferenceClick = onClickAlt,
|
||||
)
|
||||
}
|
||||
|
@ -148,6 +133,7 @@ fun MoreScreen(
|
|||
}"
|
||||
}
|
||||
}
|
||||
|
||||
is DownloadQueueState.Downloading -> {
|
||||
val pending = downloadQueueState.pending
|
||||
pluralStringResource(
|
||||
|
|
|
@ -17,7 +17,7 @@ import androidx.compose.ui.text.SpanStyle
|
|||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import tachiyomi.i18n.MR
|
||||
|
@ -42,7 +42,7 @@ fun NewUpdateScreen(
|
|||
rejectText = stringResource(MR.strings.action_not_now),
|
||||
onRejectClick = onRejectUpdate,
|
||||
) {
|
||||
Material3RichText(
|
||||
RichText(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.padding.large),
|
||||
|
@ -59,7 +59,7 @@ fun NewUpdateScreen(
|
|||
modifier = Modifier.padding(top = MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.update_check_open))
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.padding.extraSmall))
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,36 +15,43 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
internal fun GuidesStep(
|
||||
onRestoreBackup: () -> Unit,
|
||||
) {
|
||||
val handler = LocalUriHandler.current
|
||||
internal class GuidesStep(
|
||||
private val onRestoreBackup: () -> Unit,
|
||||
) : OnboardingStep {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { handler.openUri(GETTING_STARTED_URL) },
|
||||
override val isComplete: Boolean = true
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val handler = LocalUriHandler.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(stringResource(MR.strings.getting_started_guide))
|
||||
}
|
||||
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { handler.openUri(GETTING_STARTED_URL) },
|
||||
) {
|
||||
Text(stringResource(MR.strings.getting_started_guide))
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
|
||||
Text(stringResource(MR.strings.onboarding_guides_returning_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onRestoreBackup,
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_restore_backup))
|
||||
Text(stringResource(MR.strings.onboarding_guides_returning_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onRestoreBackup,
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_restore_backup))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +64,6 @@ private fun GuidesStepPreview() {
|
|||
TachiyomiTheme {
|
||||
GuidesStep(
|
||||
onRestoreBackup = {},
|
||||
)
|
||||
).Content()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,15 +13,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@ -29,24 +26,21 @@ import tachiyomi.presentation.core.screens.InfoScreen
|
|||
|
||||
@Composable
|
||||
fun OnboardingScreen(
|
||||
storagePreferences: StoragePreferences,
|
||||
uiPreferences: UiPreferences,
|
||||
onComplete: () -> Unit,
|
||||
onRestoreBackup: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val slideDistance = rememberSlideDistance()
|
||||
|
||||
var currentStep by remember { mutableIntStateOf(0) }
|
||||
val steps: List<@Composable () -> Unit> = remember {
|
||||
var currentStep by rememberSaveable { mutableIntStateOf(0) }
|
||||
val steps = remember {
|
||||
listOf(
|
||||
{ ThemeStep(uiPreferences = uiPreferences) },
|
||||
{ StorageStep(storagePref = storagePreferences.baseStorageDirectory()) },
|
||||
// TODO: prompt for notification permissions when bumping target to Android 13
|
||||
{ GuidesStep(onRestoreBackup = onRestoreBackup) },
|
||||
ThemeStep(),
|
||||
StorageStep(),
|
||||
PermissionStep(),
|
||||
GuidesStep(onRestoreBackup = onRestoreBackup),
|
||||
)
|
||||
}
|
||||
val isLastStep = currentStep == steps.size - 1
|
||||
val isLastStep = currentStep == steps.lastIndex
|
||||
|
||||
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
|
||||
|
||||
|
@ -61,16 +55,12 @@ fun OnboardingScreen(
|
|||
MR.strings.onboarding_action_next
|
||||
},
|
||||
),
|
||||
canAccept = steps[currentStep].isComplete,
|
||||
onAcceptClick = {
|
||||
if (isLastStep) {
|
||||
onComplete()
|
||||
} else {
|
||||
// TODO: this is kind of janky
|
||||
if (currentStep == 1 && !storagePreferences.baseStorageDirectory().isSet()) {
|
||||
context.toast(MR.strings.onboarding_storage_selection_required)
|
||||
} else {
|
||||
currentStep++
|
||||
}
|
||||
currentStep++
|
||||
}
|
||||
},
|
||||
) {
|
||||
|
@ -91,7 +81,7 @@ fun OnboardingScreen(
|
|||
},
|
||||
label = "stepContent",
|
||||
) {
|
||||
steps[it]()
|
||||
steps[it].Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package eu.kanade.presentation.more.onboarding
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
internal interface OnboardingStep {
|
||||
|
||||
val isComplete: Boolean
|
||||
|
||||
@Composable
|
||||
fun Content()
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,48 +5,109 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
|
||||
import eu.kanade.tachiyomi.util.system.isTvBox
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import tachiyomi.core.preference.Preference
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.core.storage.AndroidStorageFolderProvider
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Button
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
internal fun StorageStep(
|
||||
storagePref: Preference<String>,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
||||
internal class StorageStep : OnboardingStep {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
MR.strings.onboarding_storage_info,
|
||||
stringResource(MR.strings.app_name),
|
||||
SettingsDataScreen.storageLocationText(storagePref),
|
||||
),
|
||||
)
|
||||
private val storagePref = Injekt.get<StoragePreferences>().baseStorageDirectory()
|
||||
private val folderProvider = Injekt.get<AndroidStorageFolderProvider>()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
try {
|
||||
pickStorageLocation.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(MR.strings.file_picker_error)
|
||||
}
|
||||
},
|
||||
private var _isComplete by mutableStateOf(false)
|
||||
|
||||
override val isComplete: Boolean
|
||||
get() = _isComplete
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val handler = LocalUriHandler.current
|
||||
|
||||
val isTvBox = isTvBox(LocalContext.current)
|
||||
|
||||
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_storage_action_select))
|
||||
Text(
|
||||
stringResource(
|
||||
MR.strings.onboarding_storage_info,
|
||||
stringResource(MR.strings.app_name),
|
||||
SettingsDataScreen.storageLocationText(storagePref),
|
||||
),
|
||||
)
|
||||
|
||||
if (isTvBox) {
|
||||
if (!storagePref.isSet()) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
val storage = folderProvider.directory()
|
||||
if (!storage.exists()) {
|
||||
storage.mkdirs()
|
||||
}
|
||||
storagePref.set(storagePref.get())
|
||||
},
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_storage_action_create_folder))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
try {
|
||||
pickStorageLocation.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(MR.strings.file_picker_error)
|
||||
}
|
||||
},
|
||||
) {
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,33 +8,40 @@ import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
|||
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
internal fun ThemeStep(
|
||||
uiPreferences: UiPreferences,
|
||||
) {
|
||||
val themeModePref = uiPreferences.themeMode()
|
||||
val themeMode by themeModePref.collectAsState()
|
||||
internal class ThemeStep : OnboardingStep {
|
||||
|
||||
val appThemePref = uiPreferences.appTheme()
|
||||
val appTheme by appThemePref.collectAsState()
|
||||
override val isComplete: Boolean = true
|
||||
|
||||
val amoledPref = uiPreferences.themeDarkAmoled()
|
||||
val amoled by amoledPref.collectAsState()
|
||||
private val uiPreferences: UiPreferences = Injekt.get()
|
||||
|
||||
Column {
|
||||
AppThemeModePreferenceWidget(
|
||||
value = themeMode,
|
||||
onItemClick = {
|
||||
themeModePref.set(it)
|
||||
setAppCompatDelegateThemeMode(it)
|
||||
},
|
||||
)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val themeModePref = uiPreferences.themeMode()
|
||||
val themeMode by themeModePref.collectAsState()
|
||||
|
||||
AppThemePreferenceWidget(
|
||||
value = appTheme,
|
||||
amoled = amoled,
|
||||
onItemClick = { appThemePref.set(it) },
|
||||
)
|
||||
val appThemePref = uiPreferences.appTheme()
|
||||
val appTheme by appThemePref.collectAsState()
|
||||
|
||||
val amoledPref = uiPreferences.themeDarkAmoled()
|
||||
val amoled by amoledPref.collectAsState()
|
||||
|
||||
Column {
|
||||
AppThemeModePreferenceWidget(
|
||||
value = themeMode,
|
||||
onItemClick = {
|
||||
themeModePref.set(it)
|
||||
setAppCompatDelegateThemeMode(it)
|
||||
},
|
||||
)
|
||||
|
||||
AppThemePreferenceWidget(
|
||||
value = appTheme,
|
||||
amoled = amoled,
|
||||
onItemClick = { appThemePref.set(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.core.preference.Preference as PreferenceData
|
||||
|
@ -64,13 +66,13 @@ sealed class Preference {
|
|||
val pref: PreferenceData<T>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: T, entries: Map<T, String>) -> String? =
|
||||
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
||||
|
||||
val entries: Map<T, String>,
|
||||
val entries: ImmutableMap<T, String>,
|
||||
) : PreferenceItem<T>() {
|
||||
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
||||
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(
|
||||
|
@ -78,8 +80,8 @@ sealed class Preference {
|
|||
)
|
||||
|
||||
@Composable
|
||||
internal fun internalSubtitleProvider(value: Any?, entries: Map<out Any?, String>) =
|
||||
subtitleProvider(value as T, entries as Map<T, String>)
|
||||
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
|
||||
subtitleProvider(value as T, entries as ImmutableMap<T, String>)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,13 +91,13 @@ sealed class Preference {
|
|||
val value: String,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: String, entries: Map<String, String>) -> String? =
|
||||
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
|
||||
val entries: Map<String, String>,
|
||||
val entries: ImmutableMap<String, String>,
|
||||
) : PreferenceItem<String>()
|
||||
|
||||
/**
|
||||
|
@ -106,7 +108,10 @@ sealed class Preference {
|
|||
val pref: PreferenceData<Set<String>>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: Set<String>, entries: Map<String, String>) -> String? = { v, e ->
|
||||
val subtitleProvider: @Composable (
|
||||
value: Set<String>,
|
||||
entries: ImmutableMap<String, String>,
|
||||
) -> String? = { v, e ->
|
||||
val combined = remember(v) {
|
||||
v.map { e[it] }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
|
@ -118,7 +123,7 @@ sealed class Preference {
|
|||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
||||
|
||||
val entries: Map<String, String>,
|
||||
val entries: ImmutableMap<String, String>,
|
||||
) : PreferenceItem<Set<String>>()
|
||||
|
||||
/**
|
||||
|
@ -184,6 +189,6 @@ sealed class Preference {
|
|||
override val title: String,
|
||||
override val enabled: Boolean = true,
|
||||
|
||||
val preferenceItems: List<PreferenceItem<out Any>>,
|
||||
val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
|
||||
) : Preference()
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
|||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.presentation.core.components.SliderItem
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -172,8 +171,8 @@ internal fun PreferenceItem(
|
|||
)
|
||||
}
|
||||
is Preference.PreferenceItem.TrackerPreference -> {
|
||||
val uName by Injekt.get<PreferenceStore>()
|
||||
.getString(TrackPreferences.trackUsername(item.tracker.id))
|
||||
val uName by Injekt.get<TrackPreferences>()
|
||||
.trackUsername(item.tracker)
|
||||
.collectAsState()
|
||||
item.tracker.run {
|
||||
TrackingPreferenceWidget(
|
||||
|
|
|
@ -8,6 +8,7 @@ import eu.kanade.core.preference.asState
|
|||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
||||
import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -47,10 +48,15 @@ object AdvancedPlayerSettingsScreen : SearchableSettings {
|
|||
postfix = if (mpvInput.asState(scope).value.lines().size > 2) "\n..." else "",
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
title = context.stringResource(MR.strings.pref_gpu_next_title),
|
||||
subtitle = context.stringResource(MR.strings.pref_gpu_next_subtitle),
|
||||
pref = playerPreferences.gpuNext(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
title = context.stringResource(MR.strings.pref_debanding_title),
|
||||
pref = playerPreferences.videoDebanding(),
|
||||
entries = VideoDebanding.entries.associateWith { context.stringResource(it.stringRes) }
|
||||
entries = VideoDebanding.entries.associateWith { context.stringResource(it.stringRes) }.toImmutableMap()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||
/**
|
||||
* Returns a string of categories name for settings subtitle
|
||||
*/
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
fun getCategoriesLabel(
|
||||
|
|
|
@ -24,6 +24,8 @@ import androidx.core.net.toUri
|
|||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.extension.anime.interactor.TrustAnimeExtension
|
||||
import eu.kanade.domain.extension.manga.interactor.TrustMangaExtension
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences.DataSaver
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
|
@ -32,9 +34,10 @@ import eu.kanade.presentation.more.settings.screen.advanced.ClearDatabaseScreen
|
|||
import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.library.anime.AnimeMetadataUpdateJob
|
||||
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.data.library.manga.MangaMetadataUpdateJob
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
||||
|
@ -44,6 +47,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
|||
import eu.kanade.tachiyomi.network.PREF_DOH_CONTROLD
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_LIBREDNS
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_MULLVAD
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
|
||||
|
@ -51,12 +55,17 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
|||
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import okhttp3.Headers
|
||||
|
@ -159,7 +168,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_background_activity),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_disable_battery_optimization),
|
||||
subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary),
|
||||
|
@ -200,7 +209,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_data),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_invalidate_download_cache),
|
||||
subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary),
|
||||
|
@ -236,7 +245,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_network),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_clear_cookies),
|
||||
onClick = {
|
||||
|
@ -267,7 +276,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = networkPreferences.dohProvider(),
|
||||
title = stringResource(MR.strings.pref_dns_over_https),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
||||
PREF_DOH_GOOGLE to "Google",
|
||||
|
@ -281,6 +290,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
PREF_DOH_CONTROLD to "Control D",
|
||||
PREF_DOH_NJALLA to "Njalla",
|
||||
PREF_DOH_SHECAN to "Shecan",
|
||||
PREF_DOH_LIBREDNS to "LibreDNS",
|
||||
),
|
||||
onValueChanged = {
|
||||
context.stringResource(MR.strings.requires_app_restart)
|
||||
|
@ -317,16 +327,17 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_library),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_refresh_library_covers),
|
||||
onClick = {
|
||||
AnimeLibraryUpdateJob.startNow(context)
|
||||
MangaLibraryUpdateJob.startNow(context)
|
||||
AnimeMetadataUpdateJob.startNow(context)
|
||||
MangaMetadataUpdateJob.startNow(context)
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
|
@ -358,6 +369,8 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
val uriHandler = LocalUriHandler.current
|
||||
val extensionInstallerPref = basePreferences.extensionInstaller()
|
||||
var shizukuMissing by rememberSaveable { mutableStateOf(false) }
|
||||
val trustAnimeExtension = remember { Injekt.get<TrustAnimeExtension>() }
|
||||
val trustMangaExtension = remember { Injekt.get<TrustMangaExtension>() }
|
||||
|
||||
if (shizukuMissing) {
|
||||
val dismiss = { shizukuMissing = false }
|
||||
|
@ -388,12 +401,21 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
}
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_extensions),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = extensionInstallerPref,
|
||||
title = stringResource(MR.strings.ext_installer_pref),
|
||||
entries = extensionInstallerPref.entries
|
||||
.associateWith { stringResource(it.titleRes) },
|
||||
.filter {
|
||||
// TODO: allow private option in stable versions once URL handling is more fleshed out
|
||||
if (isPreviewBuildType || isDevFlavor) {
|
||||
true
|
||||
} else {
|
||||
it != BasePreferences.ExtensionInstaller.PRIVATE
|
||||
}
|
||||
}
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
onValueChanged = {
|
||||
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
|
||||
!context.isShizukuInstalled
|
||||
|
@ -405,6 +427,14 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
}
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.ext_revoke_trust),
|
||||
onClick = {
|
||||
trustMangaExtension.revokeAll()
|
||||
trustAnimeExtension.revokeAll()
|
||||
context.toast(MR.strings.requires_app_restart)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -416,12 +446,12 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
val dataSaver by sourcePreferences.dataSaver().collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.data_saver),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = sourcePreferences.dataSaver(),
|
||||
title = stringResource(MR.strings.data_saver),
|
||||
subtitle = stringResource(MR.strings.data_saver_summary),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
DataSaver.NONE to stringResource(MR.strings.disabled),
|
||||
DataSaver.BANDWIDTH_HERO to stringResource(MR.strings.bandwidth_hero),
|
||||
DataSaver.WSRV_NL to stringResource(MR.strings.wsrv),
|
||||
|
@ -462,7 +492,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
"80%",
|
||||
"90%",
|
||||
"95%",
|
||||
).associateBy { it.trimEnd('%').toInt() },
|
||||
).associateBy { it.trimEnd('%').toInt() }.toPersistentMap(),
|
||||
enabled = dataSaver != DataSaver.NONE,
|
||||
),
|
||||
kotlin.run {
|
||||
|
|
|
@ -1,34 +1,28 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.NavStyle
|
||||
import eu.kanade.domain.ui.model.StartScreen
|
||||
import eu.kanade.domain.ui.model.TabletUiMode
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.appearance.AppLanguageScreen
|
||||
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
@ -69,7 +63,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_theme),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.CustomPreference(
|
||||
title = stringResource(MR.strings.pref_app_theme),
|
||||
) {
|
||||
|
@ -107,13 +101,8 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
uiPreferences: UiPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val langs = remember { getLangs(context) }
|
||||
var currentLanguage by remember {
|
||||
mutableStateOf(
|
||||
AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "",
|
||||
)
|
||||
}
|
||||
val now = remember { Instant.now().toEpochMilli() }
|
||||
|
||||
val dateFormat by uiPreferences.dateFormat().collectAsState()
|
||||
|
@ -121,67 +110,43 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
UiPreferences.dateFormat(dateFormat).format(now)
|
||||
}
|
||||
|
||||
LaunchedEffect(currentLanguage) {
|
||||
val locale = if (currentLanguage.isEmpty()) {
|
||||
LocaleListCompat.getEmptyLocaleList()
|
||||
} else {
|
||||
LocaleListCompat.forLanguageTags(currentLanguage)
|
||||
}
|
||||
AppCompatDelegate.setApplicationLocales(locale)
|
||||
}
|
||||
|
||||
val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
libraryPrefs.bottomNavStyle().changes()
|
||||
.drop(1)
|
||||
.collectLatest { value ->
|
||||
HomeScreen.tabs = when (value) {
|
||||
0 -> HomeScreen.tabsNoHistory
|
||||
1 -> HomeScreen.tabsNoUpdates
|
||||
else -> HomeScreen.tabsNoManga
|
||||
}
|
||||
(context as? Activity)?.let {
|
||||
ActivityCompat.recreate(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPrefs.bottomNavStyle(),
|
||||
title = stringResource(MR.strings.pref_bottom_nav_style),
|
||||
entries = mapOf(
|
||||
0 to stringResource(MR.strings.pref_bottom_nav_no_history),
|
||||
1 to stringResource(MR.strings.pref_bottom_nav_no_updates),
|
||||
2 to stringResource(MR.strings.pref_bottom_nav_no_manga),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPrefs.isDefaultHomeTabLibraryManga(),
|
||||
title = stringResource(MR.strings.pref_default_home_tab_library),
|
||||
enabled = libraryPrefs.bottomNavStyle().get() != 2,
|
||||
),
|
||||
Preference.PreferenceItem.BasicListPreference(
|
||||
value = currentLanguage,
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_app_language),
|
||||
entries = langs,
|
||||
onValueChanged = { newValue ->
|
||||
currentLanguage = newValue
|
||||
true
|
||||
},
|
||||
onClick = { navigator.push(AppLanguageScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.tabletUiMode(),
|
||||
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
||||
entries = TabletUiMode.entries.associateWith { stringResource(it.titleRes) },
|
||||
entries = TabletUiMode.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
onValueChanged = {
|
||||
context.stringResource(MR.strings.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.startScreen(),
|
||||
title = stringResource(MR.strings.pref_start_screen),
|
||||
entries = StartScreen.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
onValueChanged = {
|
||||
context.stringResource(MR.strings.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.navStyle(),
|
||||
title = "Navigation Style",
|
||||
entries = NavStyle.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
onValueChanged = { true },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.dateFormat(),
|
||||
title = stringResource(MR.strings.pref_date_format),
|
||||
|
@ -189,7 +154,8 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
.associateWith {
|
||||
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
||||
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
|
||||
},
|
||||
}
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = uiPreferences.relativeTime(),
|
||||
|
@ -203,30 +169,6 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
),
|
||||
)
|
||||
}
|
||||
private fun getLangs(context: Context): Map<String, String> {
|
||||
val langs = mutableListOf<Pair<String, String>>()
|
||||
val parser = context.resources.getXml(R.xml.locales_config)
|
||||
var eventType = parser.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (eventType == XmlPullParser.START_TAG && parser.name == "locale") {
|
||||
for (i in 0..<parser.attributeCount) {
|
||||
if (parser.getAttributeName(i) == "name") {
|
||||
val langTag = parser.getAttributeValue(i)
|
||||
val displayName = LocaleHelper.getDisplayName(langTag)
|
||||
if (displayName.isNotEmpty()) {
|
||||
langs.add(Pair(langTag, displayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
|
||||
langs.sortBy { it.second }
|
||||
langs.add(0, Pair("", context.stringResource(MR.strings.label_default)))
|
||||
|
||||
return langs.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
private val DateFormats = listOf(
|
||||
|
|
|
@ -2,15 +2,23 @@ package eu.kanade.presentation.more.settings.screen
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.browse.AnimeExtensionReposScreen
|
||||
import eu.kanade.presentation.more.settings.screen.browse.MangaExtensionReposScreen
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
@ -23,11 +31,16 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
||||
val mangaReposCount by sourcePreferences.mangaExtensionRepos().collectAsState()
|
||||
val animeReposCount by sourcePreferences.animeExtensionRepos().collectAsState()
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_sources),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.hideInAnimeLibraryItems(),
|
||||
title = stringResource(MR.strings.pref_hide_in_anime_library_items),
|
||||
|
@ -36,11 +49,33 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||
pref = sourcePreferences.hideInMangaLibraryItems(),
|
||||
title = stringResource(MR.strings.pref_hide_in_manga_library_items),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.label_anime_extension_repos),
|
||||
subtitle = pluralStringResource(
|
||||
MR.plurals.num_repos,
|
||||
animeReposCount.size,
|
||||
animeReposCount.size,
|
||||
),
|
||||
onClick = {
|
||||
navigator.push(AnimeExtensionReposScreen())
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.label_manga_extension_repos),
|
||||
subtitle = pluralStringResource(
|
||||
MR.plurals.num_repos,
|
||||
mangaReposCount.size,
|
||||
mangaReposCount.size,
|
||||
),
|
||||
onClick = {
|
||||
navigator.push(MangaExtensionReposScreen())
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_nsfw_content),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.showNsfwSource(),
|
||||
title = stringResource(MR.strings.pref_show_nsfw_source),
|
||||
|
|
|
@ -4,63 +4,55 @@ import android.content.ActivityNotFoundException
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.text.format.Formatter
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.core.net.toUri
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.StorageInfo
|
||||
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.EpisodeCache
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.core.storage.displayablePath
|
||||
import tachiyomi.core.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.backup.service.BackupPreferences
|
||||
import tachiyomi.domain.backup.service.FLAG_CATEGORIES
|
||||
import tachiyomi.domain.backup.service.FLAG_CHAPTERS
|
||||
import tachiyomi.domain.backup.service.FLAG_EXTENSIONS
|
||||
import tachiyomi.domain.backup.service.FLAG_EXT_SETTINGS
|
||||
import tachiyomi.domain.backup.service.FLAG_HISTORY
|
||||
import tachiyomi.domain.backup.service.FLAG_SETTINGS
|
||||
import tachiyomi.domain.backup.service.FLAG_TRACK
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
|
@ -71,21 +63,35 @@ import uy.kohesive.injekt.api.get
|
|||
|
||||
object SettingsDataScreen : SearchableSettings {
|
||||
|
||||
val restorePreferenceKeyString = MR.strings.label_backup
|
||||
const val HELP_URL = "https://aniyomi.org/docs/faq/storage"
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitleRes() = MR.strings.label_data_storage
|
||||
|
||||
@Composable
|
||||
override fun RowScope.AppBarAction() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
IconButton(onClick = { uriHandler.openUri(HELP_URL) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
contentDescription = stringResource(MR.strings.tracking_guide),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
val storagePreferences = Injekt.get<StoragePreferences>()
|
||||
|
||||
return listOf(
|
||||
return persistentListOf(
|
||||
getStorageLocationPref(storagePreferences = storagePreferences),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
|
||||
|
||||
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
||||
getDataGroup(backupPreferences = backupPreferences),
|
||||
getDataGroup(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -107,8 +113,6 @@ object SettingsDataScreen : SearchableSettings {
|
|||
UniFile.fromUri(context, uri)?.let {
|
||||
storageDirPref.set(it.uri.toString())
|
||||
}
|
||||
Injekt.get<AnimeDownloadCache>().invalidateCache()
|
||||
Injekt.get<MangaDownloadCache>().invalidateCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,13 +124,13 @@ object SettingsDataScreen : SearchableSettings {
|
|||
val context = LocalContext.current
|
||||
val storageDir by storageDirPref.collectAsState()
|
||||
|
||||
if (storageDir == storageDirPref.defaultValue()) {
|
||||
if (!storageDirPref.isSet()) {
|
||||
return stringResource(MR.strings.no_location_set)
|
||||
}
|
||||
|
||||
return remember(storageDir) {
|
||||
val file = UniFile.fromUri(context, storageDir.toUri())
|
||||
file?.filePath ?: file?.uri?.toString()
|
||||
file?.displayablePath
|
||||
} ?: stringResource(MR.strings.invalid_location, storageDir)
|
||||
}
|
||||
|
||||
|
@ -153,20 +157,75 @@ object SettingsDataScreen : SearchableSettings {
|
|||
@Composable
|
||||
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
||||
|
||||
val chooseBackup = rememberLauncherForActivityResult(
|
||||
object : ActivityResultContracts.GetContent() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (it == null) {
|
||||
context.toast(MR.strings.file_null_uri_error)
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
navigator.push(RestoreBackupScreen(it.toString()))
|
||||
}
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_backup),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
// Manual actions
|
||||
getCreateBackupPref(),
|
||||
getRestoreBackupPref(),
|
||||
Preference.PreferenceItem.CustomPreference(
|
||||
title = stringResource(restorePreferenceKeyString),
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
subcomponent = {
|
||||
MultiChoiceSegmentedButtonRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = PrefsHorizontalPadding),
|
||||
) {
|
||||
SegmentedButton(
|
||||
checked = false,
|
||||
onCheckedChange = { navigator.push(CreateBackupScreen()) },
|
||||
shape = SegmentedButtonDefaults.itemShape(0, 2),
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_create_backup))
|
||||
}
|
||||
SegmentedButton(
|
||||
checked = false,
|
||||
onCheckedChange = {
|
||||
if (!BackupRestoreJob.isRunning(context)) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
context.toast(MR.strings.restore_miui_warning)
|
||||
}
|
||||
|
||||
// no need to catch because it's wrapped with a chooser
|
||||
chooseBackup.launch("*/*")
|
||||
} else {
|
||||
context.toast(MR.strings.restore_in_progress)
|
||||
}
|
||||
},
|
||||
shape = SegmentedButtonDefaults.itemShape(1, 2),
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_restore_backup))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
// Automatic backups
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = backupPreferences.backupInterval(),
|
||||
title = stringResource(MR.strings.pref_backup_interval),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.off),
|
||||
6 to stringResource(MR.strings.update_6hour),
|
||||
12 to stringResource(MR.strings.update_12hour),
|
||||
|
@ -188,181 +247,44 @@ object SettingsDataScreen : SearchableSettings {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_create_backup),
|
||||
subtitle = stringResource(MR.strings.pref_create_backup_summ),
|
||||
onClick = { navigator.push(CreateBackupScreen()) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
|
||||
private fun getDataGroup(): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
var error by remember { mutableStateOf<Any?>(null) }
|
||||
if (error != null) {
|
||||
val onDismissRequest = { error = null }
|
||||
when (val err = error) {
|
||||
is InvalidRestore -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
|
||||
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
context.copyToClipboard(err.message, err.message)
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_copy_to_clipboard))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is MissingRestoreComponents -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
val msg = buildString {
|
||||
append(stringResource(MR.strings.backup_restore_content_full))
|
||||
if (err.sources.isNotEmpty()) {
|
||||
append("\n\n").append(
|
||||
stringResource(MR.strings.backup_restore_missing_sources),
|
||||
)
|
||||
err.sources.joinTo(
|
||||
this,
|
||||
separator = "\n- ",
|
||||
prefix = "\n- ",
|
||||
)
|
||||
}
|
||||
if (err.trackers.isNotEmpty()) {
|
||||
append("\n\n").append(
|
||||
stringResource(MR.strings.backup_restore_missing_trackers),
|
||||
)
|
||||
err.trackers.joinTo(
|
||||
this,
|
||||
separator = "\n- ",
|
||||
prefix = "\n- ",
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(text = msg)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
BackupRestoreJob.start(context, err.uri)
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_restore))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
else -> error = null // Unknown
|
||||
}
|
||||
}
|
||||
|
||||
val chooseBackup = rememberLauncherForActivityResult(
|
||||
object : ActivityResultContracts.GetContent() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
return Intent.createChooser(
|
||||
intent,
|
||||
context.stringResource(MR.strings.file_select_backup),
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (it == null) {
|
||||
context.stringResource(MR.strings.file_null_uri_error)
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
val results = try {
|
||||
BackupFileValidator().validate(context, it)
|
||||
} catch (e: Exception) {
|
||||
error = InvalidRestore(it, e.message.toString())
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
|
||||
BackupRestoreJob.start(context, it)
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
|
||||
}
|
||||
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_restore_backup),
|
||||
subtitle = stringResource(MR.strings.pref_restore_backup_summ),
|
||||
onClick = {
|
||||
if (!BackupRestoreJob.isRunning(context)) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
context.stringResource(MR.strings.restore_miui_warning, Toast.LENGTH_LONG)
|
||||
}
|
||||
// no need to catch because it's wrapped with a chooser
|
||||
chooseBackup.launch("*/*")
|
||||
} else {
|
||||
context.stringResource(MR.strings.restore_in_progress)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDataGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||
|
||||
val backupIntervalPref = backupPreferences.backupInterval()
|
||||
val backupInterval by backupIntervalPref.collectAsState()
|
||||
|
||||
val chapterCache = remember { Injekt.get<ChapterCache>() }
|
||||
val episodeCache = remember { Injekt.get<EpisodeCache>() }
|
||||
var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
|
||||
val cacheReadableMangaSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
|
||||
val cacheReadableAnimeSize = remember(cacheReadableSizeSema) { episodeCache.readableSize }
|
||||
val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_data),
|
||||
preferenceItems = listOf(
|
||||
getMangaStorageInfoPref(cacheReadableMangaSize),
|
||||
getAnimeStorageInfoPref(cacheReadableAnimeSize),
|
||||
title = stringResource(MR.strings.pref_storage_usage),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.CustomPreference(
|
||||
title = stringResource(MR.strings.pref_storage_usage),
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
subcomponent = {
|
||||
StorageInfo(
|
||||
modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_clear_chapter_cache),
|
||||
subtitle = stringResource(
|
||||
MR.strings.used_cache_both,
|
||||
cacheReadableAnimeSize,
|
||||
cacheReadableMangaSize,
|
||||
),
|
||||
subtitle = stringResource(MR.strings.used_cache, cacheReadableSize),
|
||||
onClick = {
|
||||
scope.launchNonCancellable {
|
||||
try {
|
||||
val deletedFiles = chapterCache.clear() + episodeCache.clear()
|
||||
val deletedFiles = chapterCache.clear()
|
||||
withUIContext {
|
||||
context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles))
|
||||
cacheReadableSizeSema++
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
withUIContext { context.stringResource(MR.strings.cache_delete_error) }
|
||||
withUIContext { context.toast(MR.strings.cache_delete_error) }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -371,93 +293,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||
pref = libraryPreferences.autoClearItemCache(),
|
||||
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = backupPreferences.backupFlags(),
|
||||
enabled = backupInterval != 0,
|
||||
title = stringResource(MR.strings.pref_backup_flags),
|
||||
subtitle = stringResource(MR.strings.pref_backup_flags_summary),
|
||||
entries = mapOf(
|
||||
FLAG_CATEGORIES to stringResource(MR.strings.general_categories),
|
||||
FLAG_CHAPTERS to stringResource(MR.strings.chapters_episodes),
|
||||
FLAG_HISTORY to stringResource(MR.strings.history),
|
||||
FLAG_TRACK to stringResource(MR.strings.track),
|
||||
FLAG_SETTINGS to stringResource(MR.strings.settings),
|
||||
FLAG_EXT_SETTINGS to stringResource(MR.strings.extension_settings),
|
||||
FLAG_EXTENSIONS to stringResource(MR.strings.label_extensions),
|
||||
),
|
||||
onValueChanged = {
|
||||
if (FLAG_SETTINGS in it || FLAG_EXT_SETTINGS in it) {
|
||||
context.stringResource(MR.strings.backup_settings_warning, Toast.LENGTH_LONG)
|
||||
}
|
||||
true
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getMangaStorageInfoPref(
|
||||
chapterCacheReadableSize: String,
|
||||
): Preference.PreferenceItem.CustomPreference {
|
||||
val context = LocalContext.current
|
||||
val available = remember {
|
||||
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
|
||||
}
|
||||
val total = remember {
|
||||
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
|
||||
}
|
||||
|
||||
return Preference.PreferenceItem.CustomPreference(
|
||||
title = stringResource(MR.strings.pref_manga_storage_usage),
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
title = stringResource(MR.strings.pref_manga_storage_usage),
|
||||
subcomponent = {
|
||||
// TODO: downloads, SD cards, bar representation?, i18n
|
||||
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
|
||||
Text(text = "Available: $available / $total (chapter cache: $chapterCacheReadableSize)")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getAnimeStorageInfoPref(
|
||||
episodeCacheReadableSize: String,
|
||||
): Preference.PreferenceItem.CustomPreference {
|
||||
val context = LocalContext.current
|
||||
val available = remember {
|
||||
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
|
||||
}
|
||||
val total = remember {
|
||||
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
|
||||
}
|
||||
|
||||
return Preference.PreferenceItem.CustomPreference(
|
||||
title = stringResource(MR.strings.pref_anime_storage_usage),
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
title = stringResource(MR.strings.pref_anime_storage_usage),
|
||||
subcomponent = {
|
||||
// TODO: downloads, SD cards, bar representation?, i18n
|
||||
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
|
||||
Text(text = "Available: $available / $total (Episode cache: $episodeCacheReadableSize)")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class MissingRestoreComponents(
|
||||
val uri: Uri,
|
||||
val sources: List<String>,
|
||||
val trackers: List<String>,
|
||||
)
|
||||
|
||||
private data class InvalidRestore(
|
||||
val uri: Uri? = null,
|
||||
val message: String,
|
||||
)
|
||||
|
|
|
@ -1,24 +1,42 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.OutlinedNumericChooser
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
@ -33,23 +51,59 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val getCategories = remember { Injekt.get<GetMangaCategories>() }
|
||||
val allCategories by getCategories.subscribe().collectAsState(
|
||||
initial = runBlocking { getCategories.await() },
|
||||
val getMangaCategories = remember { Injekt.get<GetMangaCategories>() }
|
||||
val allMangaCategories by getMangaCategories.subscribe().collectAsState(
|
||||
initial = runBlocking { getMangaCategories.await() },
|
||||
)
|
||||
val getAnimeCategories = remember { Injekt.get<GetAnimeCategories>() }
|
||||
val allAnimeCategories by getAnimeCategories.subscribe().collectAsState(
|
||||
initial = runBlocking { getAnimeCategories.await() },
|
||||
)
|
||||
|
||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||
|
||||
val speedLimit by downloadPreferences.downloadSpeedLimit().collectAsState()
|
||||
var currentSpeedLimit by remember { mutableIntStateOf(speedLimit) }
|
||||
var showDownloadLimitDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showDownloadLimitDialog) {
|
||||
DownloadLimitDialog(
|
||||
initialValue = currentSpeedLimit,
|
||||
onDismissRequest = { showDownloadLimitDialog = false },
|
||||
onValueChanged = {
|
||||
currentSpeedLimit = it
|
||||
},
|
||||
onConfirm = {
|
||||
downloadPreferences.downloadSpeedLimit().set(currentSpeedLimit)
|
||||
showDownloadLimitDialog = false
|
||||
},
|
||||
)
|
||||
}
|
||||
val multithreadingDownload by downloadPreferences.multithreadingDownload().collectAsState()
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.downloadOnlyOverWifi(),
|
||||
title = stringResource(MR.strings.connected_to_wifi),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.multithreadingDownload(),
|
||||
title = stringResource(MR.strings.multi_thread_download),
|
||||
subtitle = stringResource(MR.strings.multi_thread_download_summary),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.numberOfThreads(),
|
||||
title = stringResource(MR.strings.multi_thread_download_threads_number),
|
||||
subtitle = stringResource(MR.strings.multi_thread_download_threads_number_summary),
|
||||
entries = (1..64).associateWith { it.toString() }.toImmutableMap(),
|
||||
enabled = multithreadingDownload,
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.download_speed_limit),
|
||||
subtitle = if (speedLimit == 0) {
|
||||
stringResource(MR.strings.off)
|
||||
} else {
|
||||
"$speedLimit KiB/s"
|
||||
},
|
||||
onClick = { showDownloadLimitDialog = true },
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.saveChaptersAsCBZ(),
|
||||
title = stringResource(MR.strings.save_chapter_as_cbz),
|
||||
|
@ -62,17 +116,18 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.numberOfDownloads(),
|
||||
title = stringResource(MR.strings.pref_download_slots),
|
||||
entries = (1..5).associateWith { it.toString() },
|
||||
entries = (1..5).associateWith { it.toString() }.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_slots_info)),
|
||||
getDeleteChaptersGroup(
|
||||
downloadPreferences = downloadPreferences,
|
||||
categories = allCategories,
|
||||
animeCategories = allAnimeCategories,
|
||||
mangaCategories = allMangaCategories,
|
||||
),
|
||||
getAutoDownloadGroup(
|
||||
downloadPreferences = downloadPreferences,
|
||||
allCategories = allCategories,
|
||||
allAnimeCategories = allAnimeCategories,
|
||||
allMangaCategories = allMangaCategories,
|
||||
),
|
||||
getDownloadAheadGroup(downloadPreferences = downloadPreferences),
|
||||
getExternalDownloaderGroup(
|
||||
|
@ -85,11 +140,12 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
@Composable
|
||||
private fun getDeleteChaptersGroup(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
categories: List<Category>,
|
||||
animeCategories: List<Category>,
|
||||
mangaCategories: List<Category>,
|
||||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_delete_chapters),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.removeAfterMarkedAsRead(),
|
||||
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
|
||||
|
@ -97,7 +153,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.removeAfterReadSlots(),
|
||||
title = stringResource(MR.strings.pref_remove_after_read),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
0 to stringResource(MR.strings.last_read_chapter),
|
||||
1 to stringResource(MR.strings.second_to_last),
|
||||
|
@ -110,9 +166,13 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
pref = downloadPreferences.removeBookmarkedChapters(),
|
||||
title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
|
||||
),
|
||||
getExcludedAnimeCategoriesPreference(
|
||||
downloadPreferences = downloadPreferences,
|
||||
categories = { animeCategories },
|
||||
),
|
||||
getExcludedCategoriesPreference(
|
||||
downloadPreferences = downloadPreferences,
|
||||
categories = { categories },
|
||||
categories = { mangaCategories },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -126,15 +186,31 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = downloadPreferences.removeExcludeCategories(),
|
||||
title = stringResource(MR.strings.pref_remove_exclude_categories_manga),
|
||||
entries = categories().associate { it.id.toString() to it.visualName },
|
||||
entries = categories()
|
||||
.associate { it.id.toString() to it.visualName }
|
||||
.toImmutableMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getExcludedAnimeCategoriesPreference(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
categories: () -> List<Category>,
|
||||
): Preference.PreferenceItem.MultiSelectListPreference {
|
||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = downloadPreferences.removeExcludeCategories(),
|
||||
title = stringResource(MR.strings.pref_remove_exclude_categories_anime),
|
||||
entries = categories()
|
||||
.associate { it.id.toString() to it.visualName }
|
||||
.toImmutableMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAutoDownloadGroup(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
allCategories: List<Category>,
|
||||
allAnimeCategories: List<Category>,
|
||||
allMangaCategories: List<Category>,
|
||||
): Preference.PreferenceGroup {
|
||||
val downloadNewEpisodesPref = downloadPreferences.downloadNewEpisodes()
|
||||
val downloadNewEpisodeCategoriesPref = downloadPreferences.downloadNewEpisodeCategories()
|
||||
|
@ -179,9 +255,9 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
TriStateListDialog(
|
||||
title = stringResource(MR.strings.manga_categories),
|
||||
message = stringResource(MR.strings.pref_download_new_categories_details),
|
||||
items = allCategories,
|
||||
initialChecked = included.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
|
||||
initialInversed = excluded.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
|
||||
items = allMangaCategories,
|
||||
initialChecked = included.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } },
|
||||
initialInversed = excluded.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } },
|
||||
itemLabel = { it.visualName },
|
||||
onDismissRequest = { showDialog = false },
|
||||
onValueChanged = { newIncluded, newExcluded ->
|
||||
|
@ -198,7 +274,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_auto_download),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadNewEpisodesPref,
|
||||
title = stringResource(MR.strings.pref_download_new_episodes),
|
||||
|
@ -220,7 +296,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.manga_categories),
|
||||
subtitle = getCategoriesLabel(
|
||||
allCategories = allCategories,
|
||||
allCategories = allMangaCategories,
|
||||
included = included,
|
||||
excluded = excluded,
|
||||
),
|
||||
|
@ -237,36 +313,32 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.download_ahead),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.autoDownloadWhileReading(),
|
||||
title = stringResource(MR.strings.auto_download_while_reading),
|
||||
entries = listOf(0, 2, 3, 5, 10).associateWith {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.disabled)
|
||||
} else {
|
||||
pluralStringResource(
|
||||
MR.plurals.next_unread_chapters,
|
||||
count = it,
|
||||
it,
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.autoDownloadWhileWatching(),
|
||||
title = stringResource(MR.strings.auto_download_while_watching),
|
||||
entries = listOf(0, 2, 3, 5, 10).associateWith {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.disabled)
|
||||
} else {
|
||||
pluralStringResource(
|
||||
MR.plurals.next_unseen_episodes,
|
||||
count = it,
|
||||
it,
|
||||
)
|
||||
entries = listOf(0, 2, 3, 5, 10)
|
||||
.associateWith {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.disabled)
|
||||
} else {
|
||||
pluralStringResource(MR.plurals.next_unseen_episodes, count = it, it)
|
||||
}
|
||||
}
|
||||
},
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.autoDownloadWhileReading(),
|
||||
title = stringResource(MR.strings.auto_download_while_reading),
|
||||
entries = listOf(0, 2, 3, 5, 10)
|
||||
.associateWith {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.disabled)
|
||||
} else {
|
||||
pluralStringResource(MR.plurals.next_unread_chapters, count = it, it)
|
||||
}
|
||||
}
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(
|
||||
stringResource(MR.strings.download_ahead_info),
|
||||
|
@ -299,12 +371,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
.map { pm.getApplicationLabel(it.applicationInfo).toString() }
|
||||
|
||||
val packageNamesMap: Map<String, String> =
|
||||
packageNames.zip(packageNamesReadable)
|
||||
.toMap()
|
||||
mapOf("" to "None") + packageNames.zip(packageNamesReadable).toMap()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_external_downloader),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = useExternalDownloader,
|
||||
title = stringResource(MR.strings.pref_use_external_downloader),
|
||||
|
@ -312,9 +383,58 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = externalDownloaderPreference,
|
||||
title = stringResource(MR.strings.pref_external_downloader_selection),
|
||||
entries = mapOf("" to "None") + packageNamesMap,
|
||||
entries = packageNamesMap.toPersistentMap(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadLimitDialog(
|
||||
initialValue: Int,
|
||||
onDismissRequest: () -> Unit,
|
||||
onValueChanged: (newValue: Int) -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(stringResource(MR.strings.download_speed_limit)) },
|
||||
text = {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(bottom = MaterialTheme.padding.medium)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
OutlinedNumericChooser(
|
||||
label = stringResource(MR.strings.download_speed_limit),
|
||||
placeholder = "0",
|
||||
suffix = "KiB/s",
|
||||
value = initialValue,
|
||||
step = 100,
|
||||
min = 0,
|
||||
onValueChanged = onValueChanged,
|
||||
)
|
||||
}
|
||||
Text(text = stringResource(MR.strings.download_speed_limit_hint))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onConfirm()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
|||
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.ui.category.CategoriesTab
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||
|
@ -66,8 +69,8 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
libraryPreferences,
|
||||
),
|
||||
getGlobalUpdateGroup(allCategories, allAnimeCategories, libraryPreferences),
|
||||
getChapterSwipeActionsGroup(libraryPreferences),
|
||||
getEpisodeSwipeActionsGroup(libraryPreferences),
|
||||
getChapterSwipeActionsGroup(libraryPreferences),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -78,7 +81,6 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
allAnimeCategories: List<Category>,
|
||||
libraryPreferences: LibraryPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
||||
val userAnimeCategoriesCount = allAnimeCategories.filterNot(Category::isSystemCategory).size
|
||||
|
@ -96,13 +98,13 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
allAnimeCategories.fastMap { it.id.toInt() }
|
||||
|
||||
val mangaLabels = listOf(stringResource(MR.strings.default_category_summary)) +
|
||||
allCategories.fastMap { it.visualName(context) }
|
||||
allCategories.fastMap { it.visualName }
|
||||
val animeLabels = listOf(stringResource(MR.strings.default_category_summary)) +
|
||||
allAnimeCategories.fastMap { it.visualName(context) }
|
||||
allAnimeCategories.fastMap { it.visualName }
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.general_categories),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.action_edit_anime_categories),
|
||||
subtitle = pluralStringResource(
|
||||
|
@ -117,7 +119,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
title = stringResource(MR.strings.default_anime_category),
|
||||
subtitle = selectedAnimeCategory?.visualName
|
||||
?: stringResource(MR.strings.default_category_summary),
|
||||
entries = animeIds.zip(animeLabels).toMap(),
|
||||
entries = animeIds.zip(animeLabels).toMap().toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.action_edit_manga_categories),
|
||||
|
@ -131,9 +133,8 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.defaultMangaCategory(),
|
||||
title = stringResource(MR.strings.default_manga_category),
|
||||
subtitle = selectedCategory?.visualName
|
||||
?: stringResource(MR.strings.default_category_summary),
|
||||
entries = mangaIds.zip(mangaLabels).toMap(),
|
||||
subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
|
||||
entries = mangaIds.zip(mangaLabels).toMap().toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.categorizedDisplaySettings(),
|
||||
|
@ -222,11 +223,11 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_library_update),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = autoUpdateIntervalPref,
|
||||
title = stringResource(MR.strings.pref_library_update_interval),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.update_never),
|
||||
12 to stringResource(MR.strings.update_12hour),
|
||||
24 to stringResource(MR.strings.update_24hour),
|
||||
|
@ -245,7 +246,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
enabled = autoUpdateInterval > 0,
|
||||
title = stringResource(MR.strings.pref_library_update_restriction),
|
||||
subtitle = stringResource(MR.strings.restrictions),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
|
||||
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
|
||||
DEVICE_CHARGING to stringResource(MR.strings.charging),
|
||||
|
@ -284,18 +285,12 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryPreferences.autoUpdateItemRestrictions(),
|
||||
title = stringResource(MR.strings.pref_library_update_manga_restriction),
|
||||
entries = mapOf(
|
||||
ENTRY_HAS_UNVIEWED to stringResource(
|
||||
MR.strings.pref_update_only_completely_read,
|
||||
),
|
||||
title = stringResource(MR.strings.pref_library_update_smart_update),
|
||||
entries = persistentMapOf(
|
||||
ENTRY_HAS_UNVIEWED to stringResource(MR.strings.pref_update_only_completely_read),
|
||||
ENTRY_NON_VIEWED to stringResource(MR.strings.pref_update_only_started),
|
||||
ENTRY_NON_COMPLETED to stringResource(
|
||||
MR.strings.pref_update_only_non_completed,
|
||||
),
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(
|
||||
MR.strings.pref_update_only_in_release_period,
|
||||
),
|
||||
ENTRY_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
|
@ -312,41 +307,33 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_chapter_swipe),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeChapterStartAction(),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
||||
entries = mapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(
|
||||
MR.strings.action_disable,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(
|
||||
MR.strings.action_bookmark,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(
|
||||
MR.strings.action_mark_as_read,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
|
||||
MR.strings.action_download,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
||||
stringResource(MR.strings.action_bookmark),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to
|
||||
stringResource(MR.strings.action_mark_as_read),
|
||||
LibraryPreferences.ChapterSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeChapterEndAction(),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
||||
entries = mapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(
|
||||
MR.strings.action_disable,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(
|
||||
MR.strings.action_bookmark,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(
|
||||
MR.strings.action_mark_as_read,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
|
||||
MR.strings.action_download,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
||||
stringResource(MR.strings.action_bookmark),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to
|
||||
stringResource(MR.strings.action_mark_as_read),
|
||||
LibraryPreferences.ChapterSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -359,41 +346,33 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_episode_swipe),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeEpisodeStartAction(),
|
||||
title = stringResource(MR.strings.pref_episode_swipe_start),
|
||||
entries = mapOf(
|
||||
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(
|
||||
MR.strings.action_disable,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(
|
||||
MR.strings.action_bookmark_episode,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource(
|
||||
MR.strings.action_mark_as_seen,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
|
||||
MR.strings.action_download,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.EpisodeSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
|
||||
stringResource(MR.strings.action_bookmark_episode),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
|
||||
stringResource(MR.strings.action_mark_as_seen),
|
||||
LibraryPreferences.EpisodeSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeEpisodeEndAction(),
|
||||
title = stringResource(MR.strings.pref_episode_swipe_end),
|
||||
entries = mapOf(
|
||||
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(
|
||||
MR.strings.action_disable,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(
|
||||
MR.strings.action_bookmark_episode,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource(
|
||||
MR.strings.action_mark_as_seen,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
|
||||
MR.strings.action_download,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.EpisodeSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
|
||||
stringResource(MR.strings.action_bookmark_episode),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
|
||||
stringResource(MR.strings.action_mark_as_seen),
|
||||
LibraryPreferences.EpisodeSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -186,18 +186,18 @@ object SettingsMainScreen : Screen() {
|
|||
icon = Icons.Outlined.CollectionsBookmark,
|
||||
screen = SettingsLibraryScreen,
|
||||
),
|
||||
Item(
|
||||
titleRes = MR.strings.pref_category_reader,
|
||||
subtitleRes = MR.strings.pref_reader_summary,
|
||||
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
|
||||
screen = SettingsReaderScreen,
|
||||
),
|
||||
Item(
|
||||
titleRes = MR.strings.pref_category_player,
|
||||
subtitleRes = MR.strings.pref_player_summary,
|
||||
icon = Icons.Outlined.PlayCircleOutline,
|
||||
screen = SettingsPlayerScreen,
|
||||
),
|
||||
Item(
|
||||
titleRes = MR.strings.pref_category_reader,
|
||||
subtitleRes = MR.strings.pref_reader_summary,
|
||||
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
|
||||
screen = SettingsReaderScreen,
|
||||
),
|
||||
Item(
|
||||
titleRes = MR.strings.pref_category_downloads,
|
||||
subtitleRes = MR.strings.pref_downloads_summary,
|
||||
|
|
|
@ -33,7 +33,10 @@ import eu.kanade.tachiyomi.ui.player.VLC_PLAYER
|
|||
import eu.kanade.tachiyomi.ui.player.WEB_VIDEO_CASTER
|
||||
import eu.kanade.tachiyomi.ui.player.X_PLAYER
|
||||
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@ -57,7 +60,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = playerPreferences.progressPreference(),
|
||||
title = stringResource(MR.strings.pref_progress_mark_as_seen),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
1.00F to stringResource(MR.strings.pref_progress_100),
|
||||
0.95F to stringResource(MR.strings.pref_progress_95),
|
||||
0.90F to stringResource(MR.strings.pref_progress_90),
|
||||
|
@ -91,7 +94,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_internal_player),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = playerFullscreen,
|
||||
title = stringResource(MR.strings.pref_player_fullscreen),
|
||||
|
@ -118,7 +121,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_volume_brightness),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = enableVolumeBrightnessGestures,
|
||||
title = stringResource(MR.strings.enable_volume_brightness_gestures),
|
||||
|
@ -144,11 +147,11 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_player_orientation),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = defaultPlayerOrientationType,
|
||||
title = stringResource(MR.strings.pref_default_player_orientation),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR to stringResource(
|
||||
MR.strings.rotation_free,
|
||||
),
|
||||
|
@ -179,7 +182,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = defaultPlayerOrientationPortrait,
|
||||
title = stringResource(MR.strings.pref_default_portrait_orientation),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT to stringResource(
|
||||
MR.strings.rotation_portrait,
|
||||
),
|
||||
|
@ -194,7 +197,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = defaultPlayerOrientationLandscape,
|
||||
title = stringResource(MR.strings.pref_default_landscape_orientation),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE to stringResource(
|
||||
MR.strings.rotation_landscape,
|
||||
),
|
||||
|
@ -241,7 +244,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_player_seeking),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = enableHorizontalSeekGesture,
|
||||
title = stringResource(MR.strings.enable_horizontal_seek_gesture),
|
||||
|
@ -254,7 +257,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = skipLengthPreference,
|
||||
title = stringResource(MR.strings.pref_skip_length),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
30 to stringResource(MR.strings.pref_skip_30),
|
||||
20 to stringResource(MR.strings.pref_skip_20),
|
||||
10 to stringResource(MR.strings.pref_skip_10),
|
||||
|
@ -293,7 +296,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = waitingTimeAniSkip,
|
||||
title = stringResource(MR.strings.pref_waiting_time_aniskip),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
5 to stringResource(MR.strings.pref_waiting_time_aniskip_5),
|
||||
6 to stringResource(MR.strings.pref_waiting_time_aniskip_6),
|
||||
7 to stringResource(MR.strings.pref_waiting_time_aniskip_7),
|
||||
|
@ -318,7 +321,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_pip),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = enablePip,
|
||||
title = stringResource(MR.strings.pref_enable_pip),
|
||||
|
@ -364,7 +367,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_external_player),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = alwaysUseExternalPlayer,
|
||||
title = stringResource(MR.strings.pref_always_use_external_player),
|
||||
|
@ -372,7 +375,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = externalPlayerPreference,
|
||||
title = stringResource(MR.strings.pref_external_player_preference),
|
||||
entries = mapOf("" to "None") + packageNamesMap,
|
||||
entries = (mapOf("" to "None") + packageNamesMap).toPersistentMap(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -10,6 +10,9 @@ import eu.kanade.presentation.more.settings.Preference
|
|||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
@ -31,12 +34,13 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
pref = readerPref.defaultReadingMode(),
|
||||
title = stringResource(MR.strings.pref_viewer_type),
|
||||
entries = ReadingMode.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) },
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.doubleTapAnimSpeed(),
|
||||
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.double_tap_anim_speed_0),
|
||||
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
|
||||
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
|
||||
|
@ -82,17 +86,18 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
val fullscreen by fullscreenPref.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.defaultOrientationType(),
|
||||
title = stringResource(MR.strings.pref_rotation_type),
|
||||
entries = ReaderOrientation.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) },
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerTheme(),
|
||||
title = stringResource(MR.strings.pref_reader_theme),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.black_background),
|
||||
2 to stringResource(MR.strings.gray_background),
|
||||
0 to stringResource(MR.strings.white_background),
|
||||
|
@ -126,7 +131,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_reading),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.skipRead(),
|
||||
title = stringResource(MR.strings.pref_skip_read_chapters),
|
||||
|
@ -165,29 +170,26 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pager_viewer),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
entries = ReaderPreferences.TapZones
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap(),
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.pagerNavInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
entries = mapOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none),
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(
|
||||
MR.strings.tapping_inverted_horizontal,
|
||||
),
|
||||
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(
|
||||
MR.strings.tapping_inverted_vertical,
|
||||
),
|
||||
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
|
||||
MR.strings.tapping_inverted_both,
|
||||
),
|
||||
),
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
ReaderPreferences.TappingInvertMode.VERTICAL,
|
||||
ReaderPreferences.TappingInvertMode.BOTH,
|
||||
)
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
|
@ -195,14 +197,16 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
title = stringResource(MR.strings.pref_image_scale_type),
|
||||
entries = ReaderPreferences.ImageScaleType
|
||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||
.toMap(),
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.zoomStart(),
|
||||
title = stringResource(MR.strings.pref_zoom_start),
|
||||
entries = ReaderPreferences.ZoomStart
|
||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||
.toMap(),
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cropBorders(),
|
||||
|
@ -265,29 +269,26 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.webtoon_viewer),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
entries = ReaderPreferences.TapZones
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap(),
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.webtoonNavInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
entries = mapOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none),
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(
|
||||
MR.strings.tapping_inverted_horizontal,
|
||||
),
|
||||
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(
|
||||
MR.strings.tapping_inverted_vertical,
|
||||
),
|
||||
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
|
||||
MR.strings.tapping_inverted_both,
|
||||
),
|
||||
),
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
ReaderPreferences.TappingInvertMode.VERTICAL,
|
||||
ReaderPreferences.TappingInvertMode.BOTH,
|
||||
)
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
|
@ -304,19 +305,11 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerHideThreshold(),
|
||||
title = stringResource(MR.strings.pref_hide_threshold),
|
||||
entries = mapOf(
|
||||
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(
|
||||
MR.strings.pref_highest,
|
||||
),
|
||||
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(
|
||||
MR.strings.pref_high,
|
||||
),
|
||||
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(
|
||||
MR.strings.pref_low,
|
||||
),
|
||||
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(
|
||||
MR.strings.pref_lowest,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
|
||||
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
|
||||
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
|
||||
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
|
@ -365,7 +358,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_reader_navigation),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readWithVolumeKeysPref,
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys),
|
||||
|
@ -383,7 +376,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_reader_actions),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.readWithLongTap(),
|
||||
title = stringResource(MR.strings.pref_read_with_long_tap),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue