mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-25 06:11:04 +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 acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.12.3.10)
|
- To the latest version of the app (stable is v0.15.2.4)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have gone through the FAQ (https://aniyomi.org/docs/faq/general) and troubleshooting guide (https://aniyomi.org/docs/guides/troubleshooting/)
|
- I have gone through the FAQ (https://aniyomi.org/docs/faq/general) and troubleshooting guide (https://aniyomi.org/docs/guides/troubleshooting/)
|
||||||
- If this is an issue with an anime extension, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi-extensions
|
- If this is an issue with an official anime extension, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi-extensions
|
||||||
|
- If this is an issue with an official manga extension and this issue can be replicated in the Tachiyomi app, that I should be opening an issue in https://github.com/tachiyomiorg/extensions
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
||||||
- I will fill out the title and the information in this template
|
- I will fill out the title and the information in this template
|
||||||
|
|
||||||
|
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -2,10 +2,13 @@ blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: ⚠️ Anime extension/source issue
|
- name: ⚠️ Anime extension/source issue
|
||||||
url: https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose
|
url: https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose
|
||||||
about: Issues and requests for extensions and sources should be opened in the aniyomi-extensions repository instead
|
about: Issues and requests for official extensions and sources should be opened in the aniyomi-extensions repository instead
|
||||||
- name: 📦 Aniyomi extensions
|
- name: 📦 Aniyomi extensions
|
||||||
url: https://aniyomi.org/extensions/
|
url: https://aniyomi.org/extensions/
|
||||||
about: Anime extensions and sources
|
about: Anime extensions and sources
|
||||||
- name: 🧑💻 Aniyomi help discord
|
- name: 🧑💻 Aniyomi help discord
|
||||||
url: https://discord.gg/F32UjdJZrR
|
url: https://discord.gg/F32UjdJZrR
|
||||||
about: Common questions are answered here
|
about: Common questions are answered here
|
||||||
|
- name: 🖥️ Aniyomi website
|
||||||
|
url: https://aniyomi.org/
|
||||||
|
about: Guides, troubleshooting, and answers to common questions
|
||||||
|
|
8
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
8
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
|
@ -52,7 +52,7 @@ body:
|
||||||
label: Aniyomi version
|
label: Aniyomi version
|
||||||
description: You can find your Aniyomi version in **More → About**.
|
description: You can find your Aniyomi version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "0.12.3.10"
|
Example: "0.15.2.4"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
@ -93,11 +93,13 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
|
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an official manga extension and this issue can be replicated in the Tachiyomi app, that I should be opening an issue in [Tachiyomi's extensions repository](https://github.com/tachiyomiorg/extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have gone through the [FAQ](https://aniyomi.org/docs/faq/general) and [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/).
|
- label: I have gone through the [FAQ](https://aniyomi.org/docs/faq/general) and [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated all installed extensions.
|
- label: I have updated all installed extensions.
|
||||||
required: true
|
required: true
|
||||||
|
|
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
|
@ -30,9 +30,9 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
|
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|
6
.github/workflows/build_pull_request.yml
vendored
6
.github/workflows/build_pull_request.yml
vendored
|
@ -3,8 +3,10 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'i18n/src/main/res/**/strings.xml'
|
- 'i18n/src/commonMain/resources/**/strings-aniyomi.xml'
|
||||||
- 'i18n/src/main/res/**/strings-aniyomi.xml'
|
- 'i18n/src/commonMain/resources/**/strings.xml'
|
||||||
|
- 'i18n/src/commonMain/resources/**/plurals-aniyomi.xml'
|
||||||
|
- 'i18n/src/commonMain/resources/**/plurals.xml'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||||
|
|
2
.github/workflows/build_push.yml
vendored
2
.github/workflows/build_push.yml
vendored
|
@ -66,6 +66,8 @@ jobs:
|
||||||
alias: ${{ secrets.ALIAS }}
|
alias: ${{ secrets.ALIAS }}
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: "34.0.0"
|
||||||
|
|
||||||
- name: Clean up build artifacts
|
- name: Clean up build artifacts
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'aniyomiorg/aniyomi'
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'aniyomiorg/aniyomi'
|
||||||
|
|
14
README.md
14
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) |
|
| [![CI](https://github.com/aniyomiorg/aniyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/aniyomiorg/aniyomi/actions/workflows/build_push.yml) | [![latest preview build](https://img.shields.io/github/v/release/aniyomiorg/aniyomi-preview.svg?maxAge=3600&label=download)](https://github.com/aniyomiorg/aniyomi-preview/releases) | [![CodeFactor](https://www.codefactor.io/repository/github/aniyomiorg/aniyomi/badge)](https://www.codefactor.io/repository/github/aniyomiorg/aniyomi) | [![stable release](https://img.shields.io/github/release/aniyomiorg/aniyomi.svg?maxAge=3600&label=download)](https://github.com/aniyomiorg/aniyomi/releases) | [![Translation status](https://hosted.weblate.org/widgets/aniyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/aniyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/841701076242530374?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/F32UjdJZrR) |
|
||||||
|
|
||||||
|
|
||||||
# ![app icon](.github/readme-images/app-icon.png)Aniyomi
|
# ![app icon](.github/readme-images/app-icon.png)Aniyomi
|
||||||
Aniyomi is an unofficial fork of the free and open source manga reader [Tachiyomi](https://github.com/tachiyomiorg/tachiyomi) that adds anime capabilities! For Android 6.0 and above.
|
Aniyomi is a video player and image viewer for Android 6.0 and above.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
* Watching anime from [a variety of sources](https://github.com/aniyomiorg/aniyomi-extensions)
|
* Watching videos
|
||||||
* Everything you know and love about Tachiyomi:
|
* View images
|
||||||
* Online reading from a variety of sources
|
* Local reading/watching of downloaded content
|
||||||
* Local reading of downloaded content
|
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
|
* A configurable player built on mpv-android with multiple options and settings
|
||||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
* Light and dark themes
|
||||||
* Schedule updating your library for new chapters
|
* Create backups locally to read/watch offline or to your desired cloud service
|
||||||
* Create backups locally to read offline or to your desired cloud service
|
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
Get the app from the [releases page](https://github.com/aniyomiorg/aniyomi/releases).
|
Get the app from the [releases page](https://github.com/aniyomiorg/aniyomi/releases).
|
||||||
|
|
|
@ -20,8 +20,8 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "xyz.jmir.tachiyomi.mi"
|
applicationId = "xyz.jmir.tachiyomi.mi"
|
||||||
|
|
||||||
versionCode = 113
|
versionCode = 121
|
||||||
versionName = "0.14.7"
|
versionName = "0.15.2.4"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
|
@ -130,6 +130,7 @@ android {
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
|
|
||||||
// Disable some unused things
|
// Disable some unused things
|
||||||
aidl = false
|
aidl = false
|
||||||
|
@ -253,7 +254,7 @@ dependencies {
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
implementation(libs.acra.http)
|
implementation(libs.bundles.acra)
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
implementation(libs.bundles.shizuku)
|
implementation(libs.bundles.shizuku)
|
||||||
|
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -50,7 +50,7 @@
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keepattributes *Annotation*, InnerClasses
|
||||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
-dontnote kotlinx.serialization.** # core serialization annotations
|
||||||
|
|
||||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||||
-keepclassmembers class kotlinx.serialization.json.** {
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
<!-- Storage -->
|
<!-- Storage -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
<!-- For background jobs -->
|
<!-- For background jobs -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
@ -20,10 +22,14 @@
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
<!-- To view extension packages in API 30+ -->
|
<!-- To view extension packages in API 30+ -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
|
@ -45,13 +51,64 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
android:theme="@style/Theme.Tachiyomi.SplashScreen">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Deep link to add manga repos -->
|
||||||
|
<intent-filter android:label="@string/action_add_repo">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="tachiyomi" />
|
||||||
|
<data android:host="add-repo" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Deep link to add manga repos -->
|
||||||
|
<intent-filter android:label="@string/action_add_repo">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="aniyomi" />
|
||||||
|
<data android:host="add-repo" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Open backup files -->
|
||||||
|
<intent-filter android:label="@string/pref_restore_backup">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="file" />
|
||||||
|
<data android:scheme="content" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
<!--
|
||||||
|
Work around Android's ugly primitive PatternMatcher
|
||||||
|
implementation that can't cope with finding a . early in
|
||||||
|
the path unless it's explicitly matched.
|
||||||
|
|
||||||
|
See https://stackoverflow.com/a/31028507
|
||||||
|
-->
|
||||||
|
<data android:pathPattern=".*\\.tachibk" />
|
||||||
|
<data android:pathPattern=".*\\..*\\.tachibk" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\.tachibk" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\.tachibk" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.tachibk" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!--suppress AndroidDomInspection -->
|
<!--suppress AndroidDomInspection -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
|
@ -59,17 +116,17 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:process=":error_handler"
|
|
||||||
android:name=".crash.CrashActivity"
|
android:name=".crash.CrashActivity"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:process=":error_handler" />
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.deeplink.anime.DeepLinkAnimeActivity"
|
android:name=".ui.deeplink.anime.DeepLinkAnimeActivity"
|
||||||
android:launchMode="singleTask"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
|
||||||
android:label="@string/action_global_anime_search"
|
android:label="@string/action_global_anime_search"
|
||||||
android:exported="true">
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
|
@ -93,10 +150,10 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.deeplink.manga.DeepLinkMangaActivity"
|
android:name=".ui.deeplink.manga.DeepLinkMangaActivity"
|
||||||
android:launchMode="singleTask"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
|
||||||
android:label="@string/action_global_manga_search"
|
android:label="@string/action_global_manga_search"
|
||||||
android:exported="true">
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
|
@ -124,13 +181,14 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:launchMode="singleTask"
|
android:exported="false"
|
||||||
android:exported="false">
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
<meta-data
|
||||||
|
android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
android:resource="@xml/s_pen_actions" />
|
android:resource="@xml/s_pen_actions" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
@ -151,8 +209,8 @@
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.UnlockActivity"
|
android:name=".ui.security.UnlockActivity"
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:theme="@style/Theme.Tachiyomi" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
|
@ -161,39 +219,31 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".extension.manga.util.MangaExtensionInstallActivity"
|
android:name=".extension.manga.util.MangaExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".extension.anime.util.AnimeExtensionInstallActivity"
|
android:name=".extension.anime.util.AnimeExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.TrackLoginActivity"
|
android:name=".ui.setting.track.TrackLoginActivity"
|
||||||
android:label="@string/track_activity_name"
|
android:exported="true"
|
||||||
android:exported="true">
|
android:label="@string/track_activity_name">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="aniyomi"/>
|
||||||
|
|
||||||
|
<data android:host="myanimelist-auth" />
|
||||||
<data android:host="anilist-auth" />
|
<data android:host="anilist-auth" />
|
||||||
<data android:host="bangumi-auth" />
|
<data android:host="bangumi-auth" />
|
||||||
<data android:host="myanimelist-auth"/>
|
|
||||||
<data android:host="shikimori-auth" />
|
<data android:host="shikimori-auth" />
|
||||||
|
|
||||||
<data android:scheme="tachiyomi"/>
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:host="simkl-auth"/>
|
<data android:host="simkl-auth"/>
|
||||||
<data android:scheme="aniyomi"/>
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
@ -201,17 +251,15 @@
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".extension.util.ExtensionInstallService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".extension.manga.util.MangaExtensionInstallService"
|
android:name=".extension.manga.util.MangaExtensionInstallService"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="shortService" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".extension.anime.util.AnimeExtensionInstallService"
|
android:name=".extension.anime.util.AnimeExtensionInstallService"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="shortService" />
|
||||||
<service
|
<service
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
|
@ -239,9 +287,9 @@
|
||||||
<provider
|
<provider
|
||||||
android:name="rikka.shizuku.ShizukuProvider"
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
android:authorities="${applicationId}.shizuku"
|
android:authorities="${applicationId}.shizuku"
|
||||||
android:multiprocess="false"
|
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:multiprocess="false"
|
||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|
|
@ -8,12 +8,20 @@ import eu.kanade.domain.entries.manga.interactor.GetExcludedScanlators
|
||||||
import eu.kanade.domain.entries.manga.interactor.SetExcludedScanlators
|
import eu.kanade.domain.entries.manga.interactor.SetExcludedScanlators
|
||||||
import eu.kanade.domain.entries.manga.interactor.SetMangaViewerFlags
|
import eu.kanade.domain.entries.manga.interactor.SetMangaViewerFlags
|
||||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||||
|
import eu.kanade.domain.extension.anime.interactor.CreateAnimeExtensionRepo
|
||||||
|
import eu.kanade.domain.extension.anime.interactor.DeleteAnimeExtensionRepo
|
||||||
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionLanguages
|
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionLanguages
|
||||||
|
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionRepos
|
||||||
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionSources
|
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionSources
|
||||||
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionsByType
|
import eu.kanade.domain.extension.anime.interactor.GetAnimeExtensionsByType
|
||||||
|
import eu.kanade.domain.extension.anime.interactor.TrustAnimeExtension
|
||||||
|
import eu.kanade.domain.extension.manga.interactor.CreateMangaExtensionRepo
|
||||||
|
import eu.kanade.domain.extension.manga.interactor.DeleteMangaExtensionRepo
|
||||||
import eu.kanade.domain.extension.manga.interactor.GetExtensionSources
|
import eu.kanade.domain.extension.manga.interactor.GetExtensionSources
|
||||||
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionLanguages
|
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionLanguages
|
||||||
|
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionRepos
|
||||||
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionsByType
|
import eu.kanade.domain.extension.manga.interactor.GetMangaExtensionsByType
|
||||||
|
import eu.kanade.domain.extension.manga.interactor.TrustMangaExtension
|
||||||
import eu.kanade.domain.items.chapter.interactor.GetAvailableScanlators
|
import eu.kanade.domain.items.chapter.interactor.GetAvailableScanlators
|
||||||
import eu.kanade.domain.items.chapter.interactor.SetReadStatus
|
import eu.kanade.domain.items.chapter.interactor.SetReadStatus
|
||||||
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource
|
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource
|
||||||
|
@ -84,19 +92,19 @@ import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory
|
||||||
import tachiyomi.domain.category.manga.repository.MangaCategoryRepository
|
import tachiyomi.domain.category.manga.repository.MangaCategoryRepository
|
||||||
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
||||||
|
import tachiyomi.domain.entries.anime.interactor.GetAnimeByUrlAndSourceId
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
|
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
|
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
|
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetMangaByUrlAndSourceId
|
|
||||||
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
|
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
||||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
||||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetAnimeByUrlAndSourceId
|
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||||
|
import tachiyomi.domain.entries.manga.interactor.GetMangaByUrlAndSourceId
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
|
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
||||||
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||||
|
@ -322,5 +330,14 @@ class DomainModule : InjektModule {
|
||||||
addFactory { ToggleLanguage(get()) }
|
addFactory { ToggleLanguage(get()) }
|
||||||
addFactory { ToggleMangaSource(get()) }
|
addFactory { ToggleMangaSource(get()) }
|
||||||
addFactory { ToggleMangaSourcePin(get()) }
|
addFactory { ToggleMangaSourcePin(get()) }
|
||||||
|
addFactory { TrustAnimeExtension(get()) }
|
||||||
|
addFactory { TrustMangaExtension(get()) }
|
||||||
|
|
||||||
|
addFactory { CreateMangaExtensionRepo(get()) }
|
||||||
|
addFactory { DeleteMangaExtensionRepo(get()) }
|
||||||
|
addFactory { GetMangaExtensionRepos(get()) }
|
||||||
|
addFactory { CreateAnimeExtensionRepo(get()) }
|
||||||
|
addFactory { DeleteAnimeExtensionRepo(get()) }
|
||||||
|
addFactory { GetAnimeExtensionRepos(get()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,10 @@ class BasePreferences(
|
||||||
|
|
||||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||||
|
|
||||||
enum class ExtensionInstaller(val titleRes: StringResource) {
|
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||||
LEGACY(MR.strings.ext_installer_legacy),
|
LEGACY(MR.strings.ext_installer_legacy, true),
|
||||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),
|
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
|
||||||
SHIZUKU(MR.strings.ext_installer_shizuku),
|
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||||
PRIVATE(MR.strings.ext_installer_private),
|
PRIVATE(MR.strings.ext_installer_private, false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,9 +81,9 @@ class UpdateAnime(
|
||||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
window: Pair<Long, Long> = animeFetchInterval.getWindow(dateTime),
|
window: Pair<Long, Long> = animeFetchInterval.getWindow(dateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return animeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
|
return animeRepository.updateAnime(
|
||||||
?.let { animeRepository.updateAnime(it) }
|
animeFetchInterval.toAnimeUpdate(anime, dateTime, window),
|
||||||
?: false
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {
|
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {
|
||||||
|
|
|
@ -81,9 +81,9 @@ class UpdateManga(
|
||||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
window: Pair<Long, Long> = mangaFetchInterval.getWindow(dateTime),
|
window: Pair<Long, Long> = mangaFetchInterval.getWindow(dateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return mangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
return mangaRepository.updateManga(
|
||||||
?.let { mangaRepository.updateManga(it) }
|
mangaFetchInterval.toMangaUpdate(manga, dateTime, window),
|
||||||
?: false
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||||
|
|
|
@ -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.domain.items.chapter.service.ChapterRecognition
|
||||||
import tachiyomi.source.local.entries.manga.isLocal
|
import tachiyomi.source.local.entries.manga.isLocal
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
|
||||||
|
@ -57,6 +56,7 @@ class SyncChaptersWithSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
val now = ZonedDateTime.now()
|
val now = ZonedDateTime.now()
|
||||||
|
val nowMillis = now.toInstant().toEpochMilli()
|
||||||
|
|
||||||
val sourceChapters = rawSourceChapters
|
val sourceChapters = rawSourceChapters
|
||||||
.distinctBy { it.url }
|
.distinctBy { it.url }
|
||||||
|
@ -67,36 +67,27 @@ class SyncChaptersWithSource(
|
||||||
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chapters from db.
|
|
||||||
val dbChapters = getChaptersByMangaId.await(manga.id)
|
val dbChapters = getChaptersByMangaId.await(manga.id)
|
||||||
|
|
||||||
// Chapters from the source not in db.
|
val newChapters = mutableListOf<Chapter>()
|
||||||
val toAdd = mutableListOf<Chapter>()
|
val updatedChapters = mutableListOf<Chapter>()
|
||||||
|
val removedChapters = dbChapters.filterNot { dbChapter ->
|
||||||
// Chapters whose metadata have changed.
|
|
||||||
val toChange = mutableListOf<Chapter>()
|
|
||||||
|
|
||||||
// Chapters from the db not in source.
|
|
||||||
val toDelete = dbChapters.filterNot { dbChapter ->
|
|
||||||
sourceChapters.any { sourceChapter ->
|
sourceChapters.any { sourceChapter ->
|
||||||
dbChapter.url == sourceChapter.url
|
dbChapter.url == sourceChapter.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val rightNow = Instant.now().toEpochMilli()
|
|
||||||
|
|
||||||
// Used to not set upload date of older chapters
|
// Used to not set upload date of older chapters
|
||||||
// to a higher value than newer chapters
|
// to a higher value than newer chapters
|
||||||
var maxSeenUploadDate = 0L
|
var maxSeenUploadDate = 0L
|
||||||
|
|
||||||
val sManga = manga.toSManga()
|
|
||||||
for (sourceChapter in sourceChapters) {
|
for (sourceChapter in sourceChapters) {
|
||||||
var chapter = sourceChapter
|
var chapter = sourceChapter
|
||||||
|
|
||||||
// Update metadata from source if necessary.
|
// Update metadata from source if necessary.
|
||||||
if (source is HttpSource) {
|
if (source is HttpSource) {
|
||||||
val sChapter = chapter.toSChapter()
|
val sChapter = chapter.toSChapter()
|
||||||
source.prepareNewChapter(sChapter, sManga)
|
source.prepareNewChapter(sChapter, manga.toSManga())
|
||||||
chapter = chapter.copyFromSChapter(sChapter)
|
chapter = chapter.copyFromSChapter(sChapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,13 +103,13 @@ class SyncChaptersWithSource(
|
||||||
|
|
||||||
if (dbChapter == null) {
|
if (dbChapter == null) {
|
||||||
val toAddChapter = if (chapter.dateUpload == 0L) {
|
val toAddChapter = if (chapter.dateUpload == 0L) {
|
||||||
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
|
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
|
||||||
chapter.copy(dateUpload = altDateUpload)
|
chapter.copy(dateUpload = altDateUpload)
|
||||||
} else {
|
} else {
|
||||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
||||||
chapter
|
chapter
|
||||||
}
|
}
|
||||||
toAdd.add(toAddChapter)
|
newChapters.add(toAddChapter)
|
||||||
} else {
|
} else {
|
||||||
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
|
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
|
||||||
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(
|
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(
|
||||||
|
@ -144,13 +135,13 @@ class SyncChaptersWithSource(
|
||||||
if (chapter.dateUpload != 0L) {
|
if (chapter.dateUpload != 0L) {
|
||||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||||
}
|
}
|
||||||
toChange.add(toChangeChapter)
|
updatedChapters.add(toChangeChapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
|
||||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||||
updateManga.awaitUpdateFetchInterval(
|
updateManga.awaitUpdateFetchInterval(
|
||||||
manga,
|
manga,
|
||||||
|
@ -167,20 +158,20 @@ class SyncChaptersWithSource(
|
||||||
val deletedReadChapterNumbers = TreeSet<Double>()
|
val deletedReadChapterNumbers = TreeSet<Double>()
|
||||||
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
||||||
|
|
||||||
toDelete.forEach { chapter ->
|
removedChapters.forEach { chapter ->
|
||||||
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
||||||
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
||||||
deletedChapterNumbers.add(chapter.chapterNumber)
|
deletedChapterNumbers.add(chapter.chapterNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
|
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
|
||||||
.associate { it.chapterNumber to it.dateFetch }
|
.associate { it.chapterNumber to it.dateFetch }
|
||||||
|
|
||||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||||
// Sources MUST return the chapters from most to less recent, which is common.
|
// Sources MUST return the chapters from most to less recent, which is common.
|
||||||
var itemCount = toAdd.size
|
var itemCount = newChapters.size
|
||||||
var updatedToAdd = toAdd.map { toAddItem ->
|
var updatedToAdd = newChapters.map { toAddItem ->
|
||||||
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||||
|
|
||||||
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
||||||
|
|
||||||
|
@ -199,8 +190,8 @@ class SyncChaptersWithSource(
|
||||||
chapter
|
chapter
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toDelete.isNotEmpty()) {
|
if (removedChapters.isNotEmpty()) {
|
||||||
val toDeleteIds = toDelete.map { it.id }
|
val toDeleteIds = removedChapters.map { it.id }
|
||||||
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,8 +199,8 @@ class SyncChaptersWithSource(
|
||||||
updatedToAdd = chapterRepository.addAllChapters(updatedToAdd)
|
updatedToAdd = chapterRepository.addAllChapters(updatedToAdd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toChange.isNotEmpty()) {
|
if (updatedChapters.isNotEmpty()) {
|
||||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
}
|
}
|
||||||
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||||
|
|
|
@ -21,7 +21,6 @@ import tachiyomi.domain.items.episode.repository.EpisodeRepository
|
||||||
import tachiyomi.domain.items.episode.service.EpisodeRecognition
|
import tachiyomi.domain.items.episode.service.EpisodeRecognition
|
||||||
import tachiyomi.source.local.entries.anime.isLocal
|
import tachiyomi.source.local.entries.anime.isLocal
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
|
||||||
|
@ -55,6 +54,7 @@ class SyncEpisodesWithSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
val now = ZonedDateTime.now()
|
val now = ZonedDateTime.now()
|
||||||
|
val nowMillis = now.toInstant().toEpochMilli()
|
||||||
|
|
||||||
val sourceEpisodes = rawSourceEpisodes
|
val sourceEpisodes = rawSourceEpisodes
|
||||||
.distinctBy { it.url }
|
.distinctBy { it.url }
|
||||||
|
@ -65,36 +65,27 @@ class SyncEpisodesWithSource(
|
||||||
.copy(animeId = anime.id, sourceOrder = i.toLong())
|
.copy(animeId = anime.id, sourceOrder = i.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Episodes from db.
|
|
||||||
val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
|
val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
|
||||||
|
|
||||||
// Episodes from the source not in db.
|
val newEpisodes = mutableListOf<Episode>()
|
||||||
val toAdd = mutableListOf<Episode>()
|
val updatedEpisodes = mutableListOf<Episode>()
|
||||||
|
val removedEpisodes = dbEpisodes.filterNot { dbEpisode ->
|
||||||
// Episodes whose metadata have changed.
|
|
||||||
val toChange = mutableListOf<Episode>()
|
|
||||||
|
|
||||||
// Episodes from the db not in source.
|
|
||||||
val toDelete = dbEpisodes.filterNot { dbEpisode ->
|
|
||||||
sourceEpisodes.any { sourceEpisode ->
|
sourceEpisodes.any { sourceEpisode ->
|
||||||
dbEpisode.url == sourceEpisode.url
|
dbEpisode.url == sourceEpisode.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val rightNow = Instant.now().toEpochMilli()
|
|
||||||
|
|
||||||
// Used to not set upload date of older episodes
|
// Used to not set upload date of older episodes
|
||||||
// to a higher value than newer episodes
|
// to a higher value than newer episodes
|
||||||
var maxSeenUploadDate = 0L
|
var maxSeenUploadDate = 0L
|
||||||
|
|
||||||
val sAnime = anime.toSAnime()
|
|
||||||
for (sourceEpisode in sourceEpisodes) {
|
for (sourceEpisode in sourceEpisodes) {
|
||||||
var episode = sourceEpisode
|
var episode = sourceEpisode
|
||||||
|
|
||||||
// Update metadata from source if necessary.
|
// Update metadata from source if necessary.
|
||||||
if (source is AnimeHttpSource) {
|
if (source is AnimeHttpSource) {
|
||||||
val sEpisode = episode.toSEpisode()
|
val sEpisode = episode.toSEpisode()
|
||||||
source.prepareNewEpisode(sEpisode, sAnime)
|
source.prepareNewEpisode(sEpisode, anime.toSAnime())
|
||||||
episode = episode.copyFromSEpisode(sEpisode)
|
episode = episode.copyFromSEpisode(sEpisode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,13 +101,13 @@ class SyncEpisodesWithSource(
|
||||||
|
|
||||||
if (dbEpisode == null) {
|
if (dbEpisode == null) {
|
||||||
val toAddEpisode = if (episode.dateUpload == 0L) {
|
val toAddEpisode = if (episode.dateUpload == 0L) {
|
||||||
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
|
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
|
||||||
episode.copy(dateUpload = altDateUpload)
|
episode.copy(dateUpload = altDateUpload)
|
||||||
} else {
|
} else {
|
||||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceEpisode.dateUpload)
|
maxSeenUploadDate = max(maxSeenUploadDate, sourceEpisode.dateUpload)
|
||||||
episode
|
episode
|
||||||
}
|
}
|
||||||
toAdd.add(toAddEpisode)
|
newEpisodes.add(toAddEpisode)
|
||||||
} else {
|
} else {
|
||||||
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
|
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
|
||||||
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(
|
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(
|
||||||
|
@ -144,13 +135,13 @@ class SyncEpisodesWithSource(
|
||||||
dateUpload = sourceEpisode.dateUpload,
|
dateUpload = sourceEpisode.dateUpload,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
toChange.add(toChangeEpisode)
|
updatedEpisodes.add(toChangeEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (newEpisodes.isEmpty() && removedEpisodes.isEmpty() && updatedEpisodes.isEmpty()) {
|
||||||
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
|
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
|
||||||
updateAnime.awaitUpdateFetchInterval(
|
updateAnime.awaitUpdateFetchInterval(
|
||||||
anime,
|
anime,
|
||||||
|
@ -167,20 +158,20 @@ class SyncEpisodesWithSource(
|
||||||
val deletedSeenEpisodeNumbers = TreeSet<Double>()
|
val deletedSeenEpisodeNumbers = TreeSet<Double>()
|
||||||
val deletedBookmarkedEpisodeNumbers = TreeSet<Double>()
|
val deletedBookmarkedEpisodeNumbers = TreeSet<Double>()
|
||||||
|
|
||||||
toDelete.forEach { episode ->
|
removedEpisodes.forEach { episode ->
|
||||||
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
|
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
|
||||||
if (episode.bookmark) deletedBookmarkedEpisodeNumbers.add(episode.episodeNumber)
|
if (episode.bookmark) deletedBookmarkedEpisodeNumbers.add(episode.episodeNumber)
|
||||||
deletedEpisodeNumbers.add(episode.episodeNumber)
|
deletedEpisodeNumbers.add(episode.episodeNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
val deletedEpisodeNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
|
val deletedEpisodeNumberDateFetchMap = removedEpisodes.sortedByDescending { it.dateFetch }
|
||||||
.associate { it.episodeNumber to it.dateFetch }
|
.associate { it.episodeNumber to it.dateFetch }
|
||||||
|
|
||||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||||
// Sources MUST return the episodes from most to less recent, which is common.
|
// Sources MUST return the episodes from most to less recent, which is common.
|
||||||
var itemCount = toAdd.size
|
var itemCount = newEpisodes.size
|
||||||
var updatedToAdd = toAdd.map { toAddItem ->
|
var updatedToAdd = newEpisodes.map { toAddItem ->
|
||||||
var episode = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
var episode = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||||
|
|
||||||
if (episode.isRecognizedNumber.not() || episode.episodeNumber !in deletedEpisodeNumbers) return@map episode
|
if (episode.isRecognizedNumber.not() || episode.episodeNumber !in deletedEpisodeNumbers) return@map episode
|
||||||
|
|
||||||
|
@ -199,8 +190,8 @@ class SyncEpisodesWithSource(
|
||||||
episode
|
episode
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toDelete.isNotEmpty()) {
|
if (removedEpisodes.isNotEmpty()) {
|
||||||
val toDeleteIds = toDelete.map { it.id }
|
val toDeleteIds = removedEpisodes.map { it.id }
|
||||||
episodeRepository.removeEpisodesWithIds(toDeleteIds)
|
episodeRepository.removeEpisodesWithIds(toDeleteIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,8 +199,8 @@ class SyncEpisodesWithSource(
|
||||||
updatedToAdd = episodeRepository.addAllEpisodes(updatedToAdd)
|
updatedToAdd = episodeRepository.addAllEpisodes(updatedToAdd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toChange.isNotEmpty()) {
|
if (updatedEpisodes.isNotEmpty()) {
|
||||||
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
|
val episodeUpdates = updatedEpisodes.map { it.toEpisodeUpdate() }
|
||||||
updateEpisode.awaitAll(episodeUpdates)
|
updateEpisode.awaitAll(episodeUpdates)
|
||||||
}
|
}
|
||||||
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)
|
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)
|
||||||
|
|
|
@ -37,7 +37,14 @@ class SourcePreferences(
|
||||||
SetMigrateSorting.Direction.ASCENDING,
|
SetMigrateSorting.Direction.ASCENDING,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
|
fun animeExtensionRepos() = preferenceStore.getStringSet("anime_extension_repos", emptySet())
|
||||||
|
|
||||||
|
fun mangaExtensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||||
|
|
||||||
|
fun trustedExtensions() = preferenceStore.getStringSet(
|
||||||
|
Preference.appStateKey("trusted_extensions"),
|
||||||
|
emptySet(),
|
||||||
|
)
|
||||||
|
|
||||||
// Mixture Sources
|
// Mixture Sources
|
||||||
|
|
||||||
|
|
|
@ -25,14 +25,14 @@ class RefreshAnimeTracks(
|
||||||
suspend fun await(animeId: Long): List<Pair<Tracker?, Throwable>> {
|
suspend fun await(animeId: Long): List<Pair<Tracker?, Throwable>> {
|
||||||
return supervisorScope {
|
return supervisorScope {
|
||||||
return@supervisorScope getTracks.await(animeId)
|
return@supervisorScope getTracks.await(animeId)
|
||||||
.map { it to trackerManager.get(it.syncId) }
|
.map { it to trackerManager.get(it.trackerId) }
|
||||||
.filter { (_, service) -> service?.isLoggedIn == true }
|
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||||
.map { (track, service) ->
|
.map { (track, service) ->
|
||||||
async {
|
async {
|
||||||
return@async try {
|
return@async try {
|
||||||
val updatedTrack = service!!.animeService.refresh(track.toDbTrack())
|
val updatedTrack = service!!.animeService.refresh(track.toDbTrack()).toDomainTrack()!!
|
||||||
insertTrack.await(updatedTrack.toDomainTrack()!!)
|
insertTrack.await(updatedTrack)
|
||||||
syncEpisodeProgressWithTrack.await(animeId, track, service.animeService)
|
syncEpisodeProgressWithTrack.await(animeId, updatedTrack, service.animeService)
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
service to e
|
service to e
|
||||||
|
|
|
@ -28,7 +28,7 @@ class TrackEpisode(
|
||||||
if (tracks.isEmpty()) return@withNonCancellableContext
|
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||||
|
|
||||||
tracks.mapNotNull { track ->
|
tracks.mapNotNull { track ->
|
||||||
val service = trackerManager.get(track.syncId)
|
val service = trackerManager.get(track.trackerId)
|
||||||
if (service == null || !service.isLoggedIn || episodeNumber <= track.lastEpisodeSeen) {
|
if (service == null || !service.isLoggedIn || episodeNumber <= track.lastEpisodeSeen) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,10 @@ fun AnimeTrack.copyPersonalFrom(other: AnimeTrack): AnimeTrack {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun AnimeTrack.toDbTrack(): DbAnimeTrack = DbAnimeTrack.create(syncId).also {
|
fun AnimeTrack.toDbTrack(): DbAnimeTrack = DbAnimeTrack.create(trackerId).also {
|
||||||
it.id = id
|
it.id = id
|
||||||
it.anime_id = animeId
|
it.anime_id = animeId
|
||||||
it.media_id = remoteId
|
it.remote_id = remoteId
|
||||||
it.library_id = libraryId
|
it.library_id = libraryId
|
||||||
it.title = title
|
it.title = title
|
||||||
it.last_episode_seen = lastEpisodeSeen.toFloat()
|
it.last_episode_seen = lastEpisodeSeen.toFloat()
|
||||||
|
@ -33,14 +33,16 @@ fun DbAnimeTrack.toDomainTrack(idRequired: Boolean = true): AnimeTrack? {
|
||||||
return AnimeTrack(
|
return AnimeTrack(
|
||||||
id = trackId,
|
id = trackId,
|
||||||
animeId = anime_id,
|
animeId = anime_id,
|
||||||
syncId = sync_id.toLong(),
|
trackerId = tracker_id.toLong(),
|
||||||
remoteId = media_id,
|
remoteId = remote_id,
|
||||||
libraryId = library_id,
|
libraryId = library_id,
|
||||||
title = title,
|
title = title,
|
||||||
lastEpisodeSeen = last_episode_seen.toDouble(),
|
lastEpisodeSeen = last_episode_seen.toDouble(),
|
||||||
totalEpisodes = total_episodes.toLong(),
|
totalEpisodes = total_episodes.toLong(),
|
||||||
status = status.toLong(),
|
status = status.toLong(),
|
||||||
score = score.toDouble(),
|
// Jank workaround due to precision issues while converting
|
||||||
|
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
|
||||||
|
score = score.toString().toDouble(),
|
||||||
remoteUrl = tracking_url,
|
remoteUrl = tracking_url,
|
||||||
startDate = started_watching_date,
|
startDate = started_watching_date,
|
||||||
finishDate = finished_watching_date,
|
finishDate = finished_watching_date,
|
||||||
|
|
|
@ -25,14 +25,14 @@ class RefreshMangaTracks(
|
||||||
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
|
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
|
||||||
return supervisorScope {
|
return supervisorScope {
|
||||||
return@supervisorScope getTracks.await(mangaId)
|
return@supervisorScope getTracks.await(mangaId)
|
||||||
.map { it to trackerManager.get(it.syncId) }
|
.map { it to trackerManager.get(it.trackerId) }
|
||||||
.filter { (_, service) -> service?.isLoggedIn == true }
|
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||||
.map { (track, service) ->
|
.map { (track, service) ->
|
||||||
async {
|
async {
|
||||||
return@async try {
|
return@async try {
|
||||||
val updatedTrack = service!!.mangaService.refresh(track.toDbTrack())
|
val updatedTrack = service!!.mangaService.refresh(track.toDbTrack()).toDomainTrack()!!
|
||||||
insertTrack.await(updatedTrack.toDomainTrack()!!)
|
insertTrack.await(updatedTrack)
|
||||||
syncChapterProgressWithTrack.await(mangaId, track, service.mangaService)
|
syncChapterProgressWithTrack.await(mangaId, updatedTrack, service.mangaService)
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
service to e
|
service to e
|
||||||
|
|
|
@ -27,7 +27,7 @@ class TrackChapter(
|
||||||
if (tracks.isEmpty()) return@withNonCancellableContext
|
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||||
|
|
||||||
tracks.mapNotNull { track ->
|
tracks.mapNotNull { track ->
|
||||||
val service = trackerManager.get(track.syncId)
|
val service = trackerManager.get(track.trackerId)
|
||||||
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
||||||
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
|
|
|
@ -13,10 +13,10 @@ fun MangaTrack.copyPersonalFrom(other: MangaTrack): MangaTrack {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MangaTrack.toDbTrack(): DbMangaTrack = DbMangaTrack.create(syncId).also {
|
fun MangaTrack.toDbTrack(): DbMangaTrack = DbMangaTrack.create(trackerId).also {
|
||||||
it.id = id
|
it.id = id
|
||||||
it.manga_id = mangaId
|
it.manga_id = mangaId
|
||||||
it.media_id = remoteId
|
it.remote_id = remoteId
|
||||||
it.library_id = libraryId
|
it.library_id = libraryId
|
||||||
it.title = title
|
it.title = title
|
||||||
it.last_chapter_read = lastChapterRead.toFloat()
|
it.last_chapter_read = lastChapterRead.toFloat()
|
||||||
|
@ -33,14 +33,16 @@ fun DbMangaTrack.toDomainTrack(idRequired: Boolean = true): MangaTrack? {
|
||||||
return MangaTrack(
|
return MangaTrack(
|
||||||
id = trackId,
|
id = trackId,
|
||||||
mangaId = manga_id,
|
mangaId = manga_id,
|
||||||
syncId = sync_id.toLong(),
|
trackerId = tracker_id.toLong(),
|
||||||
remoteId = media_id,
|
remoteId = remote_id,
|
||||||
libraryId = library_id,
|
libraryId = library_id,
|
||||||
title = title,
|
title = title,
|
||||||
lastChapterRead = last_chapter_read.toDouble(),
|
lastChapterRead = last_chapter_read.toDouble(),
|
||||||
totalChapters = total_chapters.toLong(),
|
totalChapters = total_chapters.toLong(),
|
||||||
status = status.toLong(),
|
status = status.toLong(),
|
||||||
score = score.toDouble(),
|
// Jank workaround due to precision issues while converting
|
||||||
|
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
|
||||||
|
score = score.toString().toDouble(),
|
||||||
remoteUrl = tracking_url,
|
remoteUrl = tracking_url,
|
||||||
startDate = started_reading_date,
|
startDate = started_reading_date,
|
||||||
finishDate = finished_reading_date,
|
finishDate = finished_reading_date,
|
||||||
|
|
|
@ -2,22 +2,35 @@ package eu.kanade.domain.track.service
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
|
||||||
class TrackPreferences(
|
class TrackPreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
|
fun trackUsername(tracker: Tracker) = preferenceStore.getString(
|
||||||
|
Preference.privateKey("pref_mangasync_username_${tracker.id}"),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
|
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
|
||||||
|
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
fun setCredentials(sync: Tracker, username: String, password: String) {
|
fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean(
|
||||||
trackUsername(sync).set(username)
|
Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"),
|
||||||
trackPassword(sync).set(password)
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun setCredentials(tracker: Tracker, username: String, password: String) {
|
||||||
|
trackUsername(tracker).set(username)
|
||||||
|
trackPassword(tracker).set(password)
|
||||||
|
trackAuthExpired(tracker).set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
|
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
|
||||||
|
|
||||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||||
|
|
||||||
|
@ -29,12 +42,4 @@ class TrackPreferences(
|
||||||
"show_next_episode_airing_time",
|
"show_next_episode_airing_time",
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
|
|
||||||
|
|
||||||
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
|
||||||
|
|
||||||
private fun trackToken(syncId: Long) = "track_token_$syncId"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package eu.kanade.domain.ui
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import eu.kanade.domain.ui.model.AppTheme
|
import eu.kanade.domain.ui.model.AppTheme
|
||||||
|
import eu.kanade.domain.ui.model.NavStyle
|
||||||
|
import eu.kanade.domain.ui.model.StartScreen
|
||||||
import eu.kanade.domain.ui.model.TabletUiMode
|
import eu.kanade.domain.ui.model.TabletUiMode
|
||||||
import eu.kanade.domain.ui.model.ThemeMode
|
import eu.kanade.domain.ui.model.ThemeMode
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
@ -34,6 +36,10 @@ class UiPreferences(
|
||||||
|
|
||||||
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||||
|
|
||||||
|
fun startScreen() = preferenceStore.getEnum("start_screen", StartScreen.ANIME)
|
||||||
|
|
||||||
|
fun navStyle() = preferenceStore.getEnum("bottom_rail_nav_style", NavStyle.MOVE_HISTORY_TO_MORE)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun dateFormat(format: String): DateFormat = when (format) {
|
fun dateFormat(format: String): DateFormat = when (format) {
|
||||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||||
|
|
|
@ -15,6 +15,7 @@ enum class AppTheme(val titleRes: StringResource?) {
|
||||||
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
||||||
MOCHA(MR.strings.theme_mocha),
|
MOCHA(MR.strings.theme_mocha),
|
||||||
SAPPHIRE(MR.strings.theme_sapphire),
|
SAPPHIRE(MR.strings.theme_sapphire),
|
||||||
|
NORD(MR.strings.theme_nord),
|
||||||
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
||||||
TAKO(MR.strings.theme_tako),
|
TAKO(MR.strings.theme_tako),
|
||||||
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
||||||
|
|
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
|
modifier = Modifier
|
||||||
.padding(
|
.padding(
|
||||||
start = MaterialTheme.padding.medium,
|
start = MaterialTheme.padding.medium,
|
||||||
end = MaterialTheme.padding.tiny,
|
end = MaterialTheme.padding.extraSmall,
|
||||||
)
|
)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
|
|
|
@ -16,8 +16,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||||
import androidx.compose.material.icons.outlined.History
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
@ -36,6 +35,7 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
@ -52,6 +52,7 @@ import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeExtensionDetailsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeExtensionDetailsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
|
@ -65,14 +66,23 @@ fun AnimeExtensionDetailsScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
state: AnimeExtensionDetailsScreenModel.State,
|
state: AnimeExtensionDetailsScreenModel.State,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickWhatsNew: () -> Unit,
|
|
||||||
onClickReadme: () -> Unit,
|
|
||||||
onClickEnableAll: () -> Unit,
|
onClickEnableAll: () -> Unit,
|
||||||
onClickDisableAll: () -> Unit,
|
onClickDisableAll: () -> Unit,
|
||||||
onClickClearCookies: () -> Unit,
|
onClickClearCookies: () -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
onClickSource: (sourceId: Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
val url = remember(state.extension) {
|
||||||
|
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
||||||
|
regex.find(state.extension?.repoUrl.orEmpty())
|
||||||
|
?.let {
|
||||||
|
val (user, repo) = it.destructured
|
||||||
|
"https://github.com/$user/$repo"
|
||||||
|
}
|
||||||
|
?: state.extension?.repoUrl
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
|
@ -82,19 +92,14 @@ fun AnimeExtensionDetailsScreen(
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||||
.apply {
|
.apply {
|
||||||
if (state.extension?.isUnofficial == false) {
|
if (url != null) {
|
||||||
add(
|
add(
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.whats_new),
|
title = stringResource(MR.strings.action_open_repo),
|
||||||
icon = Icons.Outlined.History,
|
icon = Icons.AutoMirrored.Outlined.Launch,
|
||||||
onClick = onClickWhatsNew,
|
onClick = {
|
||||||
),
|
uriHandler.openUri(url)
|
||||||
)
|
},
|
||||||
add(
|
|
||||||
AppBar.Action(
|
|
||||||
title = stringResource(MR.strings.action_faq_and_guides),
|
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
|
||||||
onClick = onClickReadme,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -124,7 +129,7 @@ fun AnimeExtensionDetailsScreen(
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
if (state.extension == null) {
|
if (state.extension == null) {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
stringRes = MR.strings.empty_screen,
|
MR.strings.empty_screen,
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
|
@ -145,7 +150,7 @@ fun AnimeExtensionDetailsScreen(
|
||||||
private fun AnimeExtensionDetails(
|
private fun AnimeExtensionDetails(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
extension: AnimeExtension.Installed,
|
extension: AnimeExtension.Installed,
|
||||||
sources: List<AnimeExtensionSourceItem>,
|
sources: ImmutableList<AnimeExtensionSourceItem>,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
onClickSource: (sourceId: Long) -> Unit,
|
||||||
|
@ -156,12 +161,7 @@ private fun AnimeExtensionDetails(
|
||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
when {
|
if (extension.isObsolete) {
|
||||||
extension.isUnofficial ->
|
|
||||||
item {
|
|
||||||
WarningBanner(MR.strings.unofficial_anime_extension_message)
|
|
||||||
}
|
|
||||||
extension.isObsolete ->
|
|
||||||
item {
|
item {
|
||||||
WarningBanner(MR.strings.obsolete_extension_message)
|
WarningBanner(MR.strings.obsolete_extension_message)
|
||||||
}
|
}
|
||||||
|
@ -296,7 +296,7 @@ private fun DetailsHeader(
|
||||||
top = MaterialTheme.padding.small,
|
top = MaterialTheme.padding.small,
|
||||||
bottom = MaterialTheme.padding.medium,
|
bottom = MaterialTheme.padding.medium,
|
||||||
),
|
),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.presentation.browse.anime
|
package eu.kanade.presentation.browse.anime
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
@ -37,16 +38,20 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.core.util.fastDistinctBy
|
||||||
import eu.kanade.presentation.browse.BaseBrowseItem
|
import eu.kanade.presentation.browse.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.anime.components.AnimeExtensionIcon
|
import eu.kanade.presentation.browse.anime.components.AnimeExtensionIcon
|
||||||
import eu.kanade.presentation.browse.manga.ExtensionHeader
|
import eu.kanade.presentation.browse.manga.ExtensionHeader
|
||||||
import eu.kanade.presentation.browse.manga.ExtensionTrustDialog
|
import eu.kanade.presentation.browse.manga.ExtensionTrustDialog
|
||||||
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
|
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
|
||||||
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionUiModel
|
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionUiModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
|
@ -65,7 +70,7 @@ fun AnimeExtensionScreen(
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onLongClickItem: (AnimeExtension) -> Unit,
|
onLongClickItem: (AnimeExtension) -> Unit,
|
||||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||||
onClickItemWebView: (AnimeExtension.Available) -> Unit,
|
onOpenWebView: (AnimeExtension.Available) -> Unit,
|
||||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||||
onUninstallExtension: (AnimeExtension) -> Unit,
|
onUninstallExtension: (AnimeExtension) -> Unit,
|
||||||
onUpdateExtension: (AnimeExtension.Installed) -> Unit,
|
onUpdateExtension: (AnimeExtension.Installed) -> Unit,
|
||||||
|
@ -98,7 +103,7 @@ fun AnimeExtensionScreen(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemWebView = onClickItemWebView,
|
onOpenWebView = onOpenWebView,
|
||||||
onInstallExtension = onInstallExtension,
|
onInstallExtension = onInstallExtension,
|
||||||
onUninstallExtension = onUninstallExtension,
|
onUninstallExtension = onUninstallExtension,
|
||||||
onUpdateExtension = onUpdateExtension,
|
onUpdateExtension = onUpdateExtension,
|
||||||
|
@ -116,7 +121,7 @@ private fun AnimeExtensionContent(
|
||||||
state: AnimeExtensionsScreenModel.State,
|
state: AnimeExtensionsScreenModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onLongClickItem: (AnimeExtension) -> Unit,
|
onLongClickItem: (AnimeExtension) -> Unit,
|
||||||
onClickItemWebView: (AnimeExtension.Available) -> Unit,
|
onOpenWebView: (AnimeExtension.Available) -> Unit,
|
||||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||||
onUninstallExtension: (AnimeExtension) -> Unit,
|
onUninstallExtension: (AnimeExtension) -> Unit,
|
||||||
|
@ -125,11 +130,24 @@ private fun AnimeExtensionContent(
|
||||||
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
var trustState by remember { mutableStateOf<AnimeExtension.Untrusted?>(null) }
|
var trustState by remember { mutableStateOf<AnimeExtension.Untrusted?>(null) }
|
||||||
|
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
contentPadding = contentPadding + topSmallPaddingValues,
|
||||||
) {
|
) {
|
||||||
|
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
||||||
|
item(key = "extension-permissions-warning") {
|
||||||
|
WarningBanner(
|
||||||
|
textRes = MR.strings.ext_permission_install_apps_warning,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
context.launchRequestPackageInstallsPermission()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.items.forEach { (header, items) ->
|
state.items.forEach { (header, items) ->
|
||||||
item(
|
item(
|
||||||
contentType = "header",
|
contentType = "header",
|
||||||
|
@ -168,7 +186,7 @@ private fun AnimeExtensionContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = items,
|
items = items.fastDistinctBy { it.hashCode() },
|
||||||
contentType = { "item" },
|
contentType = { "item" },
|
||||||
key = { "extension-${it.hashCode()}" },
|
key = { "extension-${it.hashCode()}" },
|
||||||
) { item ->
|
) { item ->
|
||||||
|
@ -183,8 +201,14 @@ private fun AnimeExtensionContent(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
|
onClickItemSecondaryAction = {
|
||||||
|
when (it) {
|
||||||
|
is AnimeExtension.Available -> onOpenWebView(it)
|
||||||
|
is AnimeExtension.Installed -> onOpenExtension(it)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
},
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemWebView = onClickItemWebView,
|
|
||||||
onClickItemAction = {
|
onClickItemAction = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is AnimeExtension.Available -> onInstallExtension(it)
|
is AnimeExtension.Available -> onInstallExtension(it)
|
||||||
|
@ -227,10 +251,10 @@ private fun AnimeExtensionItem(
|
||||||
item: AnimeExtensionUiModel.Item,
|
item: AnimeExtensionUiModel.Item,
|
||||||
onClickItem: (AnimeExtension) -> Unit,
|
onClickItem: (AnimeExtension) -> Unit,
|
||||||
onLongClickItem: (AnimeExtension) -> Unit,
|
onLongClickItem: (AnimeExtension) -> Unit,
|
||||||
onClickItemWebView: (AnimeExtension.Available) -> Unit,
|
|
||||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||||
onClickItemAction: (AnimeExtension) -> Unit,
|
onClickItemAction: (AnimeExtension) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
onClickItemSecondaryAction: (AnimeExtension) -> Unit,
|
||||||
) {
|
) {
|
||||||
val (extension, installStep) = item
|
val (extension, installStep) = item
|
||||||
BaseBrowseItem(
|
BaseBrowseItem(
|
||||||
|
@ -271,9 +295,9 @@ private fun AnimeExtensionItem(
|
||||||
AnimeExtensionItemActions(
|
AnimeExtensionItemActions(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
installStep = installStep,
|
installStep = installStep,
|
||||||
onClickItemWebView = onClickItemWebView,
|
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemAction = onClickItemAction,
|
onClickItemAction = onClickItemAction,
|
||||||
|
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
@ -303,7 +327,7 @@ private fun AnimeExtensionItemContent(
|
||||||
// Won't look good but it's not like we can ellipsize overflowing content
|
// Won't look good but it's not like we can ellipsize overflowing content
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||||
if (extension is AnimeExtension.Installed && extension.lang.isNotEmpty()) {
|
if (extension is AnimeExtension.Installed && extension.lang.isNotEmpty()) {
|
||||||
|
@ -323,7 +347,6 @@ private fun AnimeExtensionItemContent(
|
||||||
|
|
||||||
val warning = when {
|
val warning = when {
|
||||||
extension is AnimeExtension.Untrusted -> MR.strings.ext_untrusted
|
extension is AnimeExtension.Untrusted -> MR.strings.ext_untrusted
|
||||||
extension is AnimeExtension.Installed && extension.isUnofficial -> MR.strings.ext_unofficial
|
|
||||||
extension is AnimeExtension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
extension is AnimeExtension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
||||||
extension.isNsfw -> MR.strings.ext_nsfw_short
|
extension.isNsfw -> MR.strings.ext_nsfw_short
|
||||||
else -> null
|
else -> null
|
||||||
|
@ -358,15 +381,15 @@ private fun AnimeExtensionItemActions(
|
||||||
extension: AnimeExtension,
|
extension: AnimeExtension,
|
||||||
installStep: InstallStep,
|
installStep: InstallStep,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClickItemWebView: (AnimeExtension.Available) -> Unit = {},
|
|
||||||
onClickItemCancel: (AnimeExtension) -> Unit = {},
|
onClickItemCancel: (AnimeExtension) -> Unit = {},
|
||||||
onClickItemAction: (AnimeExtension) -> Unit = {},
|
onClickItemAction: (AnimeExtension) -> Unit = {},
|
||||||
|
onClickItemSecondaryAction: (AnimeExtension) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val isIdle = installStep.isCompleted()
|
val isIdle = installStep.isCompleted()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
!isIdle -> {
|
!isIdle -> {
|
||||||
|
@ -388,6 +411,13 @@ private fun AnimeExtensionItemActions(
|
||||||
installStep == InstallStep.Idle -> {
|
installStep == InstallStep.Idle -> {
|
||||||
when (extension) {
|
when (extension) {
|
||||||
is AnimeExtension.Installed -> {
|
is AnimeExtension.Installed -> {
|
||||||
|
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Settings,
|
||||||
|
contentDescription = stringResource(MR.strings.action_settings),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (extension.hasUpdate) {
|
if (extension.hasUpdate) {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -396,13 +426,6 @@ private fun AnimeExtensionItemActions(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Settings,
|
|
||||||
contentDescription = stringResource(MR.strings.action_settings),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is AnimeExtension.Untrusted -> {
|
is AnimeExtension.Untrusted -> {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
|
@ -415,7 +438,7 @@ private fun AnimeExtensionItemActions(
|
||||||
is AnimeExtension.Available -> {
|
is AnimeExtension.Available -> {
|
||||||
if (extension.sources.isNotEmpty()) {
|
if (extension.sources.isNotEmpty()) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { onClickItemWebView(extension) },
|
onClick = { onClickItemSecondaryAction(extension) },
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Public,
|
imageVector = Icons.Outlined.Public,
|
||||||
|
|
|
@ -74,9 +74,10 @@ internal fun GlobalSearchContent(
|
||||||
items.forEach { (source, result) ->
|
items.forEach { (source, result) ->
|
||||||
item(key = source.id) {
|
item(key = source.id) {
|
||||||
GlobalSearchResultItem(
|
GlobalSearchResultItem(
|
||||||
title = fromSourceId
|
title = fromSourceId?.let {
|
||||||
?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
"▶ ${source.name}".takeIf { source.id == fromSourceId }
|
||||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
} ?: source.name,
|
||||||
|
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
|
||||||
onClick = { onClickSource(source) },
|
onClick = { onClickSource(source) },
|
||||||
) {
|
) {
|
||||||
when (result) {
|
when (result) {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import eu.kanade.presentation.browse.anime.components.AnimeSourceIcon
|
||||||
import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem
|
import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem
|
||||||
import eu.kanade.tachiyomi.ui.browse.anime.migration.sources.MigrateAnimeSourceScreenModel
|
import eu.kanade.tachiyomi.ui.browse.anime.migration.sources.MigrateAnimeSourceScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import tachiyomi.domain.source.anime.model.AnimeSource
|
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.Badge
|
import tachiyomi.presentation.core.components.Badge
|
||||||
|
@ -75,7 +76,7 @@ fun MigrateAnimeSourceScreen(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MigrateAnimeSourceList(
|
private fun MigrateAnimeSourceList(
|
||||||
list: List<Pair<AnimeSource, Long>>,
|
list: ImmutableList<Pair<AnimeSource, Long>>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (AnimeSource) -> Unit,
|
onClickItem: (AnimeSource) -> Unit,
|
||||||
onLongClickItem: (AnimeSource) -> Unit,
|
onLongClickItem: (AnimeSource) -> Unit,
|
||||||
|
|
|
@ -38,7 +38,7 @@ fun GlobalAnimeSearchCardRow(
|
||||||
|
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
items(titles) {
|
items(titles) {
|
||||||
val title by getAnime(it)
|
val title by getAnime(it)
|
||||||
|
|
|
@ -74,9 +74,10 @@ internal fun GlobalSearchContent(
|
||||||
items.forEach { (source, result) ->
|
items.forEach { (source, result) ->
|
||||||
item(key = source.id) {
|
item(key = source.id) {
|
||||||
GlobalSearchResultItem(
|
GlobalSearchResultItem(
|
||||||
title = fromSourceId
|
title = fromSourceId?.let {
|
||||||
?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
"▶ ${source.name}".takeIf { source.id == fromSourceId }
|
||||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
} ?: source.name,
|
||||||
|
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
|
||||||
onClick = { onClickSource(source) },
|
onClick = { onClickSource(source) },
|
||||||
) {
|
) {
|
||||||
when (result) {
|
when (result) {
|
||||||
|
|
|
@ -16,8 +16,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||||
import androidx.compose.material.icons.outlined.History
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
@ -38,6 +37,7 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
@ -53,6 +53,7 @@ import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaExtensionDetailsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaExtensionDetailsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
|
@ -62,18 +63,27 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionDetailsScreen(
|
fun MangaExtensionDetailsScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
state: MangaExtensionDetailsScreenModel.State,
|
state: MangaExtensionDetailsScreenModel.State,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickWhatsNew: () -> Unit,
|
|
||||||
onClickReadme: () -> Unit,
|
|
||||||
onClickEnableAll: () -> Unit,
|
onClickEnableAll: () -> Unit,
|
||||||
onClickDisableAll: () -> Unit,
|
onClickDisableAll: () -> Unit,
|
||||||
onClickClearCookies: () -> Unit,
|
onClickClearCookies: () -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
onClickSource: (sourceId: Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
val url = remember(state.extension) {
|
||||||
|
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
||||||
|
regex.find(state.extension?.repoUrl.orEmpty())
|
||||||
|
?.let {
|
||||||
|
val (user, repo) = it.destructured
|
||||||
|
"https://github.com/$user/$repo"
|
||||||
|
}
|
||||||
|
?: state.extension?.repoUrl
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
|
@ -83,19 +93,14 @@ fun ExtensionDetailsScreen(
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||||
.apply {
|
.apply {
|
||||||
if (state.extension?.isUnofficial == false) {
|
if (url != null) {
|
||||||
add(
|
add(
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.whats_new),
|
title = stringResource(MR.strings.action_open_repo),
|
||||||
icon = Icons.Outlined.History,
|
icon = Icons.AutoMirrored.Outlined.Launch,
|
||||||
onClick = onClickWhatsNew,
|
onClick = {
|
||||||
),
|
uriHandler.openUri(url)
|
||||||
)
|
},
|
||||||
add(
|
|
||||||
AppBar.Action(
|
|
||||||
title = stringResource(MR.strings.action_faq_and_guides),
|
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
|
||||||
onClick = onClickReadme,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -125,7 +130,7 @@ fun ExtensionDetailsScreen(
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
if (state.extension == null) {
|
if (state.extension == null) {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
stringRes = MR.strings.empty_screen,
|
MR.strings.empty_screen,
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
|
@ -146,7 +151,7 @@ fun ExtensionDetailsScreen(
|
||||||
private fun ExtensionDetails(
|
private fun ExtensionDetails(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
extension: MangaExtension.Installed,
|
extension: MangaExtension.Installed,
|
||||||
sources: List<MangaExtensionSourceItem>,
|
sources: ImmutableList<MangaExtensionSourceItem>,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
onClickSource: (sourceId: Long) -> Unit,
|
||||||
|
@ -157,12 +162,7 @@ private fun ExtensionDetails(
|
||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
when {
|
if (extension.isObsolete) {
|
||||||
extension.isUnofficial ->
|
|
||||||
item {
|
|
||||||
WarningBanner(MR.strings.unofficial_extension_message)
|
|
||||||
}
|
|
||||||
extension.isObsolete ->
|
|
||||||
item {
|
item {
|
||||||
WarningBanner(MR.strings.obsolete_extension_message)
|
WarningBanner(MR.strings.obsolete_extension_message)
|
||||||
}
|
}
|
||||||
|
@ -295,7 +295,7 @@ private fun DetailsHeader(
|
||||||
top = MaterialTheme.padding.small,
|
top = MaterialTheme.padding.small,
|
||||||
bottom = MaterialTheme.padding.medium,
|
bottom = MaterialTheme.padding.medium,
|
||||||
),
|
),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.presentation.browse.manga
|
package eu.kanade.presentation.browse.manga
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
@ -40,14 +41,18 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.core.util.fastDistinctBy
|
||||||
import eu.kanade.presentation.browse.BaseBrowseItem
|
import eu.kanade.presentation.browse.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.manga.components.MangaExtensionIcon
|
import eu.kanade.presentation.browse.manga.components.MangaExtensionIcon
|
||||||
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
|
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
|
||||||
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionUiModel
|
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionUiModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
|
@ -67,7 +72,7 @@ fun MangaExtensionScreen(
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onLongClickItem: (MangaExtension) -> Unit,
|
onLongClickItem: (MangaExtension) -> Unit,
|
||||||
onClickItemCancel: (MangaExtension) -> Unit,
|
onClickItemCancel: (MangaExtension) -> Unit,
|
||||||
onClickItemWebView: (MangaExtension.Available) -> Unit,
|
onOpenWebView: (MangaExtension.Available) -> Unit,
|
||||||
onInstallExtension: (MangaExtension.Available) -> Unit,
|
onInstallExtension: (MangaExtension.Available) -> Unit,
|
||||||
onUninstallExtension: (MangaExtension) -> Unit,
|
onUninstallExtension: (MangaExtension) -> Unit,
|
||||||
onUpdateExtension: (MangaExtension.Installed) -> Unit,
|
onUpdateExtension: (MangaExtension.Installed) -> Unit,
|
||||||
|
@ -100,7 +105,7 @@ fun MangaExtensionScreen(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemWebView = onClickItemWebView,
|
onOpenWebView = onOpenWebView,
|
||||||
onInstallExtension = onInstallExtension,
|
onInstallExtension = onInstallExtension,
|
||||||
onUninstallExtension = onUninstallExtension,
|
onUninstallExtension = onUninstallExtension,
|
||||||
onUpdateExtension = onUpdateExtension,
|
onUpdateExtension = onUpdateExtension,
|
||||||
|
@ -118,7 +123,7 @@ private fun ExtensionContent(
|
||||||
state: MangaExtensionsScreenModel.State,
|
state: MangaExtensionsScreenModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onLongClickItem: (MangaExtension) -> Unit,
|
onLongClickItem: (MangaExtension) -> Unit,
|
||||||
onClickItemWebView: (MangaExtension.Available) -> Unit,
|
onOpenWebView: (MangaExtension.Available) -> Unit,
|
||||||
onClickItemCancel: (MangaExtension) -> Unit,
|
onClickItemCancel: (MangaExtension) -> Unit,
|
||||||
onInstallExtension: (MangaExtension.Available) -> Unit,
|
onInstallExtension: (MangaExtension.Available) -> Unit,
|
||||||
onUninstallExtension: (MangaExtension) -> Unit,
|
onUninstallExtension: (MangaExtension) -> Unit,
|
||||||
|
@ -127,11 +132,24 @@ private fun ExtensionContent(
|
||||||
onOpenExtension: (MangaExtension.Installed) -> Unit,
|
onOpenExtension: (MangaExtension.Installed) -> Unit,
|
||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
var trustState by remember { mutableStateOf<MangaExtension.Untrusted?>(null) }
|
var trustState by remember { mutableStateOf<MangaExtension.Untrusted?>(null) }
|
||||||
|
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
contentPadding = contentPadding + topSmallPaddingValues,
|
||||||
) {
|
) {
|
||||||
|
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
||||||
|
item(key = "extension-permissions-warning") {
|
||||||
|
WarningBanner(
|
||||||
|
textRes = MR.strings.ext_permission_install_apps_warning,
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
context.launchRequestPackageInstallsPermission()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.items.forEach { (header, items) ->
|
state.items.forEach { (header, items) ->
|
||||||
item(
|
item(
|
||||||
contentType = "header",
|
contentType = "header",
|
||||||
|
@ -170,7 +188,7 @@ private fun ExtensionContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = items,
|
items = items.fastDistinctBy { it.hashCode() },
|
||||||
contentType = { "item" },
|
contentType = { "item" },
|
||||||
key = { "extension-${it.hashCode()}" },
|
key = { "extension-${it.hashCode()}" },
|
||||||
) { item ->
|
) { item ->
|
||||||
|
@ -185,7 +203,13 @@ private fun ExtensionContent(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
onClickItemWebView = onClickItemWebView,
|
onClickItemSecondaryAction = {
|
||||||
|
when (it) {
|
||||||
|
is MangaExtension.Available -> onOpenWebView(it)
|
||||||
|
is MangaExtension.Installed -> onOpenExtension(it)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
},
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemAction = {
|
onClickItemAction = {
|
||||||
when (it) {
|
when (it) {
|
||||||
|
@ -229,9 +253,9 @@ private fun ExtensionItem(
|
||||||
item: MangaExtensionUiModel.Item,
|
item: MangaExtensionUiModel.Item,
|
||||||
onClickItem: (MangaExtension) -> Unit,
|
onClickItem: (MangaExtension) -> Unit,
|
||||||
onLongClickItem: (MangaExtension) -> Unit,
|
onLongClickItem: (MangaExtension) -> Unit,
|
||||||
onClickItemWebView: (MangaExtension.Available) -> Unit,
|
|
||||||
onClickItemCancel: (MangaExtension) -> Unit,
|
onClickItemCancel: (MangaExtension) -> Unit,
|
||||||
onClickItemAction: (MangaExtension) -> Unit,
|
onClickItemAction: (MangaExtension) -> Unit,
|
||||||
|
onClickItemSecondaryAction: (MangaExtension) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val (extension, installStep) = item
|
val (extension, installStep) = item
|
||||||
|
@ -273,9 +297,9 @@ private fun ExtensionItem(
|
||||||
ExtensionItemActions(
|
ExtensionItemActions(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
installStep = installStep,
|
installStep = installStep,
|
||||||
onClickItemWebView = onClickItemWebView,
|
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemAction = onClickItemAction,
|
onClickItemAction = onClickItemAction,
|
||||||
|
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
@ -305,7 +329,7 @@ private fun ExtensionItemContent(
|
||||||
// Won't look good but it's not like we can ellipsize overflowing content
|
// Won't look good but it's not like we can ellipsize overflowing content
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||||
if (extension is MangaExtension.Installed && extension.lang.isNotEmpty()) {
|
if (extension is MangaExtension.Installed && extension.lang.isNotEmpty()) {
|
||||||
|
@ -325,7 +349,6 @@ private fun ExtensionItemContent(
|
||||||
|
|
||||||
val warning = when {
|
val warning = when {
|
||||||
extension is MangaExtension.Untrusted -> MR.strings.ext_untrusted
|
extension is MangaExtension.Untrusted -> MR.strings.ext_untrusted
|
||||||
extension is MangaExtension.Installed && extension.isUnofficial -> MR.strings.ext_unofficial
|
|
||||||
extension is MangaExtension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
extension is MangaExtension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
||||||
extension.isNsfw -> MR.strings.ext_nsfw_short
|
extension.isNsfw -> MR.strings.ext_nsfw_short
|
||||||
else -> null
|
else -> null
|
||||||
|
@ -360,15 +383,15 @@ private fun ExtensionItemActions(
|
||||||
extension: MangaExtension,
|
extension: MangaExtension,
|
||||||
installStep: InstallStep,
|
installStep: InstallStep,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClickItemWebView: (MangaExtension.Available) -> Unit = {},
|
|
||||||
onClickItemCancel: (MangaExtension) -> Unit = {},
|
onClickItemCancel: (MangaExtension) -> Unit = {},
|
||||||
onClickItemAction: (MangaExtension) -> Unit = {},
|
onClickItemAction: (MangaExtension) -> Unit = {},
|
||||||
|
onClickItemSecondaryAction: (MangaExtension) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val isIdle = installStep.isCompleted()
|
val isIdle = installStep.isCompleted()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
!isIdle -> {
|
!isIdle -> {
|
||||||
|
@ -390,6 +413,13 @@ private fun ExtensionItemActions(
|
||||||
installStep == InstallStep.Idle -> {
|
installStep == InstallStep.Idle -> {
|
||||||
when (extension) {
|
when (extension) {
|
||||||
is MangaExtension.Installed -> {
|
is MangaExtension.Installed -> {
|
||||||
|
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Settings,
|
||||||
|
contentDescription = stringResource(MR.strings.action_settings),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (extension.hasUpdate) {
|
if (extension.hasUpdate) {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -398,13 +428,6 @@ private fun ExtensionItemActions(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Settings,
|
|
||||||
contentDescription = stringResource(MR.strings.action_settings),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is MangaExtension.Untrusted -> {
|
is MangaExtension.Untrusted -> {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
|
@ -417,7 +440,7 @@ private fun ExtensionItemActions(
|
||||||
is MangaExtension.Available -> {
|
is MangaExtension.Available -> {
|
||||||
if (extension.sources.isNotEmpty()) {
|
if (extension.sources.isNotEmpty()) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { onClickItemWebView(extension) },
|
onClick = { onClickItemSecondaryAction(extension) },
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Public,
|
imageVector = Icons.Outlined.Public,
|
||||||
|
|
|
@ -26,6 +26,7 @@ import eu.kanade.presentation.browse.manga.components.BaseMangaSourceItem
|
||||||
import eu.kanade.presentation.browse.manga.components.MangaSourceIcon
|
import eu.kanade.presentation.browse.manga.components.MangaSourceIcon
|
||||||
import eu.kanade.tachiyomi.ui.browse.manga.migration.sources.MigrateMangaSourceScreenModel
|
import eu.kanade.tachiyomi.ui.browse.manga.migration.sources.MigrateMangaSourceScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import tachiyomi.domain.source.manga.model.Source
|
import tachiyomi.domain.source.manga.model.Source
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.Badge
|
import tachiyomi.presentation.core.components.Badge
|
||||||
|
@ -75,7 +76,7 @@ fun MigrateMangaSourceScreen(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MigrateSourceList(
|
private fun MigrateSourceList(
|
||||||
list: List<Pair<Source, Long>>,
|
list: ImmutableList<Pair<Source, Long>>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source) -> Unit,
|
onClickItem: (Source) -> Unit,
|
||||||
onLongClickItem: (Source) -> Unit,
|
onLongClickItem: (Source) -> Unit,
|
||||||
|
|
|
@ -38,7 +38,7 @@ fun GlobalMangaSearchCardRow(
|
||||||
|
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
items(titles) {
|
items(titles) {
|
||||||
val title by getManga(it)
|
val title by getManga(it)
|
||||||
|
|
|
@ -27,6 +27,8 @@ import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import eu.kanade.core.preference.asToggleableState
|
import eu.kanade.core.preference.asToggleableState
|
||||||
import eu.kanade.presentation.category.visualName
|
import eu.kanade.presentation.category.visualName
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import tachiyomi.core.preference.CheckboxState
|
import tachiyomi.core.preference.CheckboxState
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
|
@ -39,12 +41,12 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
fun CategoryCreateDialog(
|
fun CategoryCreateDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onCreate: (String) -> Unit,
|
onCreate: (String) -> Unit,
|
||||||
categories: List<Category>,
|
categories: ImmutableList<String>,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
@ -69,10 +71,13 @@ fun CategoryCreateDialog(
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequester),
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
label = { Text(text = stringResource(MR.strings.name)) },
|
label = {
|
||||||
|
Text(text = stringResource(MR.strings.name))
|
||||||
|
},
|
||||||
supportingText = {
|
supportingText = {
|
||||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||||
MR.strings.error_category_exists
|
MR.strings.error_category_exists
|
||||||
|
@ -98,14 +103,14 @@ fun CategoryCreateDialog(
|
||||||
fun CategoryRenameDialog(
|
fun CategoryRenameDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onRename: (String) -> Unit,
|
onRename: (String) -> Unit,
|
||||||
categories: List<Category>,
|
categories: ImmutableList<String>,
|
||||||
category: Category,
|
category: String,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf(category.name) }
|
var name by remember { mutableStateOf(category) }
|
||||||
var valueHasChanged by remember { mutableStateOf(false) }
|
var valueHasChanged by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
@ -162,7 +167,7 @@ fun CategoryRenameDialog(
|
||||||
fun CategoryDeleteDialog(
|
fun CategoryDeleteDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
category: Category,
|
category: String,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
@ -183,7 +188,7 @@ fun CategoryDeleteDialog(
|
||||||
Text(text = stringResource(MR.strings.delete_category))
|
Text(text = stringResource(MR.strings.delete_category))
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(text = stringResource(MR.strings.delete_category_confirmation, category.name))
|
Text(text = stringResource(MR.strings.delete_category_confirmation, category))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -219,7 +224,7 @@ fun CategorySortAlphabeticallyDialog(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangeCategoryDialog(
|
fun ChangeCategoryDialog(
|
||||||
initialSelection: List<CheckboxState<Category>>,
|
initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onEditCategories: () -> Unit,
|
onEditCategories: () -> Unit,
|
||||||
onConfirm: (List<Long>, List<Long>) -> Unit,
|
onConfirm: (List<Long>, List<Long>) -> Unit,
|
||||||
|
@ -291,7 +296,7 @@ fun ChangeCategoryDialog(
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
val mutableList = selection.toMutableList()
|
val mutableList = selection.toMutableList()
|
||||||
mutableList[index] = it.next()
|
mutableList[index] = it.next()
|
||||||
selection = mutableList.toList()
|
selection = mutableList.toList().toImmutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
@ -325,7 +330,3 @@ fun ChangeCategoryDialog(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun List<Category>.anyWithName(name: String): Boolean {
|
|
||||||
return any { name == it.name }
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
@ -16,11 +17,13 @@ import tachiyomi.presentation.core.util.isScrollingUp
|
||||||
fun CategoryFloatingActionButton(
|
fun CategoryFloatingActionButton(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
onCreate: () -> Unit,
|
onCreate: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
|
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
|
||||||
onClick = onCreate,
|
onClick = onCreate,
|
||||||
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
|
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ fun CategoryListItem(
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
|
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||||
Text(
|
Text(
|
||||||
text = category.name,
|
text = category.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -64,13 +64,13 @@ fun CategoryListItem(
|
||||||
onClick = { onMoveUp(category) },
|
onClick = { onMoveUp(category) },
|
||||||
enabled = canMoveUp,
|
enabled = canMoveUp,
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
|
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { onMoveDown(category) },
|
onClick = { onMoveDown(category) },
|
||||||
enabled = canMoveDown,
|
enabled = canMoveDown,
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
|
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
IconButton(onClick = onRename) {
|
IconButton(onClick = onRename) {
|
||||||
|
|
|
@ -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
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
||||||
|
@ -22,12 +25,17 @@ import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DropdownMenu but overlaps anchor and has width constraints to better
|
||||||
|
* match non-Compose implementation.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DropdownMenu(
|
fun DropdownMenu(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
offset: DpOffset = DpOffset(8.dp, (-56).dp),
|
offset: DpOffset = DpOffset(8.dp, (-56).dp),
|
||||||
|
scrollState: ScrollState = rememberScrollState(),
|
||||||
properties: PopupProperties = PopupProperties(focusable = true),
|
properties: PopupProperties = PopupProperties(focusable = true),
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
|
@ -36,6 +44,7 @@ fun DropdownMenu(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
|
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
|
||||||
offset = offset,
|
offset = offset,
|
||||||
|
scrollState = scrollState,
|
||||||
properties = properties,
|
properties = properties,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
|
@ -45,6 +54,7 @@ fun DropdownMenu(
|
||||||
fun RadioMenuItem(
|
fun RadioMenuItem(
|
||||||
text: @Composable () -> Unit,
|
text: @Composable () -> Unit,
|
||||||
isChecked: Boolean,
|
isChecked: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
|
@ -64,6 +74,7 @@ fun RadioMenuItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,10 +82,12 @@ fun RadioMenuItem(
|
||||||
fun NestedMenuItem(
|
fun NestedMenuItem(
|
||||||
text: @Composable () -> Unit,
|
text: @Composable () -> Unit,
|
||||||
children: @Composable ColumnScope.(() -> Unit) -> Unit,
|
children: @Composable ColumnScope.(() -> Unit) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var nestedExpanded by remember { mutableStateOf(false) }
|
var nestedExpanded by remember { mutableStateOf(false) }
|
||||||
val closeMenu = { nestedExpanded = false }
|
val closeMenu = { nestedExpanded = false }
|
||||||
|
|
||||||
|
Box {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = text,
|
text = text,
|
||||||
onClick = { nestedExpanded = true },
|
onClick = { nestedExpanded = true },
|
||||||
|
@ -89,7 +102,9 @@ fun NestedMenuItem(
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = nestedExpanded,
|
expanded = nestedExpanded,
|
||||||
onDismissRequest = closeMenu,
|
onDismissRequest = closeMenu,
|
||||||
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
children(closeMenu)
|
children(closeMenu)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ package eu.kanade.presentation.components
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import eu.kanade.presentation.entries.DownloadAction
|
import eu.kanade.presentation.entries.DownloadAction
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
@ -14,20 +16,24 @@ fun EntryDownloadDropdownMenu(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDownloadClicked: (DownloadAction) -> Unit,
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
isManga: Boolean,
|
isManga: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
DropdownMenu(
|
val downloadAmount = if (isManga) MR.plurals.download_amount else MR.plurals.download_amount_anime
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
) {
|
|
||||||
val downloadAmount = if (isManga) MR.plurals.download_amount_manga else MR.plurals.download_amount_anime
|
|
||||||
val downloadUnviewed = if (isManga) MR.strings.download_unread else MR.strings.download_unseen
|
val downloadUnviewed = if (isManga) MR.strings.download_unread else MR.strings.download_unseen
|
||||||
listOfNotNull(
|
val options = persistentListOf(
|
||||||
DownloadAction.NEXT_1_ITEM to pluralStringResource(downloadAmount, 1, 1),
|
DownloadAction.NEXT_1_ITEM to pluralStringResource(downloadAmount, 1, 1),
|
||||||
DownloadAction.NEXT_5_ITEMS to pluralStringResource(downloadAmount, 5, 5),
|
DownloadAction.NEXT_5_ITEMS to pluralStringResource(downloadAmount, 5, 5),
|
||||||
DownloadAction.NEXT_10_ITEMS to pluralStringResource(downloadAmount, 10, 10),
|
DownloadAction.NEXT_10_ITEMS to pluralStringResource(downloadAmount, 10, 10),
|
||||||
DownloadAction.NEXT_25_ITEMS to pluralStringResource(downloadAmount, 25, 25),
|
DownloadAction.NEXT_25_ITEMS to pluralStringResource(downloadAmount, 25, 25),
|
||||||
DownloadAction.UNVIEWED_ITEMS to stringResource(downloadUnviewed),
|
DownloadAction.UNVIEWED_ITEMS to stringResource(downloadUnviewed),
|
||||||
).map { (downloadAction, string) ->
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
options.map { (downloadAction, string) ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = string) },
|
text = { Text(text = string) },
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
@ -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.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.domain.entries.anime.model.episodesFiltered
|
import eu.kanade.domain.entries.anime.model.episodesFiltered
|
||||||
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.entries.DownloadAction
|
import eu.kanade.presentation.entries.DownloadAction
|
||||||
import eu.kanade.presentation.entries.EntryScreenItem
|
import eu.kanade.presentation.entries.EntryScreenItem
|
||||||
import eu.kanade.presentation.entries.anime.components.AnimeActionRow
|
import eu.kanade.presentation.entries.anime.components.AnimeActionRow
|
||||||
|
@ -70,10 +71,9 @@ import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
|
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
|
||||||
import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo
|
import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo
|
||||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen
|
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeSourcePreferencesScreen
|
||||||
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel
|
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList
|
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
|
@ -90,17 +90,15 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||||
import tachiyomi.presentation.core.util.isScrollingUp
|
import tachiyomi.presentation.core.util.isScrollingUp
|
||||||
import java.text.DateFormat
|
import tachiyomi.source.local.entries.anime.isLocal
|
||||||
import java.util.Date
|
import java.time.Instant
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeScreen(
|
fun AnimeScreen(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: Int?,
|
nextUpdate: Instant?,
|
||||||
dateRelativeTime: Boolean,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
|
@ -156,16 +154,14 @@ fun AnimeScreen(
|
||||||
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val onSettingsClicked: (() -> Unit)? = {
|
val onSettingsClicked: (() -> Unit)? = {
|
||||||
navigator.push(SourcePreferencesScreen(state.source.id))
|
navigator.push(AnimeSourcePreferencesScreen(state.source.id))
|
||||||
}.takeIf { state.source is ConfigurableAnimeSource }
|
}.takeIf { state.source is ConfigurableAnimeSource }
|
||||||
|
|
||||||
if (!isTabletUi) {
|
if (!isTabletUi) {
|
||||||
AnimeScreenSmallImpl(
|
AnimeScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
nextUpdate = nextUpdate,
|
||||||
dateFormat = dateFormat,
|
|
||||||
fetchInterval = fetchInterval,
|
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||||
|
@ -204,13 +200,11 @@ fun AnimeScreen(
|
||||||
AnimeScreenLargeImpl(
|
AnimeScreenLargeImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
nextUpdate = nextUpdate,
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||||
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
|
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
|
||||||
dateFormat = dateFormat,
|
|
||||||
fetchInterval = fetchInterval,
|
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
onDownloadEpisode = onDownloadEpisode,
|
onDownloadEpisode = onDownloadEpisode,
|
||||||
|
@ -249,9 +243,7 @@ fun AnimeScreen(
|
||||||
private fun AnimeScreenSmallImpl(
|
private fun AnimeScreenSmallImpl(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Boolean,
|
nextUpdate: Instant?,
|
||||||
dateFormat: DateFormat,
|
|
||||||
fetchInterval: Int?,
|
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
showNextEpisodeAirTime: Boolean,
|
showNextEpisodeAirTime: Boolean,
|
||||||
|
@ -455,7 +447,7 @@ private fun AnimeScreenSmallImpl(
|
||||||
AnimeActionRow(
|
AnimeActionRow(
|
||||||
favorite = state.anime.favorite,
|
favorite = state.anime.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
fetchInterval = fetchInterval,
|
nextUpdate = nextUpdate,
|
||||||
isUserIntervalMode = state.anime.fetchInterval < 0,
|
isUserIntervalMode = state.anime.fetchInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
|
@ -526,8 +518,6 @@ private fun AnimeScreenSmallImpl(
|
||||||
anime = state.anime,
|
anime = state.anime,
|
||||||
episodes = listItem,
|
episodes = listItem,
|
||||||
isAnyEpisodeSelected = episodes.fastAny { it.selected },
|
isAnyEpisodeSelected = episodes.fastAny { it.selected },
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
|
@ -546,9 +536,7 @@ private fun AnimeScreenSmallImpl(
|
||||||
fun AnimeScreenLargeImpl(
|
fun AnimeScreenLargeImpl(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Boolean,
|
nextUpdate: Instant?,
|
||||||
dateFormat: DateFormat,
|
|
||||||
fetchInterval: Int?,
|
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
showNextEpisodeAirTime: Boolean,
|
showNextEpisodeAirTime: Boolean,
|
||||||
|
@ -734,7 +722,7 @@ fun AnimeScreenLargeImpl(
|
||||||
AnimeActionRow(
|
AnimeActionRow(
|
||||||
favorite = state.anime.favorite,
|
favorite = state.anime.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
fetchInterval = fetchInterval,
|
nextUpdate = nextUpdate,
|
||||||
isUserIntervalMode = state.anime.fetchInterval < 0,
|
isUserIntervalMode = state.anime.fetchInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
|
@ -812,8 +800,6 @@ fun AnimeScreenLargeImpl(
|
||||||
anime = state.anime,
|
anime = state.anime,
|
||||||
episodes = listItem,
|
episodes = listItem,
|
||||||
isAnyEpisodeSelected = episodes.fastAny { it.selected },
|
isAnyEpisodeSelected = episodes.fastAny { it.selected },
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
|
@ -884,8 +870,6 @@ private fun LazyListScope.sharedEpisodeItems(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
episodes: List<EpisodeList>,
|
episodes: List<EpisodeList>,
|
||||||
isAnyEpisodeSelected: Boolean,
|
isAnyEpisodeSelected: Boolean,
|
||||||
dateRelativeTime: Boolean,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||||
|
@ -904,7 +888,6 @@ private fun LazyListScope.sharedEpisodeItems(
|
||||||
contentType = { EntryScreenItem.ITEM },
|
contentType = { EntryScreenItem.ITEM },
|
||||||
) { episodeItem ->
|
) { episodeItem ->
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
when (episodeItem) {
|
when (episodeItem) {
|
||||||
is EpisodeList.MissingCount -> {
|
is EpisodeList.MissingCount -> {
|
||||||
|
@ -920,15 +903,7 @@ private fun LazyListScope.sharedEpisodeItems(
|
||||||
} else {
|
} else {
|
||||||
episodeItem.episode.name
|
episodeItem.episode.name
|
||||||
},
|
},
|
||||||
date = episodeItem.episode.dateUpload
|
date = relativeDateText(episodeItem.episode.dateUpload),
|
||||||
.takeIf { it > 0L }
|
|
||||||
?.let {
|
|
||||||
Date(it).toRelativeString(
|
|
||||||
context,
|
|
||||||
dateRelativeTime,
|
|
||||||
dateFormat,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
watchProgress = episodeItem.episode.lastSecondSeen
|
watchProgress = episodeItem.episode.lastSecondSeen
|
||||||
.takeIf { !episodeItem.episode.seen && it > 0L }
|
.takeIf { !episodeItem.episode.seen && it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
|
@ -942,7 +917,7 @@ private fun LazyListScope.sharedEpisodeItems(
|
||||||
seen = episodeItem.episode.seen,
|
seen = episodeItem.episode.seen,
|
||||||
bookmark = episodeItem.episode.bookmark,
|
bookmark = episodeItem.episode.bookmark,
|
||||||
selected = episodeItem.selected,
|
selected = episodeItem.selected,
|
||||||
downloadIndicatorEnabled = !isAnyEpisodeSelected,
|
downloadIndicatorEnabled = !isAnyEpisodeSelected && !anime.isLocal(),
|
||||||
downloadStateProvider = { episodeItem.downloadState },
|
downloadStateProvider = { episodeItem.downloadState },
|
||||||
downloadProgressProvider = { episodeItem.downloadProgress },
|
downloadProgressProvider = { episodeItem.downloadProgress },
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
|
|
|
@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -28,7 +29,7 @@ fun DuplicateAnimeDialog(
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.ContentCopy
|
import androidx.compose.material.icons.outlined.ContentCopy
|
||||||
import androidx.compose.material.icons.outlined.Download
|
import androidx.compose.material.icons.outlined.Download
|
||||||
|
import androidx.compose.material.icons.outlined.Input
|
||||||
import androidx.compose.material.icons.outlined.NavigateNext
|
import androidx.compose.material.icons.outlined.NavigateNext
|
||||||
import androidx.compose.material.icons.outlined.OpenInNew
|
import androidx.compose.material.icons.outlined.OpenInNew
|
||||||
import androidx.compose.material.icons.outlined.SystemUpdateAlt
|
import androidx.compose.material.icons.outlined.SystemUpdateAlt
|
||||||
|
@ -256,6 +257,18 @@ private fun VideoList(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onIntPlayerClicked = {
|
||||||
|
scope.launch {
|
||||||
|
MainActivity.startPlayerActivity(
|
||||||
|
context,
|
||||||
|
anime.id,
|
||||||
|
episode.id,
|
||||||
|
false,
|
||||||
|
selectedVideo,
|
||||||
|
videoList,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -289,6 +302,7 @@ private fun QualityOptions(
|
||||||
onExtDownloadClicked: () -> Unit = {},
|
onExtDownloadClicked: () -> Unit = {},
|
||||||
onCopyClicked: () -> Unit = {},
|
onCopyClicked: () -> Unit = {},
|
||||||
onExtPlayerClicked: () -> Unit = {},
|
onExtPlayerClicked: () -> Unit = {},
|
||||||
|
onIntPlayerClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val closeMenu = { EpisodeOptionsDialogScreen.onDismissDialog() }
|
val closeMenu = { EpisodeOptionsDialogScreen.onDismissDialog() }
|
||||||
|
|
||||||
|
@ -325,6 +339,15 @@ private fun QualityOptions(
|
||||||
closeMenu()
|
closeMenu()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ClickableRow(
|
||||||
|
text = stringResource(MR.strings.action_play_internally),
|
||||||
|
icon = Icons.Outlined.Input,
|
||||||
|
onClick = {
|
||||||
|
onIntPlayerClicked()
|
||||||
|
closeMenu()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -188,9 +188,9 @@ fun AnimeEpisodeListItem(
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
if (watchProgress != null || scanlator != null) DotSeparatorText()
|
|
||||||
}
|
}
|
||||||
if (watchProgress != null) {
|
if (watchProgress != null) {
|
||||||
|
DotSeparatorText()
|
||||||
Text(
|
Text(
|
||||||
text = watchProgress,
|
text = watchProgress,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
@ -200,6 +200,7 @@ fun AnimeEpisodeListItem(
|
||||||
if (scanlator != null) DotSeparatorText()
|
if (scanlator != null) DotSeparatorText()
|
||||||
}
|
}
|
||||||
if (scanlator != null) {
|
if (scanlator != null) {
|
||||||
|
DotSeparatorText()
|
||||||
Text(
|
Text(
|
||||||
text = scanlator,
|
text = scanlator,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
@ -210,19 +211,17 @@ fun AnimeEpisodeListItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onDownloadClick != null) {
|
|
||||||
EpisodeDownloadIndicator(
|
EpisodeDownloadIndicator(
|
||||||
enabled = downloadIndicatorEnabled,
|
enabled = downloadIndicatorEnabled,
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
downloadStateProvider = downloadStateProvider,
|
downloadStateProvider = downloadStateProvider,
|
||||||
downloadProgressProvider = downloadProgressProvider,
|
downloadProgressProvider = downloadProgressProvider,
|
||||||
onClick = onDownloadClick,
|
onClick = { onDownloadClick?.invoke(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSwipeAction(
|
private fun getSwipeAction(
|
||||||
action: LibraryPreferences.EpisodeSwipeAction,
|
action: LibraryPreferences.EpisodeSwipeAction,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
@ -87,14 +88,14 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
import kotlin.math.absoluteValue
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeInfoBox(
|
fun AnimeInfoBox(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
appBarPadding: Dp,
|
appBarPadding: Dp,
|
||||||
title: String,
|
title: String,
|
||||||
|
@ -106,6 +107,7 @@ fun AnimeInfoBox(
|
||||||
status: Long,
|
status: Long,
|
||||||
onCoverClick: () -> Unit,
|
onCoverClick: () -> Unit,
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
// Backdrop
|
// Backdrop
|
||||||
|
@ -164,10 +166,9 @@ fun AnimeInfoBox(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeActionRow(
|
fun AnimeActionRow(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
fetchInterval: Int?,
|
nextUpdate: Instant?,
|
||||||
isUserIntervalMode: Boolean,
|
isUserIntervalMode: Boolean,
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
|
@ -175,9 +176,20 @@ fun AnimeActionRow(
|
||||||
onTrackingClicked: () -> Unit,
|
onTrackingClicked: () -> Unit,
|
||||||
onEditIntervalClicked: (() -> Unit)?,
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onEditCategory: (() -> Unit)?,
|
onEditCategory: (() -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||||
|
|
||||||
|
// TODO: show something better when using custom interval
|
||||||
|
val nextUpdateDays = remember(nextUpdate) {
|
||||||
|
return@remember if (nextUpdate != null) {
|
||||||
|
val now = Instant.now()
|
||||||
|
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||||
AnimeActionButton(
|
AnimeActionButton(
|
||||||
title = if (favorite) {
|
title = if (favorite) {
|
||||||
|
@ -190,18 +202,20 @@ fun AnimeActionRow(
|
||||||
onClick = onAddToLibraryClicked,
|
onClick = onAddToLibraryClicked,
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
|
||||||
AnimeActionButton(
|
AnimeActionButton(
|
||||||
title = pluralStringResource(
|
title = when (nextUpdateDays) {
|
||||||
|
null -> stringResource(MR.strings.not_applicable)
|
||||||
|
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
|
||||||
|
else -> pluralStringResource(
|
||||||
MR.plurals.day,
|
MR.plurals.day,
|
||||||
count = fetchInterval.absoluteValue,
|
count = nextUpdateDays,
|
||||||
fetchInterval.absoluteValue,
|
nextUpdateDays,
|
||||||
),
|
)
|
||||||
|
},
|
||||||
icon = Icons.Default.HourglassEmpty,
|
icon = Icons.Default.HourglassEmpty,
|
||||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
onClick = onEditIntervalClicked,
|
onClick = { onEditIntervalClicked?.invoke() },
|
||||||
)
|
)
|
||||||
}
|
|
||||||
AnimeActionButton(
|
AnimeActionButton(
|
||||||
title = if (trackingCount == 0) {
|
title = if (trackingCount == 0) {
|
||||||
stringResource(MR.strings.manga_tracking_tab)
|
stringResource(MR.strings.manga_tracking_tab)
|
||||||
|
@ -227,12 +241,12 @@ fun AnimeActionRow(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExpandableAnimeDescription(
|
fun ExpandableAnimeDescription(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
defaultExpandState: Boolean,
|
defaultExpandState: Boolean,
|
||||||
description: String?,
|
description: String?,
|
||||||
tagsProvider: () -> List<String>?,
|
tagsProvider: () -> List<String>?,
|
||||||
onTagSearch: (String) -> Unit,
|
onTagSearch: (String) -> Unit,
|
||||||
onCopyTagToClipboard: (tag: String) -> Unit,
|
onCopyTagToClipboard: (tag: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
val (expanded, onExpanded) = rememberSaveable {
|
val (expanded, onExpanded) = rememberSaveable {
|
||||||
|
@ -288,7 +302,7 @@ fun ExpandableAnimeDescription(
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
tags.forEach {
|
tags.forEach {
|
||||||
TagsChip(
|
TagsChip(
|
||||||
|
@ -304,7 +318,7 @@ fun ExpandableAnimeDescription(
|
||||||
} else {
|
} else {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
items(items = tags) {
|
items(items = tags) {
|
||||||
TagsChip(
|
TagsChip(
|
||||||
|
@ -407,15 +421,15 @@ private fun AnimeAndSourceTitlesSmall(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AnimeContentInfo(
|
private fun ColumnScope.AnimeContentInfo(
|
||||||
title: String,
|
title: String,
|
||||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
author: String?,
|
author: String?,
|
||||||
artist: String?,
|
artist: String?,
|
||||||
status: Long,
|
status: Long,
|
||||||
sourceName: String,
|
sourceName: String,
|
||||||
isStubSource: Boolean,
|
isStubSource: Boolean,
|
||||||
|
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Text(
|
Text(
|
||||||
|
@ -439,7 +453,7 @@ private fun AnimeContentInfo(
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -470,7 +484,7 @@ private fun AnimeContentInfo(
|
||||||
if (!artist.isNullOrBlank() && author != artist) {
|
if (!artist.isNullOrBlank() && author != artist) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
@ -20,8 +20,8 @@ import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseAnimeListItem(
|
fun BaseAnimeListItem(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onClickItem: () -> Unit = {},
|
onClickItem: () -> Unit = {},
|
||||||
onClickCover: () -> Unit = onClickItem,
|
onClickCover: () -> Unit = onClickItem,
|
||||||
cover: @Composable RowScope.() -> Unit = { defaultCover(anime, onClickCover) },
|
cover: @Composable RowScope.() -> Unit = { defaultCover(anime, onClickCover) },
|
||||||
|
|
|
@ -46,10 +46,10 @@ enum class EpisodeDownloadAction {
|
||||||
@Composable
|
@Composable
|
||||||
fun EpisodeDownloadIndicator(
|
fun EpisodeDownloadIndicator(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
downloadStateProvider: () -> AnimeDownload.State,
|
downloadStateProvider: () -> AnimeDownload.State,
|
||||||
downloadProgressProvider: () -> Int,
|
downloadProgressProvider: () -> Int,
|
||||||
onClick: (EpisodeDownloadAction) -> Unit,
|
onClick: (EpisodeDownloadAction) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (val downloadState = downloadStateProvider()) {
|
when (val downloadState = downloadStateProvider()) {
|
||||||
AnimeDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
AnimeDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
||||||
|
@ -106,10 +106,10 @@ private fun NotDownloadedIndicator(
|
||||||
@Composable
|
@Composable
|
||||||
private fun DownloadingIndicator(
|
private fun DownloadingIndicator(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
downloadState: AnimeDownload.State,
|
downloadState: AnimeDownload.State,
|
||||||
downloadProgressProvider: () -> Int,
|
downloadProgressProvider: () -> Int,
|
||||||
onClick: (EpisodeDownloadAction) -> Unit,
|
onClick: (EpisodeDownloadAction) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||||
Box(
|
Box(
|
||||||
|
|
|
@ -2,13 +2,24 @@ package eu.kanade.presentation.entries.components
|
||||||
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DotSeparatorText() {
|
fun DotSeparatorText(
|
||||||
Text(text = " • ")
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = " • ",
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DotSeparatorNoSpaceText() {
|
fun DotSeparatorNoSpaceText(
|
||||||
Text(text = "•")
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "•",
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,7 +225,10 @@ private fun RowScope.Button(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
content: (@Composable () -> Unit)? = null,
|
content: (@Composable () -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
|
val animatedWeight by animateFloatAsState(
|
||||||
|
targetValue = if (toConfirm) 2f else 1f,
|
||||||
|
label = "weight",
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
|
@ -262,13 +265,13 @@ private fun RowScope.Button(
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryBottomActionMenu(
|
fun LibraryBottomActionMenu(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onChangeCategoryClicked: () -> Unit,
|
onChangeCategoryClicked: () -> Unit,
|
||||||
onMarkAsViewedClicked: () -> Unit,
|
onMarkAsViewedClicked: () -> Unit,
|
||||||
onMarkAsUnviewedClicked: () -> Unit,
|
onMarkAsUnviewedClicked: () -> Unit,
|
||||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||||
onDeleteClicked: () -> Unit,
|
onDeleteClicked: () -> Unit,
|
||||||
isManga: Boolean,
|
isManga: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
|
|
|
@ -20,7 +20,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
@ -161,6 +160,14 @@ fun EntryToolbar(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (onClickSettings != null) {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.settings),
|
||||||
|
onClick = onClickSettings,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,8 +22,8 @@ enum class ItemCover(val ratio: Float) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
operator fun invoke(
|
operator fun invoke(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
data: Any?,
|
data: Any?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
contentDescription: String = "",
|
contentDescription: String = "",
|
||||||
shape: Shape = MaterialTheme.shapes.extraSmall,
|
shape: Shape = MaterialTheme.shapes.extraSmall,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
|
@ -23,16 +24,17 @@ fun ItemHeader(
|
||||||
missingItemsCount: Int,
|
missingItemsCount: Int,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
isManga: Boolean,
|
isManga: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (itemCount == null) {
|
text = if (itemCount == null) {
|
||||||
|
|
|
@ -1,23 +1,37 @@
|
||||||
package eu.kanade.presentation.entries.components
|
package eu.kanade.presentation.entries.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||||
|
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeleteItemsDialog(
|
fun DeleteItemsDialog(
|
||||||
|
@ -55,21 +69,65 @@ fun DeleteItemsDialog(
|
||||||
@Composable
|
@Composable
|
||||||
fun SetIntervalDialog(
|
fun SetIntervalDialog(
|
||||||
interval: Int,
|
interval: Int,
|
||||||
|
nextUpdate: Instant?,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onValueChanged: (Int) -> Unit,
|
isManga: Boolean,
|
||||||
|
onValueChanged: ((Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
||||||
|
|
||||||
|
val nextUpdateDays = remember(nextUpdate) {
|
||||||
|
return@remember if (nextUpdate != null) {
|
||||||
|
val now = Instant.now()
|
||||||
|
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = { Text(text = stringResource(MR.strings.manga_modify_calculated_interval_title)) },
|
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
|
||||||
text = {
|
text = {
|
||||||
|
Column {
|
||||||
|
if (nextUpdateDays != null && nextUpdateDays >= 0 && interval >= 0) {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
if (isManga) {
|
||||||
|
MR.strings.manga_interval_expected_update
|
||||||
|
} else {
|
||||||
|
MR.strings.anime_interval_expected_update
|
||||||
|
},
|
||||||
|
pluralStringResource(
|
||||||
|
MR.plurals.day,
|
||||||
|
count = nextUpdateDays,
|
||||||
|
nextUpdateDays,
|
||||||
|
),
|
||||||
|
pluralStringResource(
|
||||||
|
MR.plurals.day,
|
||||||
|
count = interval.absoluteValue,
|
||||||
|
interval.absoluteValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(MaterialTheme.padding.small))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
||||||
|
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||||
val items = (0..28)
|
val maxInterval = if (isManga) {
|
||||||
|
MangaFetchInterval.MAX_INTERVAL
|
||||||
|
} else {
|
||||||
|
AnimeFetchInterval.MAX_INTERVAL
|
||||||
|
}
|
||||||
|
val items = (0..maxInterval)
|
||||||
.map {
|
.map {
|
||||||
if (it == 0) {
|
if (it == 0) {
|
||||||
stringResource(MR.strings.label_default)
|
stringResource(MR.strings.label_default)
|
||||||
|
@ -85,6 +143,8 @@ fun SetIntervalDialog(
|
||||||
onSelectionChanged = { selectedInterval = it },
|
onSelectionChanged = { selectedInterval = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismissRequest) {
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
@ -93,7 +153,7 @@ fun SetIntervalDialog(
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onValueChanged(selectedInterval)
|
onValueChanged?.invoke(selectedInterval)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
}) {
|
}) {
|
||||||
Text(text = stringResource(MR.strings.action_ok))
|
Text(text = stringResource(MR.strings.action_ok))
|
||||||
|
|
|
@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -28,7 +29,7 @@ fun DuplicateMangaDialog(
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
@ -49,6 +49,7 @@ import androidx.compose.ui.util.fastAny
|
||||||
import androidx.compose.ui.util.fastMap
|
import androidx.compose.ui.util.fastMap
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.entries.DownloadAction
|
import eu.kanade.presentation.entries.DownloadAction
|
||||||
import eu.kanade.presentation.entries.EntryScreenItem
|
import eu.kanade.presentation.entries.EntryScreenItem
|
||||||
import eu.kanade.presentation.entries.components.EntryBottomActionMenu
|
import eu.kanade.presentation.entries.components.EntryBottomActionMenu
|
||||||
|
@ -67,7 +68,6 @@ import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo
|
||||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
|
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
|
||||||
import eu.kanade.tachiyomi.ui.entries.manga.ChapterList
|
import eu.kanade.tachiyomi.ui.entries.manga.ChapterList
|
||||||
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel
|
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
|
@ -83,16 +83,14 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||||
import tachiyomi.presentation.core.util.isScrollingUp
|
import tachiyomi.presentation.core.util.isScrollingUp
|
||||||
import java.text.DateFormat
|
import tachiyomi.source.local.entries.manga.isLocal
|
||||||
import java.util.Date
|
import java.time.Instant
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaScreen(
|
fun MangaScreen(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: Int?,
|
nextUpdate: Instant?,
|
||||||
dateRelativeTime: Boolean,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -152,9 +150,7 @@ fun MangaScreen(
|
||||||
MangaScreenSmallImpl(
|
MangaScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
nextUpdate = nextUpdate,
|
||||||
dateFormat = dateFormat,
|
|
||||||
fetchInterval = fetchInterval,
|
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
|
@ -190,11 +186,9 @@ fun MangaScreen(
|
||||||
MangaScreenLargeImpl(
|
MangaScreenLargeImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
dateFormat = dateFormat,
|
nextUpdate = nextUpdate,
|
||||||
fetchInterval = fetchInterval,
|
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
onDownloadChapter = onDownloadChapter,
|
onDownloadChapter = onDownloadChapter,
|
||||||
|
@ -231,9 +225,7 @@ fun MangaScreen(
|
||||||
private fun MangaScreenSmallImpl(
|
private fun MangaScreenSmallImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Boolean,
|
nextUpdate: Instant?,
|
||||||
dateFormat: DateFormat,
|
|
||||||
fetchInterval: Int?,
|
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
@ -425,7 +417,7 @@ private fun MangaScreenSmallImpl(
|
||||||
MangaActionRow(
|
MangaActionRow(
|
||||||
favorite = state.manga.favorite,
|
favorite = state.manga.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
fetchInterval = fetchInterval,
|
nextUpdate = nextUpdate,
|
||||||
isUserIntervalMode = state.manga.fetchInterval < 0,
|
isUserIntervalMode = state.manga.fetchInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
|
@ -469,8 +461,6 @@ private fun MangaScreenSmallImpl(
|
||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = listItem,
|
chapters = listItem,
|
||||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
|
@ -488,9 +478,7 @@ private fun MangaScreenSmallImpl(
|
||||||
fun MangaScreenLargeImpl(
|
fun MangaScreenLargeImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Boolean,
|
nextUpdate: Instant?,
|
||||||
dateFormat: DateFormat,
|
|
||||||
fetchInterval: Int?,
|
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
@ -670,7 +658,7 @@ fun MangaScreenLargeImpl(
|
||||||
MangaActionRow(
|
MangaActionRow(
|
||||||
favorite = state.manga.favorite,
|
favorite = state.manga.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
fetchInterval = fetchInterval,
|
nextUpdate = nextUpdate,
|
||||||
isUserIntervalMode = state.manga.fetchInterval < 0,
|
isUserIntervalMode = state.manga.fetchInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
|
@ -721,8 +709,6 @@ fun MangaScreenLargeImpl(
|
||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = listItem,
|
chapters = listItem,
|
||||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
|
@ -775,7 +761,7 @@ private fun SharedMangaBottomActionMenu(
|
||||||
onDeleteClicked = {
|
onDeleteClicked = {
|
||||||
onMultiDeleteClicked(selected.fastMap { it.chapter })
|
onMultiDeleteClicked(selected.fastMap { it.chapter })
|
||||||
}.takeIf {
|
}.takeIf {
|
||||||
onDownloadChapter != null && selected.fastAny { it.downloadState == MangaDownload.State.DOWNLOADED }
|
selected.fastAny { it.downloadState == MangaDownload.State.DOWNLOADED }
|
||||||
},
|
},
|
||||||
isManga = true,
|
isManga = true,
|
||||||
)
|
)
|
||||||
|
@ -785,8 +771,6 @@ private fun LazyListScope.sharedChapterItems(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<ChapterList>,
|
chapters: List<ChapterList>,
|
||||||
isAnyChapterSelected: Boolean,
|
isAnyChapterSelected: Boolean,
|
||||||
dateRelativeTime: Boolean,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onChapterClicked: (Chapter) -> Unit,
|
onChapterClicked: (Chapter) -> Unit,
|
||||||
|
@ -805,7 +789,6 @@ private fun LazyListScope.sharedChapterItems(
|
||||||
contentType = { EntryScreenItem.ITEM },
|
contentType = { EntryScreenItem.ITEM },
|
||||||
) { item ->
|
) { item ->
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
when (item) {
|
when (item) {
|
||||||
is ChapterList.MissingCount -> {
|
is ChapterList.MissingCount -> {
|
||||||
|
@ -821,15 +804,7 @@ private fun LazyListScope.sharedChapterItems(
|
||||||
} else {
|
} else {
|
||||||
item.chapter.name
|
item.chapter.name
|
||||||
},
|
},
|
||||||
date = item.chapter.dateUpload
|
date = relativeDateText(item.chapter.dateUpload),
|
||||||
.takeIf { it > 0L }
|
|
||||||
?.let {
|
|
||||||
Date(it).toRelativeString(
|
|
||||||
context,
|
|
||||||
dateRelativeTime,
|
|
||||||
dateFormat,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
readProgress = item.chapter.lastPageRead
|
readProgress = item.chapter.lastPageRead
|
||||||
.takeIf { !item.chapter.read && it > 0L }
|
.takeIf { !item.chapter.read && it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
|
@ -842,7 +817,7 @@ private fun LazyListScope.sharedChapterItems(
|
||||||
read = item.chapter.read,
|
read = item.chapter.read,
|
||||||
bookmark = item.chapter.bookmark,
|
bookmark = item.chapter.bookmark,
|
||||||
selected = item.selected,
|
selected = item.selected,
|
||||||
downloadIndicatorEnabled = !isAnyChapterSelected,
|
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
|
||||||
downloadStateProvider = { item.downloadState },
|
downloadStateProvider = { item.downloadState },
|
||||||
downloadProgressProvider = { item.downloadProgress },
|
downloadProgressProvider = { item.downloadProgress },
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
|
|
|
@ -20,8 +20,8 @@ import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseMangaListItem(
|
fun BaseMangaListItem(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onClickItem: () -> Unit = {},
|
onClickItem: () -> Unit = {},
|
||||||
onClickCover: () -> Unit = onClickItem,
|
onClickCover: () -> Unit = onClickItem,
|
||||||
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
|
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
|
||||||
|
|
|
@ -45,10 +45,10 @@ enum class ChapterDownloadAction {
|
||||||
@Composable
|
@Composable
|
||||||
fun ChapterDownloadIndicator(
|
fun ChapterDownloadIndicator(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
downloadStateProvider: () -> MangaDownload.State,
|
downloadStateProvider: () -> MangaDownload.State,
|
||||||
downloadProgressProvider: () -> Int,
|
downloadProgressProvider: () -> Int,
|
||||||
onClick: (ChapterDownloadAction) -> Unit,
|
onClick: (ChapterDownloadAction) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (val downloadState = downloadStateProvider()) {
|
when (val downloadState = downloadStateProvider()) {
|
||||||
MangaDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
MangaDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
||||||
|
@ -105,10 +105,10 @@ private fun NotDownloadedIndicator(
|
||||||
@Composable
|
@Composable
|
||||||
private fun DownloadingIndicator(
|
private fun DownloadingIndicator(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
downloadState: MangaDownload.State,
|
downloadState: MangaDownload.State,
|
||||||
downloadProgressProvider: () -> Int,
|
downloadProgressProvider: () -> Int,
|
||||||
onClick: (ChapterDownloadAction) -> Unit,
|
onClick: (ChapterDownloadAction) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||||
Box(
|
Box(
|
||||||
|
|
|
@ -186,9 +186,9 @@ fun MangaChapterListItem(
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
if (readProgress != null || scanlator != null) DotSeparatorText()
|
|
||||||
}
|
}
|
||||||
if (readProgress != null) {
|
if (readProgress != null) {
|
||||||
|
DotSeparatorText()
|
||||||
Text(
|
Text(
|
||||||
text = readProgress,
|
text = readProgress,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
@ -197,6 +197,7 @@ fun MangaChapterListItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (scanlator != null) {
|
if (scanlator != null) {
|
||||||
|
DotSeparatorText()
|
||||||
Text(
|
Text(
|
||||||
text = scanlator,
|
text = scanlator,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
@ -207,19 +208,17 @@ fun MangaChapterListItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onDownloadClick != null) {
|
|
||||||
ChapterDownloadIndicator(
|
ChapterDownloadIndicator(
|
||||||
enabled = downloadIndicatorEnabled,
|
enabled = downloadIndicatorEnabled,
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
downloadStateProvider = downloadStateProvider,
|
downloadStateProvider = downloadStateProvider,
|
||||||
downloadProgressProvider = downloadProgressProvider,
|
downloadProgressProvider = downloadProgressProvider,
|
||||||
onClick = onDownloadClick,
|
onClick = { onDownloadClick?.invoke(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSwipeAction(
|
private fun getSwipeAction(
|
||||||
action: LibraryPreferences.ChapterSwipeAction,
|
action: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
@ -87,7 +88,8 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
import kotlin.math.absoluteValue
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||||
|
@ -166,7 +168,7 @@ fun MangaInfoBox(
|
||||||
fun MangaActionRow(
|
fun MangaActionRow(
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
fetchInterval: Int?,
|
nextUpdate: Instant?,
|
||||||
isUserIntervalMode: Boolean,
|
isUserIntervalMode: Boolean,
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
|
@ -178,6 +180,16 @@ fun MangaActionRow(
|
||||||
) {
|
) {
|
||||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||||
|
|
||||||
|
// TODO: show something better when using custom interval
|
||||||
|
val nextUpdateDays = remember(nextUpdate) {
|
||||||
|
return@remember if (nextUpdate != null) {
|
||||||
|
val now = Instant.now()
|
||||||
|
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = if (favorite) {
|
title = if (favorite) {
|
||||||
|
@ -190,18 +202,20 @@ fun MangaActionRow(
|
||||||
onClick = onAddToLibraryClicked,
|
onClick = onAddToLibraryClicked,
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = pluralStringResource(
|
title = when (nextUpdateDays) {
|
||||||
|
null -> stringResource(MR.strings.not_applicable)
|
||||||
|
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
|
||||||
|
else -> pluralStringResource(
|
||||||
MR.plurals.day,
|
MR.plurals.day,
|
||||||
count = fetchInterval.absoluteValue,
|
count = nextUpdateDays,
|
||||||
fetchInterval.absoluteValue,
|
nextUpdateDays,
|
||||||
),
|
)
|
||||||
|
},
|
||||||
icon = Icons.Default.HourglassEmpty,
|
icon = Icons.Default.HourglassEmpty,
|
||||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
onClick = onEditIntervalClicked,
|
onClick = { onEditIntervalClicked?.invoke() },
|
||||||
)
|
)
|
||||||
}
|
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = if (trackingCount == 0) {
|
title = if (trackingCount == 0) {
|
||||||
stringResource(MR.strings.manga_tracking_tab)
|
stringResource(MR.strings.manga_tracking_tab)
|
||||||
|
@ -287,7 +301,7 @@ fun ExpandableMangaDescription(
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
tags.forEach {
|
tags.forEach {
|
||||||
TagsChip(
|
TagsChip(
|
||||||
|
@ -303,7 +317,7 @@ fun ExpandableMangaDescription(
|
||||||
} else {
|
} else {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
items(items = tags) {
|
items(items = tags) {
|
||||||
TagsChip(
|
TagsChip(
|
||||||
|
@ -406,7 +420,7 @@ private fun MangaAndSourceTitlesSmall(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MangaContentInfo(
|
private fun ColumnScope.MangaContentInfo(
|
||||||
title: String,
|
title: String,
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
author: String?,
|
author: String?,
|
||||||
|
@ -438,7 +452,7 @@ private fun MangaContentInfo(
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -469,7 +483,7 @@ private fun MangaContentInfo(
|
||||||
if (!artist.isNullOrBlank() && author != artist) {
|
if (!artist.isNullOrBlank() && author != artist) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.presentation.history
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -11,10 +12,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ fun HistoryDeleteDialog(
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
val subtitle = if (isManga) {
|
val subtitle = if (isManga) {
|
||||||
MR.strings.dialog_with_checkbox_remove_description
|
MR.strings.dialog_with_checkbox_remove_description
|
||||||
|
@ -42,7 +43,11 @@ fun HistoryDeleteDialog(
|
||||||
Text(text = stringResource(subtitle))
|
Text(text = stringResource(subtitle))
|
||||||
|
|
||||||
LabeledCheckbox(
|
LabeledCheckbox(
|
||||||
label = stringResource(MR.strings.dialog_with_checkbox_reset),
|
label = if (isManga) {
|
||||||
|
stringResource(MR.strings.dialog_with_checkbox_reset)
|
||||||
|
} else {
|
||||||
|
stringResource(MR.strings.dialog_with_checkbox_reset_anime)
|
||||||
|
},
|
||||||
checked = removeEverything,
|
checked = removeEverything,
|
||||||
onCheckedChange = { removeEverything = it },
|
onCheckedChange = { removeEverything = it },
|
||||||
)
|
)
|
|
@ -6,24 +6,20 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.components.RelativeDateHeader
|
|
||||||
import eu.kanade.presentation.history.anime.components.AnimeHistoryItem
|
import eu.kanade.presentation.history.anime.components.AnimeHistoryItem
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
|
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
|
||||||
import tachiyomi.core.preference.InMemoryPreferenceStore
|
|
||||||
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
|
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -33,7 +29,6 @@ fun AnimeHistoryScreen(
|
||||||
onClickCover: (animeId: Long) -> Unit,
|
onClickCover: (animeId: Long) -> Unit,
|
||||||
onClickResume: (animeId: Long, episodeId: Long) -> Unit,
|
onClickResume: (animeId: Long, episodeId: Long) -> Unit,
|
||||||
onDialogChange: (AnimeHistoryScreenModel.Dialog?) -> Unit,
|
onDialogChange: (AnimeHistoryScreenModel.Dialog?) -> Unit,
|
||||||
preferences: UiPreferences = Injekt.get(),
|
|
||||||
searchQuery: String? = null,
|
searchQuery: String? = null,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -53,17 +48,12 @@ fun AnimeHistoryScreen(
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
AnimeHistoryContent(
|
AnimeHistoryScreenContent(
|
||||||
history = it,
|
history = it,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onClickCover = { history -> onClickCover(history.animeId) },
|
onClickCover = { history -> onClickCover(history.animeId) },
|
||||||
onClickResume = { history -> onClickResume(history.animeId, history.episodeId) },
|
onClickResume = { history -> onClickResume(history.animeId, history.episodeId) },
|
||||||
onClickDelete = { item ->
|
onClickDelete = { item -> onDialogChange(AnimeHistoryScreenModel.Dialog.Delete(item)) },
|
||||||
onDialogChange(
|
|
||||||
AnimeHistoryScreenModel.Dialog.Delete(item),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
preferences = preferences,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,17 +61,13 @@ fun AnimeHistoryScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AnimeHistoryContent(
|
private fun AnimeHistoryScreenContent(
|
||||||
history: List<AnimeHistoryUiModel>,
|
history: List<AnimeHistoryUiModel>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickCover: (AnimeHistoryWithRelations) -> Unit,
|
onClickCover: (AnimeHistoryWithRelations) -> Unit,
|
||||||
onClickResume: (AnimeHistoryWithRelations) -> Unit,
|
onClickResume: (AnimeHistoryWithRelations) -> Unit,
|
||||||
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
||||||
preferences: UiPreferences,
|
|
||||||
) {
|
) {
|
||||||
val relativeTime = remember { preferences.relativeTime().get() }
|
|
||||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
|
@ -97,11 +83,9 @@ private fun AnimeHistoryContent(
|
||||||
) { item ->
|
) { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is AnimeHistoryUiModel.Header -> {
|
is AnimeHistoryUiModel.Header -> {
|
||||||
RelativeDateHeader(
|
ListGroupHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
date = item.date,
|
text = relativeDateText(item.date),
|
||||||
relativeTime = relativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AnimeHistoryUiModel.Item -> {
|
is AnimeHistoryUiModel.Item -> {
|
||||||
|
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
|
||||||
onClickCover = {},
|
onClickCover = {},
|
||||||
onClickResume = { _, _ -> run {} },
|
onClickResume = { _, _ -> run {} },
|
||||||
onDialogChange = {},
|
onDialogChange = {},
|
||||||
preferences = UiPreferences(
|
|
||||||
InMemoryPreferenceStore(
|
|
||||||
sequenceOf(
|
|
||||||
InMemoryPreferenceStore.InMemoryPreference(
|
|
||||||
key = "relative_time_v2",
|
|
||||||
data = false,
|
|
||||||
defaultValue = false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,24 +6,20 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.components.RelativeDateHeader
|
|
||||||
import eu.kanade.presentation.history.manga.components.MangaHistoryItem
|
import eu.kanade.presentation.history.manga.components.MangaHistoryItem
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
|
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
|
||||||
import tachiyomi.core.preference.InMemoryPreferenceStore
|
|
||||||
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
|
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -33,7 +29,6 @@ fun MangaHistoryScreen(
|
||||||
onClickCover: (mangaId: Long) -> Unit,
|
onClickCover: (mangaId: Long) -> Unit,
|
||||||
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
||||||
onDialogChange: (MangaHistoryScreenModel.Dialog?) -> Unit,
|
onDialogChange: (MangaHistoryScreenModel.Dialog?) -> Unit,
|
||||||
preferences: UiPreferences = Injekt.get(),
|
|
||||||
searchQuery: String? = null,
|
searchQuery: String? = null,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -53,17 +48,12 @@ fun MangaHistoryScreen(
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MangaHistoryContent(
|
MangaHistoryScreenContent(
|
||||||
history = it,
|
history = it,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onClickCover = { history -> onClickCover(history.mangaId) },
|
onClickCover = { history -> onClickCover(history.mangaId) },
|
||||||
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
||||||
onClickDelete = { item ->
|
onClickDelete = { item -> onDialogChange(MangaHistoryScreenModel.Dialog.Delete(item)) },
|
||||||
onDialogChange(
|
|
||||||
MangaHistoryScreenModel.Dialog.Delete(item),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
preferences = preferences,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,17 +61,13 @@ fun MangaHistoryScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MangaHistoryContent(
|
private fun MangaHistoryScreenContent(
|
||||||
history: List<MangaHistoryUiModel>,
|
history: List<MangaHistoryUiModel>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickCover: (MangaHistoryWithRelations) -> Unit,
|
onClickCover: (MangaHistoryWithRelations) -> Unit,
|
||||||
onClickResume: (MangaHistoryWithRelations) -> Unit,
|
onClickResume: (MangaHistoryWithRelations) -> Unit,
|
||||||
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
||||||
preferences: UiPreferences,
|
|
||||||
) {
|
) {
|
||||||
val relativeTime = remember { preferences.relativeTime().get() }
|
|
||||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
|
@ -97,11 +83,9 @@ private fun MangaHistoryContent(
|
||||||
) { item ->
|
) { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is MangaHistoryUiModel.Header -> {
|
is MangaHistoryUiModel.Header -> {
|
||||||
RelativeDateHeader(
|
ListGroupHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
date = item.date,
|
text = relativeDateText(item.date),
|
||||||
relativeTime = relativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is MangaHistoryUiModel.Item -> {
|
is MangaHistoryUiModel.Item -> {
|
||||||
|
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
|
||||||
onClickCover = {},
|
onClickCover = {},
|
||||||
onClickResume = { _, _ -> run {} },
|
onClickResume = { _, _ -> run {} },
|
||||||
onDialogChange = {},
|
onDialogChange = {},
|
||||||
preferences = UiPreferences(
|
|
||||||
InMemoryPreferenceStore(
|
|
||||||
sequenceOf(
|
|
||||||
InMemoryPreferenceStore.InMemoryPreference(
|
|
||||||
key = "relative_time_v2",
|
|
||||||
data = false,
|
|
||||||
defaultValue = false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import eu.kanade.presentation.components.TabbedDialog
|
import eu.kanade.presentation.components.TabbedDialog
|
||||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||||
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibrarySettingsScreenModel
|
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibrarySettingsScreenModel
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.core.preference.TriState
|
import tachiyomi.core.preference.TriState
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
|
@ -74,6 +76,8 @@ private fun ColumnScope.FilterPage(
|
||||||
) {
|
) {
|
||||||
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedAnime().collectAsState()
|
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedAnime().collectAsState()
|
||||||
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
|
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
|
||||||
|
val autoUpdateAnimeRestrictions by screenModel.libraryPreferences.autoUpdateItemRestrictions().collectAsState()
|
||||||
|
|
||||||
TriStateItem(
|
TriStateItem(
|
||||||
label = stringResource(MR.strings.label_downloaded),
|
label = stringResource(MR.strings.label_downloaded),
|
||||||
state = if (downloadedOnly) {
|
state = if (downloadedOnly) {
|
||||||
|
@ -108,6 +112,18 @@ private fun ColumnScope.FilterPage(
|
||||||
state = filterCompleted,
|
state = filterCompleted,
|
||||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedAnime) },
|
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedAnime) },
|
||||||
)
|
)
|
||||||
|
// TODO: re-enable when custom intervals are ready for stable
|
||||||
|
if (
|
||||||
|
(isDevFlavor || isPreviewBuildType) &&
|
||||||
|
LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in autoUpdateAnimeRestrictions
|
||||||
|
) {
|
||||||
|
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
|
||||||
|
TriStateItem(
|
||||||
|
label = stringResource(MR.strings.action_filter_interval_custom),
|
||||||
|
state = filterIntervalCustom,
|
||||||
|
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val trackers = remember { screenModel.trackers }
|
val trackers = remember { screenModel.trackers }
|
||||||
when (trackers.size) {
|
when (trackers.size) {
|
||||||
|
|
|
@ -40,6 +40,7 @@ fun LibraryToolbar(
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onSearchQueryChange: (String?) -> Unit,
|
onSearchQueryChange: (String?) -> Unit,
|
||||||
scrollBehavior: TopAppBarScrollBehavior?,
|
scrollBehavior: TopAppBarScrollBehavior?,
|
||||||
|
navigateUp: (() -> Unit)? = null,
|
||||||
) = when {
|
) = when {
|
||||||
selectedCount > 0 -> LibrarySelectionToolbar(
|
selectedCount > 0 -> LibrarySelectionToolbar(
|
||||||
selectedCount = selectedCount,
|
selectedCount = selectedCount,
|
||||||
|
@ -57,6 +58,7 @@ fun LibraryToolbar(
|
||||||
onClickGlobalUpdate = onClickGlobalUpdate,
|
onClickGlobalUpdate = onClickGlobalUpdate,
|
||||||
onClickOpenRandomEntry = onClickOpenRandomEntry,
|
onClickOpenRandomEntry = onClickOpenRandomEntry,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigateUp = navigateUp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +73,7 @@ private fun LibraryRegularToolbar(
|
||||||
onClickGlobalUpdate: () -> Unit,
|
onClickGlobalUpdate: () -> Unit,
|
||||||
onClickOpenRandomEntry: () -> Unit,
|
onClickOpenRandomEntry: () -> Unit,
|
||||||
scrollBehavior: TopAppBarScrollBehavior?,
|
scrollBehavior: TopAppBarScrollBehavior?,
|
||||||
|
navigateUp: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
|
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
|
||||||
SearchToolbar(
|
SearchToolbar(
|
||||||
|
@ -119,6 +122,7 @@ private fun LibraryRegularToolbar(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigateUp = navigateUp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import eu.kanade.presentation.components.TabbedDialog
|
import eu.kanade.presentation.components.TabbedDialog
|
||||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||||
import eu.kanade.tachiyomi.ui.library.manga.MangaLibrarySettingsScreenModel
|
import eu.kanade.tachiyomi.ui.library.manga.MangaLibrarySettingsScreenModel
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.core.preference.TriState
|
import tachiyomi.core.preference.TriState
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
|
@ -74,6 +76,8 @@ private fun ColumnScope.FilterPage(
|
||||||
) {
|
) {
|
||||||
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedManga().collectAsState()
|
val filterDownloaded by screenModel.libraryPreferences.filterDownloadedManga().collectAsState()
|
||||||
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
|
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
|
||||||
|
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateItemRestrictions().collectAsState()
|
||||||
|
|
||||||
TriStateItem(
|
TriStateItem(
|
||||||
label = stringResource(MR.strings.label_downloaded),
|
label = stringResource(MR.strings.label_downloaded),
|
||||||
state = if (downloadedOnly) {
|
state = if (downloadedOnly) {
|
||||||
|
@ -108,6 +112,18 @@ private fun ColumnScope.FilterPage(
|
||||||
state = filterCompleted,
|
state = filterCompleted,
|
||||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedManga) },
|
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedManga) },
|
||||||
)
|
)
|
||||||
|
// TODO: re-enable when custom intervals are ready for stable
|
||||||
|
if (
|
||||||
|
(isDevFlavor || isPreviewBuildType) &&
|
||||||
|
LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions
|
||||||
|
) {
|
||||||
|
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
|
||||||
|
TriStateItem(
|
||||||
|
label = stringResource(MR.strings.action_filter_interval_custom),
|
||||||
|
state = filterIntervalCustom,
|
||||||
|
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val trackers = remember { screenModel.trackers }
|
val trackers = remember { screenModel.trackers }
|
||||||
when (trackers.size) {
|
when (trackers.size) {
|
||||||
|
|
|
@ -12,9 +12,7 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||||
import androidx.compose.material.icons.outlined.CloudOff
|
import androidx.compose.material.icons.outlined.CloudOff
|
||||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
|
||||||
import androidx.compose.material.icons.outlined.GetApp
|
import androidx.compose.material.icons.outlined.GetApp
|
||||||
import androidx.compose.material.icons.outlined.History
|
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.QueryStats
|
import androidx.compose.material.icons.outlined.QueryStats
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
@ -25,19 +23,18 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import eu.kanade.domain.ui.model.NavStyle
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.core.Constants
|
import eu.kanade.tachiyomi.core.Constants
|
||||||
import eu.kanade.tachiyomi.ui.more.DownloadQueueState
|
import eu.kanade.tachiyomi.ui.more.DownloadQueueState
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MoreScreen(
|
fun MoreScreen(
|
||||||
|
@ -47,6 +44,7 @@ fun MoreScreen(
|
||||||
incognitoMode: Boolean,
|
incognitoMode: Boolean,
|
||||||
onIncognitoModeChange: (Boolean) -> Unit,
|
onIncognitoModeChange: (Boolean) -> Unit,
|
||||||
isFDroid: Boolean,
|
isFDroid: Boolean,
|
||||||
|
navStyle: NavStyle,
|
||||||
onClickAlt: () -> Unit,
|
onClickAlt: () -> Unit,
|
||||||
onClickDownloadQueue: () -> Unit,
|
onClickDownloadQueue: () -> Unit,
|
||||||
onClickCategories: () -> Unit,
|
onClickCategories: () -> Unit,
|
||||||
|
@ -107,23 +105,10 @@ fun MoreScreen(
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider() }
|
||||||
|
|
||||||
val libraryPreferences: LibraryPreferences by injectLazy()
|
|
||||||
|
|
||||||
item {
|
item {
|
||||||
val bottomNavStyle = libraryPreferences.bottomNavStyle().get()
|
|
||||||
val titleRes = when (bottomNavStyle) {
|
|
||||||
0 -> MR.strings.label_recent_manga
|
|
||||||
1 -> MR.strings.label_recent_updates
|
|
||||||
else -> MR.strings.label_manga
|
|
||||||
}
|
|
||||||
val icon = when (bottomNavStyle) {
|
|
||||||
0 -> Icons.Outlined.History
|
|
||||||
1 -> ImageVector.vectorResource(id = R.drawable.ic_updates_outline_24dp)
|
|
||||||
else -> Icons.Outlined.CollectionsBookmark
|
|
||||||
}
|
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(titleRes),
|
title = navStyle.moreTab.options.title,
|
||||||
icon = icon,
|
icon = navStyle.moreIcon,
|
||||||
onPreferenceClick = onClickAlt,
|
onPreferenceClick = onClickAlt,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -148,6 +133,7 @@ fun MoreScreen(
|
||||||
}"
|
}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadQueueState.Downloading -> {
|
is DownloadQueueState.Downloading -> {
|
||||||
val pending = downloadQueueState.pending
|
val pending = downloadQueueState.pending
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
|
|
|
@ -17,7 +17,7 @@ import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import com.halilibo.richtext.markdown.Markdown
|
import com.halilibo.richtext.markdown.Markdown
|
||||||
import com.halilibo.richtext.ui.RichTextStyle
|
import com.halilibo.richtext.ui.RichTextStyle
|
||||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
import com.halilibo.richtext.ui.material3.RichText
|
||||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
@ -42,7 +42,7 @@ fun NewUpdateScreen(
|
||||||
rejectText = stringResource(MR.strings.action_not_now),
|
rejectText = stringResource(MR.strings.action_not_now),
|
||||||
onRejectClick = onRejectUpdate,
|
onRejectClick = onRejectUpdate,
|
||||||
) {
|
) {
|
||||||
Material3RichText(
|
RichText(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = MaterialTheme.padding.large),
|
.padding(vertical = MaterialTheme.padding.large),
|
||||||
|
@ -59,7 +59,7 @@ fun NewUpdateScreen(
|
||||||
modifier = Modifier.padding(top = MaterialTheme.padding.small),
|
modifier = Modifier.padding(top = MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(MR.strings.update_check_open))
|
Text(text = stringResource(MR.strings.update_check_open))
|
||||||
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
|
Spacer(modifier = Modifier.width(MaterialTheme.padding.extraSmall))
|
||||||
Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
|
Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,17 +15,22 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
|
internal class GuidesStep(
|
||||||
|
private val onRestoreBackup: () -> Unit,
|
||||||
|
) : OnboardingStep {
|
||||||
|
|
||||||
|
override val isComplete: Boolean = true
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun GuidesStep(
|
override fun Content() {
|
||||||
onRestoreBackup: () -> Unit,
|
|
||||||
) {
|
|
||||||
val handler = LocalUriHandler.current
|
val handler = LocalUriHandler.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
|
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
|
||||||
Button(
|
Button(
|
||||||
|
@ -36,6 +41,7 @@ internal fun GuidesStep(
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,6 +54,7 @@ internal fun GuidesStep(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const val GETTING_STARTED_URL = "https://aniyomi.org/docs/guides/getting-started"
|
const val GETTING_STARTED_URL = "https://aniyomi.org/docs/guides/getting-started"
|
||||||
|
|
||||||
|
@ -57,6 +64,6 @@ private fun GuidesStepPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiTheme {
|
||||||
GuidesStep(
|
GuidesStep(
|
||||||
onRestoreBackup = {},
|
onRestoreBackup = {},
|
||||||
)
|
).Content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,15 +13,12 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
import soup.compose.material.motion.animation.materialSharedAxisX
|
||||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
@ -29,24 +26,21 @@ import tachiyomi.presentation.core.screens.InfoScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OnboardingScreen(
|
fun OnboardingScreen(
|
||||||
storagePreferences: StoragePreferences,
|
|
||||||
uiPreferences: UiPreferences,
|
|
||||||
onComplete: () -> Unit,
|
onComplete: () -> Unit,
|
||||||
onRestoreBackup: () -> Unit,
|
onRestoreBackup: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val slideDistance = rememberSlideDistance()
|
val slideDistance = rememberSlideDistance()
|
||||||
|
|
||||||
var currentStep by remember { mutableIntStateOf(0) }
|
var currentStep by rememberSaveable { mutableIntStateOf(0) }
|
||||||
val steps: List<@Composable () -> Unit> = remember {
|
val steps = remember {
|
||||||
listOf(
|
listOf(
|
||||||
{ ThemeStep(uiPreferences = uiPreferences) },
|
ThemeStep(),
|
||||||
{ StorageStep(storagePref = storagePreferences.baseStorageDirectory()) },
|
StorageStep(),
|
||||||
// TODO: prompt for notification permissions when bumping target to Android 13
|
PermissionStep(),
|
||||||
{ GuidesStep(onRestoreBackup = onRestoreBackup) },
|
GuidesStep(onRestoreBackup = onRestoreBackup),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val isLastStep = currentStep == steps.size - 1
|
val isLastStep = currentStep == steps.lastIndex
|
||||||
|
|
||||||
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
|
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
|
||||||
|
|
||||||
|
@ -61,17 +55,13 @@ fun OnboardingScreen(
|
||||||
MR.strings.onboarding_action_next
|
MR.strings.onboarding_action_next
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
canAccept = steps[currentStep].isComplete,
|
||||||
onAcceptClick = {
|
onAcceptClick = {
|
||||||
if (isLastStep) {
|
if (isLastStep) {
|
||||||
onComplete()
|
onComplete()
|
||||||
} else {
|
|
||||||
// TODO: this is kind of janky
|
|
||||||
if (currentStep == 1 && !storagePreferences.baseStorageDirectory().isSet()) {
|
|
||||||
context.toast(MR.strings.onboarding_storage_selection_required)
|
|
||||||
} else {
|
} else {
|
||||||
currentStep++
|
currentStep++
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
|
@ -91,7 +81,7 @@ fun OnboardingScreen(
|
||||||
},
|
},
|
||||||
label = "stepContent",
|
label = "stepContent",
|
||||||
) {
|
) {
|
||||||
steps[it]()
|
steps[it].Content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,28 +5,53 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
|
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
|
||||||
|
import eu.kanade.tachiyomi.util.system.isTvBox
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import tachiyomi.core.preference.Preference
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import tachiyomi.core.storage.AndroidStorageFolderProvider
|
||||||
|
import tachiyomi.domain.storage.service.StoragePreferences
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Button
|
import tachiyomi.presentation.core.components.material.Button
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
internal class StorageStep : OnboardingStep {
|
||||||
|
|
||||||
|
private val storagePref = Injekt.get<StoragePreferences>().baseStorageDirectory()
|
||||||
|
private val folderProvider = Injekt.get<AndroidStorageFolderProvider>()
|
||||||
|
|
||||||
|
private var _isComplete by mutableStateOf(false)
|
||||||
|
|
||||||
|
override val isComplete: Boolean
|
||||||
|
get() = _isComplete
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun StorageStep(
|
override fun Content() {
|
||||||
storagePref: Preference<String>,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val handler = LocalUriHandler.current
|
||||||
|
|
||||||
|
val isTvBox = isTvBox(LocalContext.current)
|
||||||
|
|
||||||
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
|
@ -36,6 +61,22 @@ internal fun StorageStep(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isTvBox) {
|
||||||
|
if (!storagePref.isSet()) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = {
|
||||||
|
val storage = folderProvider.directory()
|
||||||
|
if (!storage.exists()) {
|
||||||
|
storage.mkdirs()
|
||||||
|
}
|
||||||
|
storagePref.set(storagePref.get())
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(stringResource(MR.strings.onboarding_storage_action_create_folder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -49,4 +90,24 @@ internal fun StorageStep(
|
||||||
Text(stringResource(MR.strings.onboarding_storage_action_select))
|
Text(stringResource(MR.strings.onboarding_storage_action_select))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(stringResource(MR.strings.onboarding_storage_help_info, stringResource(MR.strings.app_name)))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = { handler.openUri(SettingsDataScreen.HELP_URL) },
|
||||||
|
) {
|
||||||
|
Text(stringResource(MR.strings.onboarding_storage_help_action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
storagePref.changes()
|
||||||
|
.collectLatest { _isComplete = storagePref.isSet() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,17 @@ import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
||||||
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
internal class ThemeStep : OnboardingStep {
|
||||||
|
|
||||||
|
override val isComplete: Boolean = true
|
||||||
|
|
||||||
|
private val uiPreferences: UiPreferences = Injekt.get()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ThemeStep(
|
override fun Content() {
|
||||||
uiPreferences: UiPreferences,
|
|
||||||
) {
|
|
||||||
val themeModePref = uiPreferences.themeMode()
|
val themeModePref = uiPreferences.themeMode()
|
||||||
val themeMode by themeModePref.collectAsState()
|
val themeMode by themeModePref.collectAsState()
|
||||||
|
|
||||||
|
@ -38,3 +44,4 @@ internal fun ThemeStep(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.core.preference.Preference as PreferenceData
|
import tachiyomi.core.preference.Preference as PreferenceData
|
||||||
|
@ -64,13 +66,13 @@ sealed class Preference {
|
||||||
val pref: PreferenceData<T>,
|
val pref: PreferenceData<T>,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val subtitle: String? = "%s",
|
override val subtitle: String? = "%s",
|
||||||
val subtitleProvider: @Composable (value: T, entries: Map<T, String>) -> String? =
|
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
|
||||||
{ v, e -> subtitle?.format(e[v]) },
|
{ v, e -> subtitle?.format(e[v]) },
|
||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
||||||
|
|
||||||
val entries: Map<T, String>,
|
val entries: ImmutableMap<T, String>,
|
||||||
) : PreferenceItem<T>() {
|
) : PreferenceItem<T>() {
|
||||||
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
||||||
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(
|
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(
|
||||||
|
@ -78,8 +80,8 @@ sealed class Preference {
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun internalSubtitleProvider(value: Any?, entries: Map<out Any?, String>) =
|
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
|
||||||
subtitleProvider(value as T, entries as Map<T, String>)
|
subtitleProvider(value as T, entries as ImmutableMap<T, String>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,13 +91,13 @@ sealed class Preference {
|
||||||
val value: String,
|
val value: String,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val subtitle: String? = "%s",
|
override val subtitle: String? = "%s",
|
||||||
val subtitleProvider: @Composable (value: String, entries: Map<String, String>) -> String? =
|
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
|
||||||
{ v, e -> subtitle?.format(e[v]) },
|
{ v, e -> subtitle?.format(e[v]) },
|
||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||||
|
|
||||||
val entries: Map<String, String>,
|
val entries: ImmutableMap<String, String>,
|
||||||
) : PreferenceItem<String>()
|
) : PreferenceItem<String>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,7 +108,10 @@ sealed class Preference {
|
||||||
val pref: PreferenceData<Set<String>>,
|
val pref: PreferenceData<Set<String>>,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val subtitle: String? = "%s",
|
override val subtitle: String? = "%s",
|
||||||
val subtitleProvider: @Composable (value: Set<String>, entries: Map<String, String>) -> String? = { v, e ->
|
val subtitleProvider: @Composable (
|
||||||
|
value: Set<String>,
|
||||||
|
entries: ImmutableMap<String, String>,
|
||||||
|
) -> String? = { v, e ->
|
||||||
val combined = remember(v) {
|
val combined = remember(v) {
|
||||||
v.map { e[it] }
|
v.map { e[it] }
|
||||||
.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
|
@ -118,7 +123,7 @@ sealed class Preference {
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
||||||
|
|
||||||
val entries: Map<String, String>,
|
val entries: ImmutableMap<String, String>,
|
||||||
) : PreferenceItem<Set<String>>()
|
) : PreferenceItem<Set<String>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -184,6 +189,6 @@ sealed class Preference {
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
|
|
||||||
val preferenceItems: List<PreferenceItem<out Any>>,
|
val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
|
||||||
) : Preference()
|
) : Preference()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
|
||||||
import tachiyomi.presentation.core.components.SliderItem
|
import tachiyomi.presentation.core.components.SliderItem
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -172,8 +171,8 @@ internal fun PreferenceItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Preference.PreferenceItem.TrackerPreference -> {
|
is Preference.PreferenceItem.TrackerPreference -> {
|
||||||
val uName by Injekt.get<PreferenceStore>()
|
val uName by Injekt.get<TrackPreferences>()
|
||||||
.getString(TrackPreferences.trackUsername(item.tracker.id))
|
.trackUsername(item.tracker)
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
item.tracker.run {
|
item.tracker.run {
|
||||||
TrackingPreferenceWidget(
|
TrackingPreferenceWidget(
|
||||||
|
|
|
@ -8,6 +8,7 @@ import eu.kanade.core.preference.asState
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
||||||
import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding
|
import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -47,10 +48,15 @@ object AdvancedPlayerSettingsScreen : SearchableSettings {
|
||||||
postfix = if (mpvInput.asState(scope).value.lines().size > 2) "\n..." else "",
|
postfix = if (mpvInput.asState(scope).value.lines().size > 2) "\n..." else "",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
title = context.stringResource(MR.strings.pref_gpu_next_title),
|
||||||
|
subtitle = context.stringResource(MR.strings.pref_gpu_next_subtitle),
|
||||||
|
pref = playerPreferences.gpuNext(),
|
||||||
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
title = context.stringResource(MR.strings.pref_debanding_title),
|
title = context.stringResource(MR.strings.pref_debanding_title),
|
||||||
pref = playerPreferences.videoDebanding(),
|
pref = playerPreferences.videoDebanding(),
|
||||||
entries = VideoDebanding.entries.associateWith { context.stringResource(it.stringRes) }
|
entries = VideoDebanding.entries.associateWith { context.stringResource(it.stringRes) }.toImmutableMap()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||||
/**
|
/**
|
||||||
* Returns a string of categories name for settings subtitle
|
* Returns a string of categories name for settings subtitle
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
@Composable
|
@Composable
|
||||||
fun getCategoriesLabel(
|
fun getCategoriesLabel(
|
||||||
|
|
|
@ -24,6 +24,8 @@ import androidx.core.net.toUri
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
|
import eu.kanade.domain.extension.anime.interactor.TrustAnimeExtension
|
||||||
|
import eu.kanade.domain.extension.manga.interactor.TrustMangaExtension
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.domain.source.service.SourcePreferences.DataSaver
|
import eu.kanade.domain.source.service.SourcePreferences.DataSaver
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
|
@ -32,9 +34,10 @@ import eu.kanade.presentation.more.settings.screen.advanced.ClearDatabaseScreen
|
||||||
import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen
|
import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen
|
||||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||||
|
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.library.anime.AnimeMetadataUpdateJob
|
import eu.kanade.tachiyomi.data.library.anime.AnimeMetadataUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.library.manga.MangaMetadataUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
||||||
|
@ -44,6 +47,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CONTROLD
|
import eu.kanade.tachiyomi.network.PREF_DOH_CONTROLD
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
|
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
|
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_LIBREDNS
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_MULLVAD
|
import eu.kanade.tachiyomi.network.PREF_DOH_MULLVAD
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA
|
import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
|
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
|
||||||
|
@ -51,12 +55,17 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
||||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||||
import eu.kanade.tachiyomi.util.system.powerManager
|
import eu.kanade.tachiyomi.util.system.powerManager
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
@ -159,7 +168,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_background_activity),
|
title = stringResource(MR.strings.label_background_activity),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_disable_battery_optimization),
|
title = stringResource(MR.strings.pref_disable_battery_optimization),
|
||||||
subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary),
|
subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary),
|
||||||
|
@ -200,7 +209,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_data),
|
title = stringResource(MR.strings.label_data),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_invalidate_download_cache),
|
title = stringResource(MR.strings.pref_invalidate_download_cache),
|
||||||
subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary),
|
subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary),
|
||||||
|
@ -236,7 +245,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_network),
|
title = stringResource(MR.strings.label_network),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_clear_cookies),
|
title = stringResource(MR.strings.pref_clear_cookies),
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -267,7 +276,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = networkPreferences.dohProvider(),
|
pref = networkPreferences.dohProvider(),
|
||||||
title = stringResource(MR.strings.pref_dns_over_https),
|
title = stringResource(MR.strings.pref_dns_over_https),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
-1 to stringResource(MR.strings.disabled),
|
-1 to stringResource(MR.strings.disabled),
|
||||||
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
||||||
PREF_DOH_GOOGLE to "Google",
|
PREF_DOH_GOOGLE to "Google",
|
||||||
|
@ -281,6 +290,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
PREF_DOH_CONTROLD to "Control D",
|
PREF_DOH_CONTROLD to "Control D",
|
||||||
PREF_DOH_NJALLA to "Njalla",
|
PREF_DOH_NJALLA to "Njalla",
|
||||||
PREF_DOH_SHECAN to "Shecan",
|
PREF_DOH_SHECAN to "Shecan",
|
||||||
|
PREF_DOH_LIBREDNS to "LibreDNS",
|
||||||
),
|
),
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
context.stringResource(MR.strings.requires_app_restart)
|
context.stringResource(MR.strings.requires_app_restart)
|
||||||
|
@ -317,16 +327,17 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_library),
|
title = stringResource(MR.strings.label_library),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_refresh_library_covers),
|
title = stringResource(MR.strings.pref_refresh_library_covers),
|
||||||
onClick = {
|
onClick = {
|
||||||
|
AnimeLibraryUpdateJob.startNow(context)
|
||||||
MangaLibraryUpdateJob.startNow(context)
|
MangaLibraryUpdateJob.startNow(context)
|
||||||
AnimeMetadataUpdateJob.startNow(context)
|
AnimeMetadataUpdateJob.startNow(context)
|
||||||
|
MangaMetadataUpdateJob.startNow(context)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
@ -358,6 +369,8 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val extensionInstallerPref = basePreferences.extensionInstaller()
|
val extensionInstallerPref = basePreferences.extensionInstaller()
|
||||||
var shizukuMissing by rememberSaveable { mutableStateOf(false) }
|
var shizukuMissing by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val trustAnimeExtension = remember { Injekt.get<TrustAnimeExtension>() }
|
||||||
|
val trustMangaExtension = remember { Injekt.get<TrustMangaExtension>() }
|
||||||
|
|
||||||
if (shizukuMissing) {
|
if (shizukuMissing) {
|
||||||
val dismiss = { shizukuMissing = false }
|
val dismiss = { shizukuMissing = false }
|
||||||
|
@ -388,12 +401,21 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
}
|
}
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_extensions),
|
title = stringResource(MR.strings.label_extensions),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = extensionInstallerPref,
|
pref = extensionInstallerPref,
|
||||||
title = stringResource(MR.strings.ext_installer_pref),
|
title = stringResource(MR.strings.ext_installer_pref),
|
||||||
entries = extensionInstallerPref.entries
|
entries = extensionInstallerPref.entries
|
||||||
.associateWith { stringResource(it.titleRes) },
|
.filter {
|
||||||
|
// TODO: allow private option in stable versions once URL handling is more fleshed out
|
||||||
|
if (isPreviewBuildType || isDevFlavor) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
it != BasePreferences.ExtensionInstaller.PRIVATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
|
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
|
||||||
!context.isShizukuInstalled
|
!context.isShizukuInstalled
|
||||||
|
@ -405,6 +427,14 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(MR.strings.ext_revoke_trust),
|
||||||
|
onClick = {
|
||||||
|
trustMangaExtension.revokeAll()
|
||||||
|
trustAnimeExtension.revokeAll()
|
||||||
|
context.toast(MR.strings.requires_app_restart)
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -416,12 +446,12 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
val dataSaver by sourcePreferences.dataSaver().collectAsState()
|
val dataSaver by sourcePreferences.dataSaver().collectAsState()
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.data_saver),
|
title = stringResource(MR.strings.data_saver),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = sourcePreferences.dataSaver(),
|
pref = sourcePreferences.dataSaver(),
|
||||||
title = stringResource(MR.strings.data_saver),
|
title = stringResource(MR.strings.data_saver),
|
||||||
subtitle = stringResource(MR.strings.data_saver_summary),
|
subtitle = stringResource(MR.strings.data_saver_summary),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
DataSaver.NONE to stringResource(MR.strings.disabled),
|
DataSaver.NONE to stringResource(MR.strings.disabled),
|
||||||
DataSaver.BANDWIDTH_HERO to stringResource(MR.strings.bandwidth_hero),
|
DataSaver.BANDWIDTH_HERO to stringResource(MR.strings.bandwidth_hero),
|
||||||
DataSaver.WSRV_NL to stringResource(MR.strings.wsrv),
|
DataSaver.WSRV_NL to stringResource(MR.strings.wsrv),
|
||||||
|
@ -462,7 +492,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
"80%",
|
"80%",
|
||||||
"90%",
|
"90%",
|
||||||
"95%",
|
"95%",
|
||||||
).associateBy { it.trimEnd('%').toInt() },
|
).associateBy { it.trimEnd('%').toInt() }.toPersistentMap(),
|
||||||
enabled = dataSaver != DataSaver.NONE,
|
enabled = dataSaver != DataSaver.NONE,
|
||||||
),
|
),
|
||||||
kotlin.run {
|
kotlin.run {
|
||||||
|
|
|
@ -1,34 +1,28 @@
|
||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.os.LocaleListCompat
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
|
import eu.kanade.domain.ui.model.NavStyle
|
||||||
|
import eu.kanade.domain.ui.model.StartScreen
|
||||||
import eu.kanade.domain.ui.model.TabletUiMode
|
import eu.kanade.domain.ui.model.TabletUiMode
|
||||||
import eu.kanade.domain.ui.model.ThemeMode
|
import eu.kanade.domain.ui.model.ThemeMode
|
||||||
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
|
import eu.kanade.presentation.more.settings.screen.appearance.AppLanguageScreen
|
||||||
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
||||||
import eu.kanade.tachiyomi.R
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.drop
|
|
||||||
import org.xmlpull.v1.XmlPullParser
|
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
|
@ -69,7 +63,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_theme),
|
title = stringResource(MR.strings.pref_category_theme),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.CustomPreference(
|
Preference.PreferenceItem.CustomPreference(
|
||||||
title = stringResource(MR.strings.pref_app_theme),
|
title = stringResource(MR.strings.pref_app_theme),
|
||||||
) {
|
) {
|
||||||
|
@ -107,13 +101,8 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
uiPreferences: UiPreferences,
|
uiPreferences: UiPreferences,
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val langs = remember { getLangs(context) }
|
|
||||||
var currentLanguage by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val now = remember { Instant.now().toEpochMilli() }
|
val now = remember { Instant.now().toEpochMilli() }
|
||||||
|
|
||||||
val dateFormat by uiPreferences.dateFormat().collectAsState()
|
val dateFormat by uiPreferences.dateFormat().collectAsState()
|
||||||
|
@ -121,67 +110,43 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
UiPreferences.dateFormat(dateFormat).format(now)
|
UiPreferences.dateFormat(dateFormat).format(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(currentLanguage) {
|
|
||||||
val locale = if (currentLanguage.isEmpty()) {
|
|
||||||
LocaleListCompat.getEmptyLocaleList()
|
|
||||||
} else {
|
|
||||||
LocaleListCompat.forLanguageTags(currentLanguage)
|
|
||||||
}
|
|
||||||
AppCompatDelegate.setApplicationLocales(locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
libraryPrefs.bottomNavStyle().changes()
|
|
||||||
.drop(1)
|
|
||||||
.collectLatest { value ->
|
|
||||||
HomeScreen.tabs = when (value) {
|
|
||||||
0 -> HomeScreen.tabsNoHistory
|
|
||||||
1 -> HomeScreen.tabsNoUpdates
|
|
||||||
else -> HomeScreen.tabsNoManga
|
|
||||||
}
|
|
||||||
(context as? Activity)?.let {
|
|
||||||
ActivityCompat.recreate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_display),
|
title = stringResource(MR.strings.pref_category_display),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
pref = libraryPrefs.bottomNavStyle(),
|
|
||||||
title = stringResource(MR.strings.pref_bottom_nav_style),
|
|
||||||
entries = mapOf(
|
|
||||||
0 to stringResource(MR.strings.pref_bottom_nav_no_history),
|
|
||||||
1 to stringResource(MR.strings.pref_bottom_nav_no_updates),
|
|
||||||
2 to stringResource(MR.strings.pref_bottom_nav_no_manga),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
pref = libraryPrefs.isDefaultHomeTabLibraryManga(),
|
|
||||||
title = stringResource(MR.strings.pref_default_home_tab_library),
|
|
||||||
enabled = libraryPrefs.bottomNavStyle().get() != 2,
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.BasicListPreference(
|
|
||||||
value = currentLanguage,
|
|
||||||
title = stringResource(MR.strings.pref_app_language),
|
title = stringResource(MR.strings.pref_app_language),
|
||||||
entries = langs,
|
onClick = { navigator.push(AppLanguageScreen()) },
|
||||||
onValueChanged = { newValue ->
|
|
||||||
currentLanguage = newValue
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = uiPreferences.tabletUiMode(),
|
pref = uiPreferences.tabletUiMode(),
|
||||||
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
||||||
entries = TabletUiMode.entries.associateWith { stringResource(it.titleRes) },
|
entries = TabletUiMode.entries
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
context.stringResource(MR.strings.requires_app_restart)
|
context.stringResource(MR.strings.requires_app_restart)
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
pref = uiPreferences.startScreen(),
|
||||||
|
title = stringResource(MR.strings.pref_start_screen),
|
||||||
|
entries = StartScreen.entries
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
|
onValueChanged = {
|
||||||
|
context.stringResource(MR.strings.requires_app_restart)
|
||||||
|
true
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
pref = uiPreferences.navStyle(),
|
||||||
|
title = "Navigation Style",
|
||||||
|
entries = NavStyle.entries
|
||||||
|
.associateWith { stringResource(it.titleRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
|
onValueChanged = { true },
|
||||||
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = uiPreferences.dateFormat(),
|
pref = uiPreferences.dateFormat(),
|
||||||
title = stringResource(MR.strings.pref_date_format),
|
title = stringResource(MR.strings.pref_date_format),
|
||||||
|
@ -189,7 +154,8 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
.associateWith {
|
.associateWith {
|
||||||
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
||||||
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
|
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
|
||||||
},
|
}
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = uiPreferences.relativeTime(),
|
pref = uiPreferences.relativeTime(),
|
||||||
|
@ -203,30 +169,6 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
private fun getLangs(context: Context): Map<String, String> {
|
|
||||||
val langs = mutableListOf<Pair<String, String>>()
|
|
||||||
val parser = context.resources.getXml(R.xml.locales_config)
|
|
||||||
var eventType = parser.eventType
|
|
||||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
|
||||||
if (eventType == XmlPullParser.START_TAG && parser.name == "locale") {
|
|
||||||
for (i in 0..<parser.attributeCount) {
|
|
||||||
if (parser.getAttributeName(i) == "name") {
|
|
||||||
val langTag = parser.getAttributeValue(i)
|
|
||||||
val displayName = LocaleHelper.getDisplayName(langTag)
|
|
||||||
if (displayName.isNotEmpty()) {
|
|
||||||
langs.add(Pair(langTag, displayName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
eventType = parser.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
langs.sortBy { it.second }
|
|
||||||
langs.add(0, Pair("", context.stringResource(MR.strings.label_default)))
|
|
||||||
|
|
||||||
return langs.toMap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DateFormats = listOf(
|
private val DateFormats = listOf(
|
||||||
|
|
|
@ -2,15 +2,23 @@ package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.AnimeExtensionReposScreen
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.MangaExtensionReposScreen
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
@ -23,11 +31,16 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||||
@Composable
|
@Composable
|
||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
||||||
|
val mangaReposCount by sourcePreferences.mangaExtensionRepos().collectAsState()
|
||||||
|
val animeReposCount by sourcePreferences.animeExtensionRepos().collectAsState()
|
||||||
|
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_sources),
|
title = stringResource(MR.strings.label_sources),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = sourcePreferences.hideInAnimeLibraryItems(),
|
pref = sourcePreferences.hideInAnimeLibraryItems(),
|
||||||
title = stringResource(MR.strings.pref_hide_in_anime_library_items),
|
title = stringResource(MR.strings.pref_hide_in_anime_library_items),
|
||||||
|
@ -36,11 +49,33 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||||
pref = sourcePreferences.hideInMangaLibraryItems(),
|
pref = sourcePreferences.hideInMangaLibraryItems(),
|
||||||
title = stringResource(MR.strings.pref_hide_in_manga_library_items),
|
title = stringResource(MR.strings.pref_hide_in_manga_library_items),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(MR.strings.label_anime_extension_repos),
|
||||||
|
subtitle = pluralStringResource(
|
||||||
|
MR.plurals.num_repos,
|
||||||
|
animeReposCount.size,
|
||||||
|
animeReposCount.size,
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
navigator.push(AnimeExtensionReposScreen())
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(MR.strings.label_manga_extension_repos),
|
||||||
|
subtitle = pluralStringResource(
|
||||||
|
MR.plurals.num_repos,
|
||||||
|
mangaReposCount.size,
|
||||||
|
mangaReposCount.size,
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
navigator.push(MangaExtensionReposScreen())
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_nsfw_content),
|
title = stringResource(MR.strings.pref_category_nsfw_content),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = sourcePreferences.showNsfwSource(),
|
pref = sourcePreferences.showNsfwSource(),
|
||||||
title = stringResource(MR.strings.pref_show_nsfw_source),
|
title = stringResource(MR.strings.pref_show_nsfw_source),
|
||||||
|
|
|
@ -4,63 +4,55 @@ import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||||
|
import androidx.compose.material3.SegmentedButton
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||||
|
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||||
|
import eu.kanade.presentation.more.settings.screen.data.StorageInfo
|
||||||
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
|
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.EpisodeCache
|
|
||||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
|
||||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.core.storage.displayablePath
|
||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.backup.service.BackupPreferences
|
import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import tachiyomi.domain.backup.service.FLAG_CATEGORIES
|
|
||||||
import tachiyomi.domain.backup.service.FLAG_CHAPTERS
|
|
||||||
import tachiyomi.domain.backup.service.FLAG_EXTENSIONS
|
|
||||||
import tachiyomi.domain.backup.service.FLAG_EXT_SETTINGS
|
|
||||||
import tachiyomi.domain.backup.service.FLAG_HISTORY
|
|
||||||
import tachiyomi.domain.backup.service.FLAG_SETTINGS
|
|
||||||
import tachiyomi.domain.backup.service.FLAG_TRACK
|
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
import tachiyomi.domain.storage.service.StoragePreferences
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
@ -71,21 +63,35 @@ import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
object SettingsDataScreen : SearchableSettings {
|
object SettingsDataScreen : SearchableSettings {
|
||||||
|
|
||||||
|
val restorePreferenceKeyString = MR.strings.label_backup
|
||||||
|
const val HELP_URL = "https://aniyomi.org/docs/faq/storage"
|
||||||
|
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
@Composable
|
@Composable
|
||||||
override fun getTitleRes() = MR.strings.label_data_storage
|
override fun getTitleRes() = MR.strings.label_data_storage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun RowScope.AppBarAction() {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
IconButton(onClick = { uriHandler.openUri(HELP_URL) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||||
|
contentDescription = stringResource(MR.strings.tracking_guide),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||||
val storagePreferences = Injekt.get<StoragePreferences>()
|
val storagePreferences = Injekt.get<StoragePreferences>()
|
||||||
|
|
||||||
return listOf(
|
return persistentListOf(
|
||||||
getStorageLocationPref(storagePreferences = storagePreferences),
|
getStorageLocationPref(storagePreferences = storagePreferences),
|
||||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
|
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
|
||||||
|
|
||||||
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
||||||
getDataGroup(backupPreferences = backupPreferences),
|
getDataGroup(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,8 +113,6 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
UniFile.fromUri(context, uri)?.let {
|
UniFile.fromUri(context, uri)?.let {
|
||||||
storageDirPref.set(it.uri.toString())
|
storageDirPref.set(it.uri.toString())
|
||||||
}
|
}
|
||||||
Injekt.get<AnimeDownloadCache>().invalidateCache()
|
|
||||||
Injekt.get<MangaDownloadCache>().invalidateCache()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,13 +124,13 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val storageDir by storageDirPref.collectAsState()
|
val storageDir by storageDirPref.collectAsState()
|
||||||
|
|
||||||
if (storageDir == storageDirPref.defaultValue()) {
|
if (!storageDirPref.isSet()) {
|
||||||
return stringResource(MR.strings.no_location_set)
|
return stringResource(MR.strings.no_location_set)
|
||||||
}
|
}
|
||||||
|
|
||||||
return remember(storageDir) {
|
return remember(storageDir) {
|
||||||
val file = UniFile.fromUri(context, storageDir.toUri())
|
val file = UniFile.fromUri(context, storageDir.toUri())
|
||||||
file?.filePath ?: file?.uri?.toString()
|
file?.displayablePath
|
||||||
} ?: stringResource(MR.strings.invalid_location, storageDir)
|
} ?: stringResource(MR.strings.invalid_location, storageDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,20 +157,75 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
@Composable
|
@Composable
|
||||||
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
|
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
||||||
|
|
||||||
|
val chooseBackup = rememberLauncherForActivityResult(
|
||||||
|
object : ActivityResultContracts.GetContent() {
|
||||||
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
|
val intent = super.createIntent(context, input)
|
||||||
|
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (it == null) {
|
||||||
|
context.toast(MR.strings.file_null_uri_error)
|
||||||
|
return@rememberLauncherForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.push(RestoreBackupScreen(it.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_backup),
|
title = stringResource(MR.strings.label_backup),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
// Manual actions
|
// Manual actions
|
||||||
getCreateBackupPref(),
|
Preference.PreferenceItem.CustomPreference(
|
||||||
getRestoreBackupPref(),
|
title = stringResource(restorePreferenceKeyString),
|
||||||
|
) {
|
||||||
|
BasePreferenceWidget(
|
||||||
|
subcomponent = {
|
||||||
|
MultiChoiceSegmentedButtonRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = PrefsHorizontalPadding),
|
||||||
|
) {
|
||||||
|
SegmentedButton(
|
||||||
|
checked = false,
|
||||||
|
onCheckedChange = { navigator.push(CreateBackupScreen()) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(0, 2),
|
||||||
|
) {
|
||||||
|
Text(stringResource(MR.strings.pref_create_backup))
|
||||||
|
}
|
||||||
|
SegmentedButton(
|
||||||
|
checked = false,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (!BackupRestoreJob.isRunning(context)) {
|
||||||
|
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||||
|
context.toast(MR.strings.restore_miui_warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
// no need to catch because it's wrapped with a chooser
|
||||||
|
chooseBackup.launch("*/*")
|
||||||
|
} else {
|
||||||
|
context.toast(MR.strings.restore_in_progress)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(1, 2),
|
||||||
|
) {
|
||||||
|
Text(stringResource(MR.strings.pref_restore_backup))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
// Automatic backups
|
// Automatic backups
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = backupPreferences.backupInterval(),
|
pref = backupPreferences.backupInterval(),
|
||||||
title = stringResource(MR.strings.pref_backup_interval),
|
title = stringResource(MR.strings.pref_backup_interval),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
0 to stringResource(MR.strings.off),
|
0 to stringResource(MR.strings.off),
|
||||||
6 to stringResource(MR.strings.update_6hour),
|
6 to stringResource(MR.strings.update_6hour),
|
||||||
12 to stringResource(MR.strings.update_12hour),
|
12 to stringResource(MR.strings.update_12hour),
|
||||||
|
@ -188,181 +247,44 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
|
private fun getDataGroup(): Preference.PreferenceGroup {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
|
||||||
return Preference.PreferenceItem.TextPreference(
|
|
||||||
title = stringResource(MR.strings.pref_create_backup),
|
|
||||||
subtitle = stringResource(MR.strings.pref_create_backup_summ),
|
|
||||||
onClick = { navigator.push(CreateBackupScreen()) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var error by remember { mutableStateOf<Any?>(null) }
|
|
||||||
if (error != null) {
|
|
||||||
val onDismissRequest = { error = null }
|
|
||||||
when (val err = error) {
|
|
||||||
is InvalidRestore -> {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
|
|
||||||
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
context.copyToClipboard(err.message, err.message)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_copy_to_clipboard))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(text = stringResource(MR.strings.action_ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is MissingRestoreComponents -> {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
val msg = buildString {
|
|
||||||
append(stringResource(MR.strings.backup_restore_content_full))
|
|
||||||
if (err.sources.isNotEmpty()) {
|
|
||||||
append("\n\n").append(
|
|
||||||
stringResource(MR.strings.backup_restore_missing_sources),
|
|
||||||
)
|
|
||||||
err.sources.joinTo(
|
|
||||||
this,
|
|
||||||
separator = "\n- ",
|
|
||||||
prefix = "\n- ",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (err.trackers.isNotEmpty()) {
|
|
||||||
append("\n\n").append(
|
|
||||||
stringResource(MR.strings.backup_restore_missing_trackers),
|
|
||||||
)
|
|
||||||
err.trackers.joinTo(
|
|
||||||
this,
|
|
||||||
separator = "\n- ",
|
|
||||||
prefix = "\n- ",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(text = msg)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
BackupRestoreJob.start(context, err.uri)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_restore))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> error = null // Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val chooseBackup = rememberLauncherForActivityResult(
|
|
||||||
object : ActivityResultContracts.GetContent() {
|
|
||||||
override fun createIntent(context: Context, input: String): Intent {
|
|
||||||
val intent = super.createIntent(context, input)
|
|
||||||
return Intent.createChooser(
|
|
||||||
intent,
|
|
||||||
context.stringResource(MR.strings.file_select_backup),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
if (it == null) {
|
|
||||||
context.stringResource(MR.strings.file_null_uri_error)
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
val results = try {
|
|
||||||
BackupFileValidator().validate(context, it)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
error = InvalidRestore(it, e.message.toString())
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
|
|
||||||
BackupRestoreJob.start(context, it)
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Preference.PreferenceItem.TextPreference(
|
|
||||||
title = stringResource(MR.strings.pref_restore_backup),
|
|
||||||
subtitle = stringResource(MR.strings.pref_restore_backup_summ),
|
|
||||||
onClick = {
|
|
||||||
if (!BackupRestoreJob.isRunning(context)) {
|
|
||||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
|
||||||
context.stringResource(MR.strings.restore_miui_warning, Toast.LENGTH_LONG)
|
|
||||||
}
|
|
||||||
// no need to catch because it's wrapped with a chooser
|
|
||||||
chooseBackup.launch("*/*")
|
|
||||||
} else {
|
|
||||||
context.stringResource(MR.strings.restore_in_progress)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getDataGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
|
||||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||||
|
|
||||||
val backupIntervalPref = backupPreferences.backupInterval()
|
|
||||||
val backupInterval by backupIntervalPref.collectAsState()
|
|
||||||
|
|
||||||
val chapterCache = remember { Injekt.get<ChapterCache>() }
|
val chapterCache = remember { Injekt.get<ChapterCache>() }
|
||||||
val episodeCache = remember { Injekt.get<EpisodeCache>() }
|
|
||||||
var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
|
var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
|
||||||
val cacheReadableMangaSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
|
val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
|
||||||
val cacheReadableAnimeSize = remember(cacheReadableSizeSema) { episodeCache.readableSize }
|
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_data),
|
title = stringResource(MR.strings.pref_storage_usage),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
getMangaStorageInfoPref(cacheReadableMangaSize),
|
Preference.PreferenceItem.CustomPreference(
|
||||||
getAnimeStorageInfoPref(cacheReadableAnimeSize),
|
title = stringResource(MR.strings.pref_storage_usage),
|
||||||
|
) {
|
||||||
|
BasePreferenceWidget(
|
||||||
|
subcomponent = {
|
||||||
|
StorageInfo(
|
||||||
|
modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_clear_chapter_cache),
|
title = stringResource(MR.strings.pref_clear_chapter_cache),
|
||||||
subtitle = stringResource(
|
subtitle = stringResource(MR.strings.used_cache, cacheReadableSize),
|
||||||
MR.strings.used_cache_both,
|
|
||||||
cacheReadableAnimeSize,
|
|
||||||
cacheReadableMangaSize,
|
|
||||||
),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launchNonCancellable {
|
scope.launchNonCancellable {
|
||||||
try {
|
try {
|
||||||
val deletedFiles = chapterCache.clear() + episodeCache.clear()
|
val deletedFiles = chapterCache.clear()
|
||||||
withUIContext {
|
withUIContext {
|
||||||
context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles))
|
context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles))
|
||||||
cacheReadableSizeSema++
|
cacheReadableSizeSema++
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
withUIContext { context.stringResource(MR.strings.cache_delete_error) }
|
withUIContext { context.toast(MR.strings.cache_delete_error) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -371,93 +293,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
pref = libraryPreferences.autoClearItemCache(),
|
pref = libraryPreferences.autoClearItemCache(),
|
||||||
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
|
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
|
||||||
pref = backupPreferences.backupFlags(),
|
|
||||||
enabled = backupInterval != 0,
|
|
||||||
title = stringResource(MR.strings.pref_backup_flags),
|
|
||||||
subtitle = stringResource(MR.strings.pref_backup_flags_summary),
|
|
||||||
entries = mapOf(
|
|
||||||
FLAG_CATEGORIES to stringResource(MR.strings.general_categories),
|
|
||||||
FLAG_CHAPTERS to stringResource(MR.strings.chapters_episodes),
|
|
||||||
FLAG_HISTORY to stringResource(MR.strings.history),
|
|
||||||
FLAG_TRACK to stringResource(MR.strings.track),
|
|
||||||
FLAG_SETTINGS to stringResource(MR.strings.settings),
|
|
||||||
FLAG_EXT_SETTINGS to stringResource(MR.strings.extension_settings),
|
|
||||||
FLAG_EXTENSIONS to stringResource(MR.strings.label_extensions),
|
|
||||||
),
|
|
||||||
onValueChanged = {
|
|
||||||
if (FLAG_SETTINGS in it || FLAG_EXT_SETTINGS in it) {
|
|
||||||
context.stringResource(MR.strings.backup_settings_warning, Toast.LENGTH_LONG)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun getMangaStorageInfoPref(
|
|
||||||
chapterCacheReadableSize: String,
|
|
||||||
): Preference.PreferenceItem.CustomPreference {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val available = remember {
|
|
||||||
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
|
|
||||||
}
|
}
|
||||||
val total = remember {
|
|
||||||
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Preference.PreferenceItem.CustomPreference(
|
|
||||||
title = stringResource(MR.strings.pref_manga_storage_usage),
|
|
||||||
) {
|
|
||||||
BasePreferenceWidget(
|
|
||||||
title = stringResource(MR.strings.pref_manga_storage_usage),
|
|
||||||
subcomponent = {
|
|
||||||
// TODO: downloads, SD cards, bar representation?, i18n
|
|
||||||
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
|
|
||||||
Text(text = "Available: $available / $total (chapter cache: $chapterCacheReadableSize)")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun getAnimeStorageInfoPref(
|
|
||||||
episodeCacheReadableSize: String,
|
|
||||||
): Preference.PreferenceItem.CustomPreference {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val available = remember {
|
|
||||||
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
|
|
||||||
}
|
|
||||||
val total = remember {
|
|
||||||
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Preference.PreferenceItem.CustomPreference(
|
|
||||||
title = stringResource(MR.strings.pref_anime_storage_usage),
|
|
||||||
) {
|
|
||||||
BasePreferenceWidget(
|
|
||||||
title = stringResource(MR.strings.pref_anime_storage_usage),
|
|
||||||
subcomponent = {
|
|
||||||
// TODO: downloads, SD cards, bar representation?, i18n
|
|
||||||
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
|
|
||||||
Text(text = "Available: $available / $total (Episode cache: $episodeCacheReadableSize)")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class MissingRestoreComponents(
|
|
||||||
val uri: Uri,
|
|
||||||
val sources: List<String>,
|
|
||||||
val trackers: List<String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class InvalidRestore(
|
|
||||||
val uri: Uri? = null,
|
|
||||||
val message: String,
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,24 +1,42 @@
|
||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.util.fastMap
|
import androidx.compose.ui.util.fastMap
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.presentation.category.visualName
|
import eu.kanade.presentation.category.visualName
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||||
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
|
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.OutlinedNumericChooser
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
|
@ -33,23 +51,59 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val getCategories = remember { Injekt.get<GetMangaCategories>() }
|
val getMangaCategories = remember { Injekt.get<GetMangaCategories>() }
|
||||||
val allCategories by getCategories.subscribe().collectAsState(
|
val allMangaCategories by getMangaCategories.subscribe().collectAsState(
|
||||||
initial = runBlocking { getCategories.await() },
|
initial = runBlocking { getMangaCategories.await() },
|
||||||
)
|
)
|
||||||
val getAnimeCategories = remember { Injekt.get<GetAnimeCategories>() }
|
val getAnimeCategories = remember { Injekt.get<GetAnimeCategories>() }
|
||||||
val allAnimeCategories by getAnimeCategories.subscribe().collectAsState(
|
val allAnimeCategories by getAnimeCategories.subscribe().collectAsState(
|
||||||
initial = runBlocking { getAnimeCategories.await() },
|
initial = runBlocking { getAnimeCategories.await() },
|
||||||
)
|
)
|
||||||
|
|
||||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||||
|
val speedLimit by downloadPreferences.downloadSpeedLimit().collectAsState()
|
||||||
|
var currentSpeedLimit by remember { mutableIntStateOf(speedLimit) }
|
||||||
|
var showDownloadLimitDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
if (showDownloadLimitDialog) {
|
||||||
|
DownloadLimitDialog(
|
||||||
|
initialValue = currentSpeedLimit,
|
||||||
|
onDismissRequest = { showDownloadLimitDialog = false },
|
||||||
|
onValueChanged = {
|
||||||
|
currentSpeedLimit = it
|
||||||
|
},
|
||||||
|
onConfirm = {
|
||||||
|
downloadPreferences.downloadSpeedLimit().set(currentSpeedLimit)
|
||||||
|
showDownloadLimitDialog = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val multithreadingDownload by downloadPreferences.multithreadingDownload().collectAsState()
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = downloadPreferences.downloadOnlyOverWifi(),
|
pref = downloadPreferences.downloadOnlyOverWifi(),
|
||||||
title = stringResource(MR.strings.connected_to_wifi),
|
title = stringResource(MR.strings.connected_to_wifi),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
pref = downloadPreferences.multithreadingDownload(),
|
||||||
|
title = stringResource(MR.strings.multi_thread_download),
|
||||||
|
subtitle = stringResource(MR.strings.multi_thread_download_summary),
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
pref = downloadPreferences.numberOfThreads(),
|
||||||
|
title = stringResource(MR.strings.multi_thread_download_threads_number),
|
||||||
|
subtitle = stringResource(MR.strings.multi_thread_download_threads_number_summary),
|
||||||
|
entries = (1..64).associateWith { it.toString() }.toImmutableMap(),
|
||||||
|
enabled = multithreadingDownload,
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(MR.strings.download_speed_limit),
|
||||||
|
subtitle = if (speedLimit == 0) {
|
||||||
|
stringResource(MR.strings.off)
|
||||||
|
} else {
|
||||||
|
"$speedLimit KiB/s"
|
||||||
|
},
|
||||||
|
onClick = { showDownloadLimitDialog = true },
|
||||||
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = downloadPreferences.saveChaptersAsCBZ(),
|
pref = downloadPreferences.saveChaptersAsCBZ(),
|
||||||
title = stringResource(MR.strings.save_chapter_as_cbz),
|
title = stringResource(MR.strings.save_chapter_as_cbz),
|
||||||
|
@ -62,17 +116,18 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = downloadPreferences.numberOfDownloads(),
|
pref = downloadPreferences.numberOfDownloads(),
|
||||||
title = stringResource(MR.strings.pref_download_slots),
|
title = stringResource(MR.strings.pref_download_slots),
|
||||||
entries = (1..5).associateWith { it.toString() },
|
entries = (1..5).associateWith { it.toString() }.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_slots_info)),
|
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_slots_info)),
|
||||||
getDeleteChaptersGroup(
|
getDeleteChaptersGroup(
|
||||||
downloadPreferences = downloadPreferences,
|
downloadPreferences = downloadPreferences,
|
||||||
categories = allCategories,
|
animeCategories = allAnimeCategories,
|
||||||
|
mangaCategories = allMangaCategories,
|
||||||
),
|
),
|
||||||
getAutoDownloadGroup(
|
getAutoDownloadGroup(
|
||||||
downloadPreferences = downloadPreferences,
|
downloadPreferences = downloadPreferences,
|
||||||
allCategories = allCategories,
|
|
||||||
allAnimeCategories = allAnimeCategories,
|
allAnimeCategories = allAnimeCategories,
|
||||||
|
allMangaCategories = allMangaCategories,
|
||||||
),
|
),
|
||||||
getDownloadAheadGroup(downloadPreferences = downloadPreferences),
|
getDownloadAheadGroup(downloadPreferences = downloadPreferences),
|
||||||
getExternalDownloaderGroup(
|
getExternalDownloaderGroup(
|
||||||
|
@ -85,11 +140,12 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
@Composable
|
@Composable
|
||||||
private fun getDeleteChaptersGroup(
|
private fun getDeleteChaptersGroup(
|
||||||
downloadPreferences: DownloadPreferences,
|
downloadPreferences: DownloadPreferences,
|
||||||
categories: List<Category>,
|
animeCategories: List<Category>,
|
||||||
|
mangaCategories: List<Category>,
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_delete_chapters),
|
title = stringResource(MR.strings.pref_category_delete_chapters),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = downloadPreferences.removeAfterMarkedAsRead(),
|
pref = downloadPreferences.removeAfterMarkedAsRead(),
|
||||||
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
|
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
|
||||||
|
@ -97,7 +153,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = downloadPreferences.removeAfterReadSlots(),
|
pref = downloadPreferences.removeAfterReadSlots(),
|
||||||
title = stringResource(MR.strings.pref_remove_after_read),
|
title = stringResource(MR.strings.pref_remove_after_read),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
-1 to stringResource(MR.strings.disabled),
|
-1 to stringResource(MR.strings.disabled),
|
||||||
0 to stringResource(MR.strings.last_read_chapter),
|
0 to stringResource(MR.strings.last_read_chapter),
|
||||||
1 to stringResource(MR.strings.second_to_last),
|
1 to stringResource(MR.strings.second_to_last),
|
||||||
|
@ -110,9 +166,13 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
pref = downloadPreferences.removeBookmarkedChapters(),
|
pref = downloadPreferences.removeBookmarkedChapters(),
|
||||||
title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
|
title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
|
||||||
),
|
),
|
||||||
|
getExcludedAnimeCategoriesPreference(
|
||||||
|
downloadPreferences = downloadPreferences,
|
||||||
|
categories = { animeCategories },
|
||||||
|
),
|
||||||
getExcludedCategoriesPreference(
|
getExcludedCategoriesPreference(
|
||||||
downloadPreferences = downloadPreferences,
|
downloadPreferences = downloadPreferences,
|
||||||
categories = { categories },
|
categories = { mangaCategories },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -126,15 +186,31 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = downloadPreferences.removeExcludeCategories(),
|
pref = downloadPreferences.removeExcludeCategories(),
|
||||||
title = stringResource(MR.strings.pref_remove_exclude_categories_manga),
|
title = stringResource(MR.strings.pref_remove_exclude_categories_manga),
|
||||||
entries = categories().associate { it.id.toString() to it.visualName },
|
entries = categories()
|
||||||
|
.associate { it.id.toString() to it.visualName }
|
||||||
|
.toImmutableMap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getExcludedAnimeCategoriesPreference(
|
||||||
|
downloadPreferences: DownloadPreferences,
|
||||||
|
categories: () -> List<Category>,
|
||||||
|
): Preference.PreferenceItem.MultiSelectListPreference {
|
||||||
|
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
|
pref = downloadPreferences.removeExcludeCategories(),
|
||||||
|
title = stringResource(MR.strings.pref_remove_exclude_categories_anime),
|
||||||
|
entries = categories()
|
||||||
|
.associate { it.id.toString() to it.visualName }
|
||||||
|
.toImmutableMap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getAutoDownloadGroup(
|
private fun getAutoDownloadGroup(
|
||||||
downloadPreferences: DownloadPreferences,
|
downloadPreferences: DownloadPreferences,
|
||||||
allCategories: List<Category>,
|
|
||||||
allAnimeCategories: List<Category>,
|
allAnimeCategories: List<Category>,
|
||||||
|
allMangaCategories: List<Category>,
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
val downloadNewEpisodesPref = downloadPreferences.downloadNewEpisodes()
|
val downloadNewEpisodesPref = downloadPreferences.downloadNewEpisodes()
|
||||||
val downloadNewEpisodeCategoriesPref = downloadPreferences.downloadNewEpisodeCategories()
|
val downloadNewEpisodeCategoriesPref = downloadPreferences.downloadNewEpisodeCategories()
|
||||||
|
@ -179,9 +255,9 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
TriStateListDialog(
|
TriStateListDialog(
|
||||||
title = stringResource(MR.strings.manga_categories),
|
title = stringResource(MR.strings.manga_categories),
|
||||||
message = stringResource(MR.strings.pref_download_new_categories_details),
|
message = stringResource(MR.strings.pref_download_new_categories_details),
|
||||||
items = allCategories,
|
items = allMangaCategories,
|
||||||
initialChecked = included.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
|
initialChecked = included.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } },
|
||||||
initialInversed = excluded.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
|
initialInversed = excluded.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } },
|
||||||
itemLabel = { it.visualName },
|
itemLabel = { it.visualName },
|
||||||
onDismissRequest = { showDialog = false },
|
onDismissRequest = { showDialog = false },
|
||||||
onValueChanged = { newIncluded, newExcluded ->
|
onValueChanged = { newIncluded, newExcluded ->
|
||||||
|
@ -198,7 +274,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_auto_download),
|
title = stringResource(MR.strings.pref_category_auto_download),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = downloadNewEpisodesPref,
|
pref = downloadNewEpisodesPref,
|
||||||
title = stringResource(MR.strings.pref_download_new_episodes),
|
title = stringResource(MR.strings.pref_download_new_episodes),
|
||||||
|
@ -220,7 +296,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.manga_categories),
|
title = stringResource(MR.strings.manga_categories),
|
||||||
subtitle = getCategoriesLabel(
|
subtitle = getCategoriesLabel(
|
||||||
allCategories = allCategories,
|
allCategories = allMangaCategories,
|
||||||
included = included,
|
included = included,
|
||||||
excluded = excluded,
|
excluded = excluded,
|
||||||
),
|
),
|
||||||
|
@ -237,36 +313,32 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.download_ahead),
|
title = stringResource(MR.strings.download_ahead),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
|
||||||
pref = downloadPreferences.autoDownloadWhileReading(),
|
|
||||||
title = stringResource(MR.strings.auto_download_while_reading),
|
|
||||||
entries = listOf(0, 2, 3, 5, 10).associateWith {
|
|
||||||
if (it == 0) {
|
|
||||||
stringResource(MR.strings.disabled)
|
|
||||||
} else {
|
|
||||||
pluralStringResource(
|
|
||||||
MR.plurals.next_unread_chapters,
|
|
||||||
count = it,
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = downloadPreferences.autoDownloadWhileWatching(),
|
pref = downloadPreferences.autoDownloadWhileWatching(),
|
||||||
title = stringResource(MR.strings.auto_download_while_watching),
|
title = stringResource(MR.strings.auto_download_while_watching),
|
||||||
entries = listOf(0, 2, 3, 5, 10).associateWith {
|
entries = listOf(0, 2, 3, 5, 10)
|
||||||
|
.associateWith {
|
||||||
if (it == 0) {
|
if (it == 0) {
|
||||||
stringResource(MR.strings.disabled)
|
stringResource(MR.strings.disabled)
|
||||||
} else {
|
} else {
|
||||||
pluralStringResource(
|
pluralStringResource(MR.plurals.next_unseen_episodes, count = it, it)
|
||||||
MR.plurals.next_unseen_episodes,
|
|
||||||
count = it,
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
.toImmutableMap(),
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
pref = downloadPreferences.autoDownloadWhileReading(),
|
||||||
|
title = stringResource(MR.strings.auto_download_while_reading),
|
||||||
|
entries = listOf(0, 2, 3, 5, 10)
|
||||||
|
.associateWith {
|
||||||
|
if (it == 0) {
|
||||||
|
stringResource(MR.strings.disabled)
|
||||||
|
} else {
|
||||||
|
pluralStringResource(MR.plurals.next_unread_chapters, count = it, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.InfoPreference(
|
Preference.PreferenceItem.InfoPreference(
|
||||||
stringResource(MR.strings.download_ahead_info),
|
stringResource(MR.strings.download_ahead_info),
|
||||||
|
@ -299,12 +371,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
.map { pm.getApplicationLabel(it.applicationInfo).toString() }
|
.map { pm.getApplicationLabel(it.applicationInfo).toString() }
|
||||||
|
|
||||||
val packageNamesMap: Map<String, String> =
|
val packageNamesMap: Map<String, String> =
|
||||||
packageNames.zip(packageNamesReadable)
|
mapOf("" to "None") + packageNames.zip(packageNamesReadable).toMap()
|
||||||
.toMap()
|
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_external_downloader),
|
title = stringResource(MR.strings.pref_category_external_downloader),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = useExternalDownloader,
|
pref = useExternalDownloader,
|
||||||
title = stringResource(MR.strings.pref_use_external_downloader),
|
title = stringResource(MR.strings.pref_use_external_downloader),
|
||||||
|
@ -312,9 +383,58 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = externalDownloaderPreference,
|
pref = externalDownloaderPreference,
|
||||||
title = stringResource(MR.strings.pref_external_downloader_selection),
|
title = stringResource(MR.strings.pref_external_downloader_selection),
|
||||||
entries = mapOf("" to "None") + packageNamesMap,
|
entries = packageNamesMap.toPersistentMap(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DownloadLimitDialog(
|
||||||
|
initialValue: Int,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onValueChanged: (newValue: Int) -> Unit,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = { Text(stringResource(MR.strings.download_speed_limit)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = MaterialTheme.padding.medium)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
OutlinedNumericChooser(
|
||||||
|
label = stringResource(MR.strings.download_speed_limit),
|
||||||
|
placeholder = "0",
|
||||||
|
suffix = "KiB/s",
|
||||||
|
value = initialValue,
|
||||||
|
step = 100,
|
||||||
|
min = 0,
|
||||||
|
onValueChanged = onValueChanged,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(text = stringResource(MR.strings.download_speed_limit_hint))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onConfirm()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.action_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||||
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoriesTab
|
import eu.kanade.tachiyomi.ui.category.CategoriesTab
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||||
|
@ -66,8 +69,8 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
libraryPreferences,
|
libraryPreferences,
|
||||||
),
|
),
|
||||||
getGlobalUpdateGroup(allCategories, allAnimeCategories, libraryPreferences),
|
getGlobalUpdateGroup(allCategories, allAnimeCategories, libraryPreferences),
|
||||||
getChapterSwipeActionsGroup(libraryPreferences),
|
|
||||||
getEpisodeSwipeActionsGroup(libraryPreferences),
|
getEpisodeSwipeActionsGroup(libraryPreferences),
|
||||||
|
getChapterSwipeActionsGroup(libraryPreferences),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +81,6 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
allAnimeCategories: List<Category>,
|
allAnimeCategories: List<Category>,
|
||||||
libraryPreferences: LibraryPreferences,
|
libraryPreferences: LibraryPreferences,
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
||||||
val userAnimeCategoriesCount = allAnimeCategories.filterNot(Category::isSystemCategory).size
|
val userAnimeCategoriesCount = allAnimeCategories.filterNot(Category::isSystemCategory).size
|
||||||
|
@ -96,13 +98,13 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
allAnimeCategories.fastMap { it.id.toInt() }
|
allAnimeCategories.fastMap { it.id.toInt() }
|
||||||
|
|
||||||
val mangaLabels = listOf(stringResource(MR.strings.default_category_summary)) +
|
val mangaLabels = listOf(stringResource(MR.strings.default_category_summary)) +
|
||||||
allCategories.fastMap { it.visualName(context) }
|
allCategories.fastMap { it.visualName }
|
||||||
val animeLabels = listOf(stringResource(MR.strings.default_category_summary)) +
|
val animeLabels = listOf(stringResource(MR.strings.default_category_summary)) +
|
||||||
allAnimeCategories.fastMap { it.visualName(context) }
|
allAnimeCategories.fastMap { it.visualName }
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.general_categories),
|
title = stringResource(MR.strings.general_categories),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.action_edit_anime_categories),
|
title = stringResource(MR.strings.action_edit_anime_categories),
|
||||||
subtitle = pluralStringResource(
|
subtitle = pluralStringResource(
|
||||||
|
@ -117,7 +119,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
title = stringResource(MR.strings.default_anime_category),
|
title = stringResource(MR.strings.default_anime_category),
|
||||||
subtitle = selectedAnimeCategory?.visualName
|
subtitle = selectedAnimeCategory?.visualName
|
||||||
?: stringResource(MR.strings.default_category_summary),
|
?: stringResource(MR.strings.default_category_summary),
|
||||||
entries = animeIds.zip(animeLabels).toMap(),
|
entries = animeIds.zip(animeLabels).toMap().toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.action_edit_manga_categories),
|
title = stringResource(MR.strings.action_edit_manga_categories),
|
||||||
|
@ -131,9 +133,8 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryPreferences.defaultMangaCategory(),
|
pref = libraryPreferences.defaultMangaCategory(),
|
||||||
title = stringResource(MR.strings.default_manga_category),
|
title = stringResource(MR.strings.default_manga_category),
|
||||||
subtitle = selectedCategory?.visualName
|
subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
|
||||||
?: stringResource(MR.strings.default_category_summary),
|
entries = mangaIds.zip(mangaLabels).toMap().toImmutableMap(),
|
||||||
entries = mangaIds.zip(mangaLabels).toMap(),
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = libraryPreferences.categorizedDisplaySettings(),
|
pref = libraryPreferences.categorizedDisplaySettings(),
|
||||||
|
@ -222,11 +223,11 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_library_update),
|
title = stringResource(MR.strings.pref_category_library_update),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = autoUpdateIntervalPref,
|
pref = autoUpdateIntervalPref,
|
||||||
title = stringResource(MR.strings.pref_library_update_interval),
|
title = stringResource(MR.strings.pref_library_update_interval),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
0 to stringResource(MR.strings.update_never),
|
0 to stringResource(MR.strings.update_never),
|
||||||
12 to stringResource(MR.strings.update_12hour),
|
12 to stringResource(MR.strings.update_12hour),
|
||||||
24 to stringResource(MR.strings.update_24hour),
|
24 to stringResource(MR.strings.update_24hour),
|
||||||
|
@ -245,7 +246,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
enabled = autoUpdateInterval > 0,
|
enabled = autoUpdateInterval > 0,
|
||||||
title = stringResource(MR.strings.pref_library_update_restriction),
|
title = stringResource(MR.strings.pref_library_update_restriction),
|
||||||
subtitle = stringResource(MR.strings.restrictions),
|
subtitle = stringResource(MR.strings.restrictions),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
|
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
|
||||||
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
|
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
|
||||||
DEVICE_CHARGING to stringResource(MR.strings.charging),
|
DEVICE_CHARGING to stringResource(MR.strings.charging),
|
||||||
|
@ -284,18 +285,12 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryPreferences.autoUpdateItemRestrictions(),
|
pref = libraryPreferences.autoUpdateItemRestrictions(),
|
||||||
title = stringResource(MR.strings.pref_library_update_manga_restriction),
|
title = stringResource(MR.strings.pref_library_update_smart_update),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
ENTRY_HAS_UNVIEWED to stringResource(
|
ENTRY_HAS_UNVIEWED to stringResource(MR.strings.pref_update_only_completely_read),
|
||||||
MR.strings.pref_update_only_completely_read,
|
|
||||||
),
|
|
||||||
ENTRY_NON_VIEWED to stringResource(MR.strings.pref_update_only_started),
|
ENTRY_NON_VIEWED to stringResource(MR.strings.pref_update_only_started),
|
||||||
ENTRY_NON_COMPLETED to stringResource(
|
ENTRY_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
|
||||||
MR.strings.pref_update_only_non_completed,
|
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
|
||||||
),
|
|
||||||
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(
|
|
||||||
MR.strings.pref_update_only_in_release_period,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
@ -312,41 +307,33 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_chapter_swipe),
|
title = stringResource(MR.strings.pref_chapter_swipe),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryPreferences.swipeChapterStartAction(),
|
pref = libraryPreferences.swipeChapterStartAction(),
|
||||||
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(
|
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||||
MR.strings.action_disable,
|
stringResource(MR.strings.disabled),
|
||||||
),
|
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
||||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(
|
stringResource(MR.strings.action_bookmark),
|
||||||
MR.strings.action_bookmark,
|
LibraryPreferences.ChapterSwipeAction.ToggleRead to
|
||||||
),
|
stringResource(MR.strings.action_mark_as_read),
|
||||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(
|
LibraryPreferences.ChapterSwipeAction.Download to
|
||||||
MR.strings.action_mark_as_read,
|
stringResource(MR.strings.action_download),
|
||||||
),
|
|
||||||
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
|
|
||||||
MR.strings.action_download,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryPreferences.swipeChapterEndAction(),
|
pref = libraryPreferences.swipeChapterEndAction(),
|
||||||
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(
|
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||||
MR.strings.action_disable,
|
stringResource(MR.strings.disabled),
|
||||||
),
|
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
||||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(
|
stringResource(MR.strings.action_bookmark),
|
||||||
MR.strings.action_bookmark,
|
LibraryPreferences.ChapterSwipeAction.ToggleRead to
|
||||||
),
|
stringResource(MR.strings.action_mark_as_read),
|
||||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(
|
LibraryPreferences.ChapterSwipeAction.Download to
|
||||||
MR.strings.action_mark_as_read,
|
stringResource(MR.strings.action_download),
|
||||||
),
|
|
||||||
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
|
|
||||||
MR.strings.action_download,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -359,41 +346,33 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_episode_swipe),
|
title = stringResource(MR.strings.pref_episode_swipe),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryPreferences.swipeEpisodeStartAction(),
|
pref = libraryPreferences.swipeEpisodeStartAction(),
|
||||||
title = stringResource(MR.strings.pref_episode_swipe_start),
|
title = stringResource(MR.strings.pref_episode_swipe_start),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(
|
LibraryPreferences.EpisodeSwipeAction.Disabled to
|
||||||
MR.strings.action_disable,
|
stringResource(MR.strings.disabled),
|
||||||
),
|
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
|
||||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(
|
stringResource(MR.strings.action_bookmark_episode),
|
||||||
MR.strings.action_bookmark_episode,
|
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
|
||||||
),
|
stringResource(MR.strings.action_mark_as_seen),
|
||||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource(
|
LibraryPreferences.EpisodeSwipeAction.Download to
|
||||||
MR.strings.action_mark_as_seen,
|
stringResource(MR.strings.action_download),
|
||||||
),
|
|
||||||
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
|
|
||||||
MR.strings.action_download,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryPreferences.swipeEpisodeEndAction(),
|
pref = libraryPreferences.swipeEpisodeEndAction(),
|
||||||
title = stringResource(MR.strings.pref_episode_swipe_end),
|
title = stringResource(MR.strings.pref_episode_swipe_end),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(
|
LibraryPreferences.EpisodeSwipeAction.Disabled to
|
||||||
MR.strings.action_disable,
|
stringResource(MR.strings.disabled),
|
||||||
),
|
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
|
||||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(
|
stringResource(MR.strings.action_bookmark_episode),
|
||||||
MR.strings.action_bookmark_episode,
|
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
|
||||||
),
|
stringResource(MR.strings.action_mark_as_seen),
|
||||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource(
|
LibraryPreferences.EpisodeSwipeAction.Download to
|
||||||
MR.strings.action_mark_as_seen,
|
stringResource(MR.strings.action_download),
|
||||||
),
|
|
||||||
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
|
|
||||||
MR.strings.action_download,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -186,18 +186,18 @@ object SettingsMainScreen : Screen() {
|
||||||
icon = Icons.Outlined.CollectionsBookmark,
|
icon = Icons.Outlined.CollectionsBookmark,
|
||||||
screen = SettingsLibraryScreen,
|
screen = SettingsLibraryScreen,
|
||||||
),
|
),
|
||||||
Item(
|
|
||||||
titleRes = MR.strings.pref_category_reader,
|
|
||||||
subtitleRes = MR.strings.pref_reader_summary,
|
|
||||||
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
|
|
||||||
screen = SettingsReaderScreen,
|
|
||||||
),
|
|
||||||
Item(
|
Item(
|
||||||
titleRes = MR.strings.pref_category_player,
|
titleRes = MR.strings.pref_category_player,
|
||||||
subtitleRes = MR.strings.pref_player_summary,
|
subtitleRes = MR.strings.pref_player_summary,
|
||||||
icon = Icons.Outlined.PlayCircleOutline,
|
icon = Icons.Outlined.PlayCircleOutline,
|
||||||
screen = SettingsPlayerScreen,
|
screen = SettingsPlayerScreen,
|
||||||
),
|
),
|
||||||
|
Item(
|
||||||
|
titleRes = MR.strings.pref_category_reader,
|
||||||
|
subtitleRes = MR.strings.pref_reader_summary,
|
||||||
|
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
|
||||||
|
screen = SettingsReaderScreen,
|
||||||
|
),
|
||||||
Item(
|
Item(
|
||||||
titleRes = MR.strings.pref_category_downloads,
|
titleRes = MR.strings.pref_category_downloads,
|
||||||
subtitleRes = MR.strings.pref_downloads_summary,
|
subtitleRes = MR.strings.pref_downloads_summary,
|
||||||
|
|
|
@ -33,7 +33,10 @@ import eu.kanade.tachiyomi.ui.player.VLC_PLAYER
|
||||||
import eu.kanade.tachiyomi.ui.player.WEB_VIDEO_CASTER
|
import eu.kanade.tachiyomi.ui.player.WEB_VIDEO_CASTER
|
||||||
import eu.kanade.tachiyomi.ui.player.X_PLAYER
|
import eu.kanade.tachiyomi.ui.player.X_PLAYER
|
||||||
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
@ -57,7 +60,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = playerPreferences.progressPreference(),
|
pref = playerPreferences.progressPreference(),
|
||||||
title = stringResource(MR.strings.pref_progress_mark_as_seen),
|
title = stringResource(MR.strings.pref_progress_mark_as_seen),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
1.00F to stringResource(MR.strings.pref_progress_100),
|
1.00F to stringResource(MR.strings.pref_progress_100),
|
||||||
0.95F to stringResource(MR.strings.pref_progress_95),
|
0.95F to stringResource(MR.strings.pref_progress_95),
|
||||||
0.90F to stringResource(MR.strings.pref_progress_90),
|
0.90F to stringResource(MR.strings.pref_progress_90),
|
||||||
|
@ -91,7 +94,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_internal_player),
|
title = stringResource(MR.strings.pref_category_internal_player),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = playerFullscreen,
|
pref = playerFullscreen,
|
||||||
title = stringResource(MR.strings.pref_player_fullscreen),
|
title = stringResource(MR.strings.pref_player_fullscreen),
|
||||||
|
@ -118,7 +121,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_volume_brightness),
|
title = stringResource(MR.strings.pref_category_volume_brightness),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = enableVolumeBrightnessGestures,
|
pref = enableVolumeBrightnessGestures,
|
||||||
title = stringResource(MR.strings.enable_volume_brightness_gestures),
|
title = stringResource(MR.strings.enable_volume_brightness_gestures),
|
||||||
|
@ -144,11 +147,11 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_player_orientation),
|
title = stringResource(MR.strings.pref_category_player_orientation),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = defaultPlayerOrientationType,
|
pref = defaultPlayerOrientationType,
|
||||||
title = stringResource(MR.strings.pref_default_player_orientation),
|
title = stringResource(MR.strings.pref_default_player_orientation),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR to stringResource(
|
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR to stringResource(
|
||||||
MR.strings.rotation_free,
|
MR.strings.rotation_free,
|
||||||
),
|
),
|
||||||
|
@ -179,7 +182,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = defaultPlayerOrientationPortrait,
|
pref = defaultPlayerOrientationPortrait,
|
||||||
title = stringResource(MR.strings.pref_default_portrait_orientation),
|
title = stringResource(MR.strings.pref_default_portrait_orientation),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT to stringResource(
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT to stringResource(
|
||||||
MR.strings.rotation_portrait,
|
MR.strings.rotation_portrait,
|
||||||
),
|
),
|
||||||
|
@ -194,7 +197,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = defaultPlayerOrientationLandscape,
|
pref = defaultPlayerOrientationLandscape,
|
||||||
title = stringResource(MR.strings.pref_default_landscape_orientation),
|
title = stringResource(MR.strings.pref_default_landscape_orientation),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE to stringResource(
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE to stringResource(
|
||||||
MR.strings.rotation_landscape,
|
MR.strings.rotation_landscape,
|
||||||
),
|
),
|
||||||
|
@ -241,7 +244,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_player_seeking),
|
title = stringResource(MR.strings.pref_category_player_seeking),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = enableHorizontalSeekGesture,
|
pref = enableHorizontalSeekGesture,
|
||||||
title = stringResource(MR.strings.enable_horizontal_seek_gesture),
|
title = stringResource(MR.strings.enable_horizontal_seek_gesture),
|
||||||
|
@ -254,7 +257,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = skipLengthPreference,
|
pref = skipLengthPreference,
|
||||||
title = stringResource(MR.strings.pref_skip_length),
|
title = stringResource(MR.strings.pref_skip_length),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
30 to stringResource(MR.strings.pref_skip_30),
|
30 to stringResource(MR.strings.pref_skip_30),
|
||||||
20 to stringResource(MR.strings.pref_skip_20),
|
20 to stringResource(MR.strings.pref_skip_20),
|
||||||
10 to stringResource(MR.strings.pref_skip_10),
|
10 to stringResource(MR.strings.pref_skip_10),
|
||||||
|
@ -293,7 +296,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = waitingTimeAniSkip,
|
pref = waitingTimeAniSkip,
|
||||||
title = stringResource(MR.strings.pref_waiting_time_aniskip),
|
title = stringResource(MR.strings.pref_waiting_time_aniskip),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
5 to stringResource(MR.strings.pref_waiting_time_aniskip_5),
|
5 to stringResource(MR.strings.pref_waiting_time_aniskip_5),
|
||||||
6 to stringResource(MR.strings.pref_waiting_time_aniskip_6),
|
6 to stringResource(MR.strings.pref_waiting_time_aniskip_6),
|
||||||
7 to stringResource(MR.strings.pref_waiting_time_aniskip_7),
|
7 to stringResource(MR.strings.pref_waiting_time_aniskip_7),
|
||||||
|
@ -318,7 +321,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_pip),
|
title = stringResource(MR.strings.pref_category_pip),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = enablePip,
|
pref = enablePip,
|
||||||
title = stringResource(MR.strings.pref_enable_pip),
|
title = stringResource(MR.strings.pref_enable_pip),
|
||||||
|
@ -364,7 +367,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_external_player),
|
title = stringResource(MR.strings.pref_category_external_player),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = alwaysUseExternalPlayer,
|
pref = alwaysUseExternalPlayer,
|
||||||
title = stringResource(MR.strings.pref_always_use_external_player),
|
title = stringResource(MR.strings.pref_always_use_external_player),
|
||||||
|
@ -372,7 +375,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = externalPlayerPreference,
|
pref = externalPlayerPreference,
|
||||||
title = stringResource(MR.strings.pref_external_player_preference),
|
title = stringResource(MR.strings.pref_external_player_preference),
|
||||||
entries = mapOf("" to "None") + packageNamesMap,
|
entries = (mapOf("" to "None") + packageNamesMap).toPersistentMap(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,6 +10,9 @@ import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
|
@ -31,12 +34,13 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
pref = readerPref.defaultReadingMode(),
|
pref = readerPref.defaultReadingMode(),
|
||||||
title = stringResource(MR.strings.pref_viewer_type),
|
title = stringResource(MR.strings.pref_viewer_type),
|
||||||
entries = ReadingMode.entries.drop(1)
|
entries = ReadingMode.entries.drop(1)
|
||||||
.associate { it.flagValue to stringResource(it.stringRes) },
|
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPref.doubleTapAnimSpeed(),
|
pref = readerPref.doubleTapAnimSpeed(),
|
||||||
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
1 to stringResource(MR.strings.double_tap_anim_speed_0),
|
1 to stringResource(MR.strings.double_tap_anim_speed_0),
|
||||||
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
|
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
|
||||||
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
|
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
|
||||||
|
@ -82,17 +86,18 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
val fullscreen by fullscreenPref.collectAsState()
|
val fullscreen by fullscreenPref.collectAsState()
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_display),
|
title = stringResource(MR.strings.pref_category_display),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.defaultOrientationType(),
|
pref = readerPreferences.defaultOrientationType(),
|
||||||
title = stringResource(MR.strings.pref_rotation_type),
|
title = stringResource(MR.strings.pref_rotation_type),
|
||||||
entries = ReaderOrientation.entries.drop(1)
|
entries = ReaderOrientation.entries.drop(1)
|
||||||
.associate { it.flagValue to stringResource(it.stringRes) },
|
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.readerTheme(),
|
pref = readerPreferences.readerTheme(),
|
||||||
title = stringResource(MR.strings.pref_reader_theme),
|
title = stringResource(MR.strings.pref_reader_theme),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
1 to stringResource(MR.strings.black_background),
|
1 to stringResource(MR.strings.black_background),
|
||||||
2 to stringResource(MR.strings.gray_background),
|
2 to stringResource(MR.strings.gray_background),
|
||||||
0 to stringResource(MR.strings.white_background),
|
0 to stringResource(MR.strings.white_background),
|
||||||
|
@ -126,7 +131,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_reading),
|
title = stringResource(MR.strings.pref_category_reading),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readerPreferences.skipRead(),
|
pref = readerPreferences.skipRead(),
|
||||||
title = stringResource(MR.strings.pref_skip_read_chapters),
|
title = stringResource(MR.strings.pref_skip_read_chapters),
|
||||||
|
@ -165,29 +170,26 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pager_viewer),
|
title = stringResource(MR.strings.pager_viewer),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = navModePref,
|
pref = navModePref,
|
||||||
title = stringResource(MR.strings.pref_viewer_nav),
|
title = stringResource(MR.strings.pref_viewer_nav),
|
||||||
entries = ReaderPreferences.TapZones
|
entries = ReaderPreferences.TapZones
|
||||||
.mapIndexed { index, it -> index to stringResource(it) }
|
.mapIndexed { index, it -> index to stringResource(it) }
|
||||||
.toMap(),
|
.toMap()
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.pagerNavInverted(),
|
pref = readerPreferences.pagerNavInverted(),
|
||||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||||
entries = mapOf(
|
entries = persistentListOf(
|
||||||
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none),
|
ReaderPreferences.TappingInvertMode.NONE,
|
||||||
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(
|
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||||
MR.strings.tapping_inverted_horizontal,
|
ReaderPreferences.TappingInvertMode.VERTICAL,
|
||||||
),
|
ReaderPreferences.TappingInvertMode.BOTH,
|
||||||
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(
|
)
|
||||||
MR.strings.tapping_inverted_vertical,
|
.associateWith { stringResource(it.titleRes) }
|
||||||
),
|
.toImmutableMap(),
|
||||||
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
|
|
||||||
MR.strings.tapping_inverted_both,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabled = navMode != 5,
|
enabled = navMode != 5,
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
|
@ -195,14 +197,16 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
title = stringResource(MR.strings.pref_image_scale_type),
|
title = stringResource(MR.strings.pref_image_scale_type),
|
||||||
entries = ReaderPreferences.ImageScaleType
|
entries = ReaderPreferences.ImageScaleType
|
||||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||||
.toMap(),
|
.toMap()
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.zoomStart(),
|
pref = readerPreferences.zoomStart(),
|
||||||
title = stringResource(MR.strings.pref_zoom_start),
|
title = stringResource(MR.strings.pref_zoom_start),
|
||||||
entries = ReaderPreferences.ZoomStart
|
entries = ReaderPreferences.ZoomStart
|
||||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||||
.toMap(),
|
.toMap()
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readerPreferences.cropBorders(),
|
pref = readerPreferences.cropBorders(),
|
||||||
|
@ -265,29 +269,26 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.webtoon_viewer),
|
title = stringResource(MR.strings.webtoon_viewer),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = navModePref,
|
pref = navModePref,
|
||||||
title = stringResource(MR.strings.pref_viewer_nav),
|
title = stringResource(MR.strings.pref_viewer_nav),
|
||||||
entries = ReaderPreferences.TapZones
|
entries = ReaderPreferences.TapZones
|
||||||
.mapIndexed { index, it -> index to stringResource(it) }
|
.mapIndexed { index, it -> index to stringResource(it) }
|
||||||
.toMap(),
|
.toMap()
|
||||||
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.webtoonNavInverted(),
|
pref = readerPreferences.webtoonNavInverted(),
|
||||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||||
entries = mapOf(
|
entries = persistentListOf(
|
||||||
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none),
|
ReaderPreferences.TappingInvertMode.NONE,
|
||||||
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(
|
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||||
MR.strings.tapping_inverted_horizontal,
|
ReaderPreferences.TappingInvertMode.VERTICAL,
|
||||||
),
|
ReaderPreferences.TappingInvertMode.BOTH,
|
||||||
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(
|
)
|
||||||
MR.strings.tapping_inverted_vertical,
|
.associateWith { stringResource(it.titleRes) }
|
||||||
),
|
.toImmutableMap(),
|
||||||
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
|
|
||||||
MR.strings.tapping_inverted_both,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabled = navMode != 5,
|
enabled = navMode != 5,
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SliderPreference(
|
Preference.PreferenceItem.SliderPreference(
|
||||||
|
@ -304,19 +305,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = readerPreferences.readerHideThreshold(),
|
pref = readerPreferences.readerHideThreshold(),
|
||||||
title = stringResource(MR.strings.pref_hide_threshold),
|
title = stringResource(MR.strings.pref_hide_threshold),
|
||||||
entries = mapOf(
|
entries = persistentMapOf(
|
||||||
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(
|
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
|
||||||
MR.strings.pref_highest,
|
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
|
||||||
),
|
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
|
||||||
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(
|
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
|
||||||
MR.strings.pref_high,
|
|
||||||
),
|
|
||||||
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(
|
|
||||||
MR.strings.pref_low,
|
|
||||||
),
|
|
||||||
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(
|
|
||||||
MR.strings.pref_lowest,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
@ -365,7 +358,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_reader_navigation),
|
title = stringResource(MR.strings.pref_reader_navigation),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readWithVolumeKeysPref,
|
pref = readWithVolumeKeysPref,
|
||||||
title = stringResource(MR.strings.pref_read_with_volume_keys),
|
title = stringResource(MR.strings.pref_read_with_volume_keys),
|
||||||
|
@ -383,7 +376,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_reader_actions),
|
title = stringResource(MR.strings.pref_reader_actions),
|
||||||
preferenceItems = listOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = readerPreferences.readWithLongTap(),
|
pref = readerPreferences.readWithLongTap(),
|
||||||
title = stringResource(MR.strings.pref_read_with_long_tap),
|
title = stringResource(MR.strings.pref_read_with_long_tap),
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue