mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2025-12-16 13:52:03 +01:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9c7cbf2c4 | ||
|
|
e63a52b8e3 | ||
|
|
49991d38d9 | ||
|
|
33c62ab711 | ||
|
|
899bd26956 | ||
|
|
a37f3eb709 | ||
|
|
9ae71dfe93 | ||
|
|
c65a9aecf5 | ||
|
|
02e50411de | ||
|
|
6e822dfd5b | ||
|
|
7292dadd5f | ||
|
|
b1067b942e | ||
|
|
d6c4af89c4 | ||
|
|
cf6f7c521c | ||
|
|
c6601c1f94 | ||
|
|
68899aea61 | ||
|
|
c3edf9b5d0 | ||
|
|
97e04392d3 | ||
|
|
3d178737b1 | ||
|
|
bf737cf95c | ||
|
|
c91ec9a33b | ||
|
|
a8040cb21a | ||
|
|
f60782f11f | ||
|
|
7d6e1bdafc | ||
|
|
5854ad97e0 | ||
|
|
4b8fa059d5 | ||
|
|
3dc2f9a711 | ||
|
|
8033a94ee2 | ||
|
|
028da099dd | ||
|
|
e6c6c32d81 | ||
|
|
bce6af62fc | ||
|
|
6510a9617a | ||
|
|
14510f1d26 | ||
|
|
f115edf2ea | ||
|
|
8a8362203f | ||
|
|
f3336fc5c3 | ||
|
|
727289c8eb | ||
|
|
9c91ddd4e3 | ||
|
|
3ea026e311 | ||
|
|
36f307e3bb | ||
|
|
89678ebb17 | ||
|
|
80b7d14af1 | ||
|
|
bbd8098a61 | ||
|
|
f8ef0f143b |
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@@ -3,10 +3,10 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.15.0)
|
- To the latest version of the app (stable is v0.15.3)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/)
|
- I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/)
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an official extension, 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
|
||||||
|
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: ⚠️ Extension/source issue
|
- name: ⚠️ Extension/source issue
|
||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
url: https://github.com/tachiyomiorg/extensions/issues/new/choose
|
||||||
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
about: Issues and requests for official extensions and sources should be opened in the extensions repository instead
|
||||||
- name: 📦 Tachiyomi extensions
|
- name: 📦 Tachiyomi extensions
|
||||||
url: https://tachiyomi.org/extensions/
|
url: https://tachiyomi.org/extensions/
|
||||||
about: List of all available extensions with download links
|
about: List of all available extensions with download links
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
6
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -53,7 +53,7 @@ body:
|
|||||||
label: Tachiyomi version
|
label: Tachiyomi version
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "0.15.0"
|
Example: "0.15.3"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -94,11 +94,11 @@ 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/tachiyomiorg/tachiyomi-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/tachiyomiorg/extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/).
|
- label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.15.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.15.3](https://github.com/tachiyomiorg/tachiyomi/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
@@ -31,9 +31,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/tachiyomiorg/tachiyomi-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/tachiyomiorg/extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.15.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.15.3](https://github.com/tachiyomiorg/tachiyomi/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
|
||||||
|
|||||||
4
.github/workflows/build_push.yml
vendored
4
.github/workflows/build_push.yml
vendored
@@ -22,6 +22,10 @@ jobs:
|
|||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
run: |
|
||||||
|
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ representative at an online or offline event.
|
|||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community moderators responsible for enforcement at
|
reported to the community moderators via issues.
|
||||||
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community moderators are obligated to respect the privacy and security of the
|
All community moderators are obligated to respect the privacy and security of the
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
|
|||||||
|
|
||||||
## Getting help
|
## Getting help
|
||||||
|
|
||||||
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
|
No support is currently provided.
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -1,6 +1,6 @@
|
|||||||
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
| Build | Stable | Weekly Preview | Contribute |
|
||||||
|-------|----------|---------|------------|---------|
|
|-------|--------|----------------|------------|
|
||||||
| [](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
| [](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) |
|
||||||
|
|
||||||
# Tachiyomi
|
# Tachiyomi
|
||||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||||
@@ -28,8 +28,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Issues</summary>
|
<details><summary>Issues</summary>
|
||||||
|
|
||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://tachiyomi.org/changelogs/) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
**Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://tachiyomi.org/changelogs/) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
||||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
|||||||
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
|
|
||||||
Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
|
Source requests are not accepted.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>Contributing</summary>
|
<details><summary>Contributing</summary>
|
||||||
@@ -70,7 +69,6 @@ See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
|||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
[See our website.](https://tachiyomi.org/)
|
[See our website.](https://tachiyomi.org/)
|
||||||
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ plugins {
|
|||||||
id("com.github.zellius.shortcut-helper")
|
id("com.github.zellius.shortcut-helper")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
|
||||||
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
|
|
||||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
@@ -22,8 +18,8 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
|
|
||||||
versionCode = 115
|
versionCode = 119
|
||||||
versionName = "0.15.0"
|
versionName = "0.15.3"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -31,9 +27,6 @@ android {
|
|||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||||
buildConfigField("boolean", "PREVIEW", "false")
|
buildConfigField("boolean", "PREVIEW", "false")
|
||||||
|
|
||||||
// Please disable ACRA or use your own instance in forked versions of the project
|
|
||||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += SUPPORTED_ABIS
|
abiFilters += SUPPORTED_ABIS
|
||||||
}
|
}
|
||||||
@@ -245,10 +238,6 @@ dependencies {
|
|||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
|
|
||||||
// Crash reports/analytics
|
|
||||||
implementation(libs.bundles.acra)
|
|
||||||
"standardImplementation"(libs.firebase.analytics)
|
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
implementation(libs.bundles.shizuku)
|
implementation(libs.bundles.shizuku)
|
||||||
|
|
||||||
|
|||||||
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@@ -71,7 +71,3 @@
|
|||||||
|
|
||||||
# XmlUtil
|
# XmlUtil
|
||||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
||||||
|
|
||||||
# Firebase
|
|
||||||
-keep class com.google.firebase.installations.** { *; }
|
|
||||||
-keep interface com.google.firebase.installations.** { *; }
|
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
<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" />
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
<!-- For background jobs -->
|
<!-- For background jobs -->
|
||||||
@@ -21,19 +22,17 @@
|
|||||||
<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" />
|
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" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
<!-- Remove permission from Firebase dependency -->
|
|
||||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
|
|
||||||
tools:node="remove" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
@@ -52,13 +51,53 @@
|
|||||||
|
|
||||||
<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 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>
|
||||||
|
|
||||||
|
<!-- 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"
|
||||||
@@ -66,16 +105,16 @@
|
|||||||
</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.DeepLinkActivity"
|
android:name=".ui.deeplink.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
|
||||||
android:label="@string/action_search"
|
android:label="@string/action_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" />
|
||||||
@@ -99,20 +138,21 @@
|
|||||||
|
|
||||||
<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:resource="@xml/s_pen_actions"/>
|
android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
|
android:resource="@xml/s_pen_actions" />
|
||||||
</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"
|
||||||
@@ -121,25 +161,25 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
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:host="anilist-auth"/>
|
<data android:scheme="tachiyomi" />
|
||||||
<data android:host="bangumi-auth"/>
|
|
||||||
<data android:host="myanimelist-auth"/>
|
|
||||||
<data android:host="shikimori-auth"/>
|
|
||||||
|
|
||||||
<data android:scheme="tachiyomi"/>
|
<data android:host="anilist-auth" />
|
||||||
|
<data android:host="bangumi-auth" />
|
||||||
|
<data android:host="myanimelist-auth" />
|
||||||
|
<data android:host="shikimori-auth" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
@@ -179,9 +219,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
|
||||||
@@ -191,11 +231,6 @@
|
|||||||
android:name="android.webkit.WebView.MetricsOptOut"
|
android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
|
||||||
<!-- Disable advertising ID collection for Firebase -->
|
|
||||||
<meta-data
|
|
||||||
android:name="google_analytics_adid_collection_enabled"
|
|
||||||
android:value="false" />
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
|
|||||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||||
|
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
|
||||||
|
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||||
|
import eu.kanade.domain.extension.interactor.GetExtensionRepos
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||||
|
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||||
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
|
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
|
||||||
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
|
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
|
||||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.domain.source.interactor.CreateSourceRepo
|
|
||||||
import eu.kanade.domain.source.interactor.DeleteSourceRepo
|
|
||||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
||||||
import eu.kanade.domain.source.interactor.GetSourceRepos
|
|
||||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||||
@@ -170,9 +171,10 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { ToggleLanguage(get()) }
|
addFactory { ToggleLanguage(get()) }
|
||||||
addFactory { ToggleSource(get()) }
|
addFactory { ToggleSource(get()) }
|
||||||
addFactory { ToggleSourcePin(get()) }
|
addFactory { ToggleSourcePin(get()) }
|
||||||
|
addFactory { TrustExtension(get()) }
|
||||||
|
|
||||||
addFactory { CreateSourceRepo(get()) }
|
addFactory { CreateExtensionRepo(get()) }
|
||||||
addFactory { DeleteSourceRepo(get()) }
|
addFactory { DeleteExtensionRepo(get()) }
|
||||||
addFactory { GetSourceRepos(get()) }
|
addFactory { GetExtensionRepos(get()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package eu.kanade.domain.base
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
|
||||||
import tachiyomi.core.preference.Preference
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -22,8 +20,6 @@ class BasePreferences(
|
|||||||
|
|
||||||
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
||||||
|
|
||||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
|
||||||
|
|
||||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||||
|
|
||||||
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.extension.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import tachiyomi.core.preference.plusAssign
|
import tachiyomi.core.preference.plusAssign
|
||||||
|
|
||||||
class CreateSourceRepo(private val preferences: SourcePreferences) {
|
class CreateExtensionRepo(private val preferences: SourcePreferences) {
|
||||||
|
|
||||||
fun await(name: String): Result {
|
fun await(name: String): Result {
|
||||||
// Do not allow invalid formats
|
// Do not allow invalid formats
|
||||||
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_REPO_BASE_URL)) {
|
if (!name.matches(repoRegex)) {
|
||||||
return Result.InvalidUrl
|
return Result.InvalidUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences.extensionRepos() += name.substringBeforeLast("/index.min.json")
|
preferences.extensionRepos() += name.removeSuffix("/index.min.json")
|
||||||
|
|
||||||
return Result.Success
|
return Result.Success
|
||||||
}
|
}
|
||||||
@@ -22,5 +22,4 @@ class CreateSourceRepo(private val preferences: SourcePreferences) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const val OFFICIAL_REPO_BASE_URL = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo"
|
|
||||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.extension.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import tachiyomi.core.preference.minusAssign
|
import tachiyomi.core.preference.minusAssign
|
||||||
|
|
||||||
class DeleteSourceRepo(private val preferences: SourcePreferences) {
|
class DeleteExtensionRepo(private val preferences: SourcePreferences) {
|
||||||
|
|
||||||
fun await(repo: String) {
|
fun await(repo: String) {
|
||||||
preferences.extensionRepos() -= repo
|
preferences.extensionRepos() -= repo
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.domain.extension.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class GetExtensionRepos(private val preferences: SourcePreferences) {
|
||||||
|
|
||||||
|
fun subscribe(): Flow<Set<String>> {
|
||||||
|
return preferences.extensionRepos().changes()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.domain.extension.interactor
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import tachiyomi.core.preference.getAndSet
|
||||||
|
|
||||||
|
class TrustExtension(
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,9 +81,9 @@ class UpdateManga(
|
|||||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
|
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return fetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
return mangaRepository.update(
|
||||||
?.let { mangaRepository.update(it) }
|
fetchInterval.toMangaUpdate(manga, dateTime, window),
|
||||||
?: false
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
class GetSourceRepos(private val preferences: SourcePreferences) {
|
|
||||||
|
|
||||||
fun subscribe(): Flow<List<String>> {
|
|
||||||
return preferences.extensionRepos().changes()
|
|
||||||
.map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,11 +38,14 @@ class SourcePreferences(
|
|||||||
SetMigrateSorting.Direction.ASCENDING,
|
SetMigrateSorting.Direction.ASCENDING,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
|
||||||
|
|
||||||
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||||
|
|
||||||
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
|
fun trustedExtensions() = preferenceStore.getStringSet(
|
||||||
|
Preference.appStateKey("trusted_extensions"),
|
||||||
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
|
emptySet(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
|||||||
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,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.domain.ui.model
|
package eu.kanade.domain.ui.model
|
||||||
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
enum class AppTheme(val titleRes: StringResource?) {
|
enum class AppTheme(val titleRes: StringResource?) {
|
||||||
@@ -9,6 +11,9 @@ enum class AppTheme(val titleRes: StringResource?) {
|
|||||||
GREEN_APPLE(MR.strings.theme_greenapple),
|
GREEN_APPLE(MR.strings.theme_greenapple),
|
||||||
LAVENDER(MR.strings.theme_lavender),
|
LAVENDER(MR.strings.theme_lavender),
|
||||||
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
||||||
|
|
||||||
|
// TODO: re-enable for preview
|
||||||
|
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
|
||||||
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),
|
||||||
|
|||||||
@@ -16,7 +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.outlined.History
|
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||||
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
|
||||||
@@ -53,6 +53,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
|||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
||||||
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
|
||||||
@@ -66,13 +67,23 @@ fun ExtensionDetailsScreen(
|
|||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
state: ExtensionDetailsScreenModel.State,
|
state: ExtensionDetailsScreenModel.State,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickWhatsNew: () -> 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,12 +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)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -138,7 +151,7 @@ fun ExtensionDetailsScreen(
|
|||||||
private fun ExtensionDetails(
|
private fun ExtensionDetails(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
extension: Extension.Installed,
|
extension: Extension.Installed,
|
||||||
sources: List<ExtensionSourceItem>,
|
sources: ImmutableList<ExtensionSourceItem>,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
onClickSource: (sourceId: Long) -> Unit,
|
||||||
@@ -149,27 +162,7 @@ private fun ExtensionDetails(
|
|||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
when {
|
if (extension.isObsolete) {
|
||||||
extension.isRepoSource ->
|
|
||||||
item {
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
WarningBanner(
|
|
||||||
MR.strings.repo_extension_message,
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
extension.repoUrl ?: return@clickable
|
|
||||||
uriHandler.openUri(
|
|
||||||
extension.repoUrl
|
|
||||||
.replace("https://raw.githubusercontent.com", "https://github.com")
|
|
||||||
.removeSuffix("/repo/"),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,11 +40,14 @@ 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 cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
@@ -52,6 +55,7 @@ import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
|||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
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
|
||||||
@@ -59,6 +63,7 @@ import tachiyomi.presentation.core.components.material.padding
|
|||||||
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import tachiyomi.presentation.core.theme.header
|
import tachiyomi.presentation.core.theme.header
|
||||||
import tachiyomi.presentation.core.util.plus
|
import tachiyomi.presentation.core.util.plus
|
||||||
@@ -80,6 +85,8 @@ fun ExtensionScreen(
|
|||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
PullRefresh(
|
PullRefresh(
|
||||||
refreshing = state.isRefreshing,
|
refreshing = state.isRefreshing,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
@@ -96,6 +103,13 @@ fun ExtensionScreen(
|
|||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
stringRes = msg,
|
stringRes = msg,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
actions = persistentListOf(
|
||||||
|
EmptyScreenAction(
|
||||||
|
stringRes = MR.strings.label_extension_repos,
|
||||||
|
icon = Icons.Outlined.Settings,
|
||||||
|
onClick = { navigator.push(ExtensionReposScreen()) },
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -133,13 +147,13 @@ private fun ExtensionContent(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||||
val installGranted = rememberRequestPackageInstallsPermissionState()
|
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
contentPadding = contentPadding + topSmallPaddingValues,
|
||||||
) {
|
) {
|
||||||
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
||||||
item {
|
item(key = "extension-permissions-warning") {
|
||||||
WarningBanner(
|
WarningBanner(
|
||||||
textRes = MR.strings.ext_permission_install_apps_warning,
|
textRes = MR.strings.ext_permission_install_apps_warning,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
@@ -342,7 +356,6 @@ private fun ExtensionItemContent(
|
|||||||
|
|
||||||
val warning = when {
|
val warning = when {
|
||||||
extension is Extension.Untrusted -> MR.strings.ext_untrusted
|
extension is Extension.Untrusted -> MR.strings.ext_untrusted
|
||||||
extension is Extension.Installed && extension.isUnofficial -> MR.strings.ext_unofficial
|
|
||||||
extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
extension is Extension.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
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ internal fun GlobalSearchContent(
|
|||||||
title = fromSourceId?.let {
|
title = fromSourceId?.let {
|
||||||
"▶ ${source.name}".takeIf { source.id == fromSourceId }
|
"▶ ${source.name}".takeIf { source.id == fromSourceId }
|
||||||
} ?: source.name,
|
} ?: source.name,
|
||||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
|
||||||
onClick = { onClickSource(source) },
|
onClick = { onClickSource(source) },
|
||||||
) {
|
) {
|
||||||
when (result) {
|
when (result) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import eu.kanade.presentation.browse.components.BaseSourceItem
|
|||||||
import eu.kanade.presentation.browse.components.SourceIcon
|
import eu.kanade.presentation.browse.components.SourceIcon
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
|
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.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 MigrateSourceScreen(
|
|||||||
|
|
||||||
@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,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import androidx.compose.material.icons.outlined.Refresh
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
@@ -15,7 +15,7 @@ import tachiyomi.presentation.core.screens.EmptyScreenAction
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun NoActionPreview() {
|
private fun NoActionPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
stringRes = MR.strings.empty_screen,
|
stringRes = MR.strings.empty_screen,
|
||||||
@@ -27,7 +27,7 @@ private fun NoActionPreview() {
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun WithActionPreview() {
|
private fun WithActionPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
stringRes = MR.strings.empty_screen,
|
stringRes = MR.strings.empty_screen,
|
||||||
|
|||||||
@@ -101,6 +101,6 @@ data class TabContent(
|
|||||||
val titleRes: StringResource,
|
val titleRes: StringResource,
|
||||||
val badgeNumber: Int? = null,
|
val badgeNumber: Int? = null,
|
||||||
val searchEnabled: Boolean = false,
|
val searchEnabled: Boolean = false,
|
||||||
val actions: ImmutableList<AppBar.Action> = persistentListOf(),
|
val actions: ImmutableList<AppBar.AppBarAction> = persistentListOf(),
|
||||||
val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit,
|
val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -63,7 +63,7 @@ fun CrashScreen(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun CrashScreenPreview() {
|
private fun CrashScreenPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
CrashScreen(exception = RuntimeException("Dummy")) {}
|
CrashScreen(exception = RuntimeException("Dummy")) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import eu.kanade.presentation.components.AppBarTitle
|
|||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
import eu.kanade.presentation.components.relativeDateText
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.history.components.HistoryItem
|
import eu.kanade.presentation.history.components.HistoryItem
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
|
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||||
@@ -143,7 +143,7 @@ internal fun HistoryScreenPreviews(
|
|||||||
@PreviewParameter(HistoryScreenModelStateProvider::class)
|
@PreviewParameter(HistoryScreenModelStateProvider::class)
|
||||||
historyState: HistoryScreenModel.State,
|
historyState: HistoryScreenModel.State,
|
||||||
) {
|
) {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
HistoryScreen(
|
HistoryScreen(
|
||||||
state = historyState,
|
state = historyState,
|
||||||
snackbarHostState = SnackbarHostState(),
|
snackbarHostState = SnackbarHostState(),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ 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 eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
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.components.material.padding
|
||||||
@@ -91,7 +91,7 @@ fun HistoryDeleteAllDialog(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun HistoryDeleteDialogPreview() {
|
private fun HistoryDeleteDialogPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
HistoryDeleteDialog(
|
HistoryDeleteDialog(
|
||||||
onDismissRequest = {},
|
onDismissRequest = {},
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.manga.components.MangaCover
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.presentation.util.formatChapterNumber
|
import eu.kanade.presentation.util.formatChapterNumber
|
||||||
import eu.kanade.tachiyomi.util.lang.toTimestampString
|
import eu.kanade.tachiyomi.util.lang.toTimestampString
|
||||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||||
@@ -98,7 +98,7 @@ private fun HistoryItemPreviews(
|
|||||||
@PreviewParameter(HistoryWithRelationsProvider::class)
|
@PreviewParameter(HistoryWithRelationsProvider::class)
|
||||||
historyWithRelations: HistoryWithRelations,
|
historyWithRelations: HistoryWithRelations,
|
||||||
) {
|
) {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
HistoryItem(
|
HistoryItem(
|
||||||
history = historyWithRelations,
|
history = historyWithRelations,
|
||||||
|
|||||||
@@ -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.LibrarySettingsScreenModel
|
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
|
||||||
|
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.filterDownloaded().collectAsState()
|
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState()
|
||||||
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
|
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
|
||||||
|
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateMangaRestrictions().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::filterCompleted) },
|
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
|
||||||
)
|
)
|
||||||
|
// TODO: re-enable when custom intervals are ready for stable
|
||||||
|
if (
|
||||||
|
(isDevFlavor || isPreviewBuildType) &&
|
||||||
|
LibraryPreferences.MANGA_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) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import androidx.compose.material.icons.outlined.Folder
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import tachiyomi.presentation.core.components.Badge
|
import tachiyomi.presentation.core.components.Badge
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -50,7 +50,7 @@ internal fun LanguageBadge(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun BadgePreview() {
|
private fun BadgePreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Column {
|
Column {
|
||||||
DownloadsBadge(count = 10)
|
DownloadsBadge(count = 10)
|
||||||
UnreadBadge(count = 10)
|
UnreadBadge(count = 10)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ 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.Instant
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeleteChaptersDialog(
|
fun DeleteChaptersDialog(
|
||||||
@@ -85,7 +86,7 @@ fun SetIntervalDialog(
|
|||||||
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
|
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
if (nextUpdateDays != null && nextUpdateDays >= 0) {
|
if (nextUpdateDays != null && nextUpdateDays >= 0 && interval >= 0) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
MR.strings.manga_interval_expected_update,
|
MR.strings.manga_interval_expected_update,
|
||||||
@@ -96,8 +97,8 @@ fun SetIntervalDialog(
|
|||||||
),
|
),
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
MR.plurals.day,
|
MR.plurals.day,
|
||||||
count = interval,
|
count = interval.absoluteValue,
|
||||||
interval,
|
interval.absoluteValue,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -105,7 +106,6 @@ fun SetIntervalDialog(
|
|||||||
Spacer(Modifier.height(MaterialTheme.padding.small))
|
Spacer(Modifier.height(MaterialTheme.padding.small))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: selecting "1" then doesn't allow for future changes unless defaulting first?
|
|
||||||
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
||||||
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
||||||
|
|
||||||
|
|||||||
@@ -201,14 +201,14 @@ fun MangaActionRow(
|
|||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = if (nextUpdateDays != null) {
|
title = when (nextUpdateDays) {
|
||||||
pluralStringResource(
|
null -> stringResource(MR.strings.not_applicable)
|
||||||
|
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
|
||||||
|
else -> pluralStringResource(
|
||||||
MR.plurals.day,
|
MR.plurals.day,
|
||||||
count = nextUpdateDays,
|
count = nextUpdateDays,
|
||||||
nextUpdateDays,
|
nextUpdateDays,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
stringResource(MR.strings.not_applicable)
|
|
||||||
},
|
},
|
||||||
icon = Icons.Default.HourglassEmpty,
|
icon = Icons.Default.HourglassEmpty,
|
||||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
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 eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
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.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
@@ -44,7 +44,7 @@ fun MissingChapterCountListItem(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun Preview() {
|
private fun Preview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
MissingChapterCountListItem(count = 42)
|
MissingChapterCountListItem(count = 42)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.presentation.more
|
package eu.kanade.presentation.more
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
@@ -23,7 +22,6 @@ 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.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
|
||||||
@@ -60,14 +58,7 @@ fun MoreScreen(
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
if (isFDroid) {
|
if (isFDroid) {
|
||||||
WarningBanner(
|
// Don't really care about slow updaters now
|
||||||
textRes = MR.strings.fdroid_warning,
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
uriHandler.openUri(
|
|
||||||
"https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import com.halilibo.richtext.markdown.Markdown
|
|||||||
import com.halilibo.richtext.ui.RichTextStyle
|
import com.halilibo.richtext.ui.RichTextStyle
|
||||||
import com.halilibo.richtext.ui.material3.RichText
|
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.TachiyomiPreviewTheme
|
||||||
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
|
||||||
@@ -69,7 +69,7 @@ fun NewUpdateScreen(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun NewUpdateScreenPreview() {
|
private fun NewUpdateScreenPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
NewUpdateScreen(
|
NewUpdateScreen(
|
||||||
versionName = "v0.99.9",
|
versionName = "v0.99.9",
|
||||||
changelogInfo = """
|
changelogInfo = """
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
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.TachiyomiPreviewTheme
|
||||||
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
|
||||||
@@ -41,6 +41,7 @@ internal class GuidesStep(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ const val GETTING_STARTED_URL = "https://tachiyomi.org/docs/guides/getting-start
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun GuidesStepPreview() {
|
private fun GuidesStepPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
GuidesStep(
|
GuidesStep(
|
||||||
onRestoreBackup = {},
|
onRestoreBackup = {},
|
||||||
).Content()
|
).Content()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -14,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
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.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.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
@@ -38,6 +40,8 @@ internal class StorageStep : OnboardingStep {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val handler = LocalUriHandler.current
|
||||||
|
|
||||||
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -64,6 +68,19 @@ internal class StorageStep : OnboardingStep {
|
|||||||
) {
|
) {
|
||||||
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) {
|
LaunchedEffect(Unit) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ 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.interactor.TrustExtension
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.presentation.more.settings.screen.advanced.ClearDatabaseScreen
|
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
|
||||||
@@ -47,7 +48,6 @@ 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.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.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
|
||||||
@@ -87,12 +87,6 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
return buildList {
|
return buildList {
|
||||||
addAll(
|
addAll(
|
||||||
listOf(
|
listOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
pref = basePreferences.acraEnabled(),
|
|
||||||
title = stringResource(MR.strings.pref_enable_acra),
|
|
||||||
subtitle = stringResource(MR.strings.pref_acra_summary),
|
|
||||||
enabled = isPreviewBuildType || isReleaseBuildType,
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_dump_crash_logs),
|
title = stringResource(MR.strings.pref_dump_crash_logs),
|
||||||
subtitle = stringResource(MR.strings.pref_dump_crash_logs_summary),
|
subtitle = stringResource(MR.strings.pref_dump_crash_logs_summary),
|
||||||
@@ -340,6 +334,7 @@ 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 trustExtension = remember { Injekt.get<TrustExtension>() }
|
||||||
|
|
||||||
if (shizukuMissing) {
|
if (shizukuMissing) {
|
||||||
val dismiss = { shizukuMissing = false }
|
val dismiss = { shizukuMissing = false }
|
||||||
@@ -392,6 +387,13 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(MR.strings.ext_revoke_trust),
|
||||||
|
onClick = {
|
||||||
|
trustExtension.revokeAll()
|
||||||
|
context.toast(MR.strings.requires_app_restart)
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,26 @@
|
|||||||
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.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 eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import org.xmlpull.v1.XmlPullParser
|
|
||||||
import tachiyomi.core.i18n.stringResource
|
|
||||||
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
|
||||||
@@ -107,11 +99,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()
|
||||||
@@ -119,26 +108,12 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_display),
|
title = stringResource(MR.strings.pref_category_display),
|
||||||
preferenceItems = persistentListOf(
|
preferenceItems = persistentListOf(
|
||||||
Preference.PreferenceItem.BasicListPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
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(),
|
||||||
@@ -173,30 +148,6 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
private fun getLangs(context: Context): ImmutableMap<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().toImmutableMap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DateFormats = listOf(
|
private val DateFormats = listOf(
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ import android.net.Uri
|
|||||||
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.RowScope
|
||||||
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.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||||
import androidx.compose.material3.SegmentedButton
|
import androidx.compose.material3.SegmentedButton
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
@@ -22,6 +27,7 @@ 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
|
||||||
@@ -58,11 +64,23 @@ import uy.kohesive.injekt.api.get
|
|||||||
object SettingsDataScreen : SearchableSettings {
|
object SettingsDataScreen : SearchableSettings {
|
||||||
|
|
||||||
val restorePreferenceKeyString = MR.strings.label_backup
|
val restorePreferenceKeyString = MR.strings.label_backup
|
||||||
|
const val HELP_URL = "https://tachiyomi.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>()
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen.about
|
package eu.kanade.presentation.more.settings.screen.about
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
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.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Public
|
import androidx.compose.material.icons.outlined.Public
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -30,13 +27,10 @@ import eu.kanade.presentation.util.LocalBackPress
|
|||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
|
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
|
||||||
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
|
|
||||||
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
|
|
||||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||||
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
@@ -48,7 +42,6 @@ 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.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.icons.CustomIcons
|
import tachiyomi.presentation.core.icons.CustomIcons
|
||||||
import tachiyomi.presentation.core.icons.Discord
|
|
||||||
import tachiyomi.presentation.core.icons.Facebook
|
import tachiyomi.presentation.core.icons.Facebook
|
||||||
import tachiyomi.presentation.core.icons.Github
|
import tachiyomi.presentation.core.icons.Github
|
||||||
import tachiyomi.presentation.core.icons.Reddit
|
import tachiyomi.presentation.core.icons.Reddit
|
||||||
@@ -98,61 +91,6 @@ object AboutScreen : Screen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
|
||||||
item {
|
|
||||||
TextPreferenceWidget(
|
|
||||||
title = stringResource(MR.strings.check_for_updates),
|
|
||||||
widget = {
|
|
||||||
AnimatedVisibility(visible = isCheckingUpdates) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(28.dp),
|
|
||||||
strokeWidth = 3.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPreferenceClick = {
|
|
||||||
if (!isCheckingUpdates) {
|
|
||||||
scope.launch {
|
|
||||||
isCheckingUpdates = true
|
|
||||||
|
|
||||||
checkVersion(
|
|
||||||
context = context,
|
|
||||||
onAvailableUpdate = { result ->
|
|
||||||
val updateScreen = NewUpdateScreen(
|
|
||||||
versionName = result.release.version,
|
|
||||||
changelogInfo = result.release.info,
|
|
||||||
releaseLink = result.release.releaseLink,
|
|
||||||
downloadLink = result.release.getDownloadLink(),
|
|
||||||
)
|
|
||||||
navigator.push(updateScreen)
|
|
||||||
},
|
|
||||||
onFinish = {
|
|
||||||
isCheckingUpdates = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!BuildConfig.DEBUG) {
|
|
||||||
item {
|
|
||||||
TextPreferenceWidget(
|
|
||||||
title = stringResource(MR.strings.whats_new),
|
|
||||||
onPreferenceClick = { uriHandler.openUri(RELEASE_URL) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
TextPreferenceWidget(
|
|
||||||
title = stringResource(MR.strings.help_translate),
|
|
||||||
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/docs/contribute#translation") },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
item {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(MR.strings.licenses),
|
title = stringResource(MR.strings.licenses),
|
||||||
@@ -179,11 +117,6 @@ object AboutScreen : Screen() {
|
|||||||
icon = Icons.Outlined.Public,
|
icon = Icons.Outlined.Public,
|
||||||
url = "https://tachiyomi.org",
|
url = "https://tachiyomi.org",
|
||||||
)
|
)
|
||||||
LinkIcon(
|
|
||||||
label = "Discord",
|
|
||||||
icon = CustomIcons.Discord,
|
|
||||||
url = "https://discord.gg/tachiyomi",
|
|
||||||
)
|
|
||||||
LinkIcon(
|
LinkIcon(
|
||||||
label = "X",
|
label = "X",
|
||||||
icon = CustomIcons.X,
|
icon = CustomIcons.X,
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package eu.kanade.presentation.more.settings.screen.appearance
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
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.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import org.xmlpull.v1.XmlPullParser
|
||||||
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
|
class AppLanguageScreen : Screen() {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
|
val langs = remember { getLangs(context) }
|
||||||
|
var currentLanguage by remember {
|
||||||
|
mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(currentLanguage) {
|
||||||
|
val locale = if (currentLanguage.isEmpty()) {
|
||||||
|
LocaleListCompat.getEmptyLocaleList()
|
||||||
|
} else {
|
||||||
|
LocaleListCompat.forLanguageTags(currentLanguage)
|
||||||
|
}
|
||||||
|
AppCompatDelegate.setApplicationLocales(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
AppBar(
|
||||||
|
title = stringResource(MR.strings.pref_app_language),
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
) {
|
||||||
|
items(langs) {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
currentLanguage = it.langTag
|
||||||
|
},
|
||||||
|
headlineContent = { Text(it.displayName) },
|
||||||
|
supportingContent = {
|
||||||
|
it.localizedDisplayName?.let {
|
||||||
|
Text(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (currentLanguage == it.langTag) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLangs(context: Context): ImmutableList<Language> {
|
||||||
|
val langs = mutableListOf<Language>()
|
||||||
|
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.getLocalizedDisplayName(langTag)
|
||||||
|
if (displayName.isNotEmpty()) {
|
||||||
|
langs.add(Language(langTag, displayName, LocaleHelper.getDisplayName(langTag)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventType = parser.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
langs.sortBy { it.displayName }
|
||||||
|
langs.add(0, Language("", context.stringResource(MR.strings.label_default), null))
|
||||||
|
|
||||||
|
return langs.toImmutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Language(
|
||||||
|
val langTag: String,
|
||||||
|
val displayName: String,
|
||||||
|
val localizedDisplayName: String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,16 +16,22 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
class ExtensionReposScreen : Screen() {
|
class ExtensionReposScreen(
|
||||||
|
private val url: String? = null,
|
||||||
|
) : Screen() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
|
|
||||||
|
|
||||||
|
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(url) {
|
||||||
|
url?.let { screenModel.createRepo(it) }
|
||||||
|
}
|
||||||
|
|
||||||
if (state is RepoScreenState.Loading) {
|
if (state is RepoScreenState.Loading) {
|
||||||
LoadingScreen()
|
LoadingScreen()
|
||||||
return
|
return
|
||||||
@@ -46,7 +52,7 @@ class ExtensionReposScreen : Screen() {
|
|||||||
ExtensionRepoCreateDialog(
|
ExtensionRepoCreateDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onCreate = { screenModel.createRepo(it) },
|
onCreate = { screenModel.createRepo(it) },
|
||||||
categories = successState.repos,
|
repos = successState.repos,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RepoDialog.Delete -> {
|
is RepoDialog.Delete -> {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import androidx.compose.runtime.Immutable
|
|||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.domain.source.interactor.CreateSourceRepo
|
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
|
||||||
import eu.kanade.domain.source.interactor.DeleteSourceRepo
|
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
|
||||||
import eu.kanade.domain.source.interactor.GetSourceRepos
|
import eu.kanade.domain.extension.interactor.GetExtensionRepos
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
@@ -19,9 +19,9 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class ExtensionReposScreenModel(
|
class ExtensionReposScreenModel(
|
||||||
private val getSourceRepos: GetSourceRepos = Injekt.get(),
|
private val getExtensionRepos: GetExtensionRepos = Injekt.get(),
|
||||||
private val createSourceRepo: CreateSourceRepo = Injekt.get(),
|
private val createExtensionRepo: CreateExtensionRepo = Injekt.get(),
|
||||||
private val deleteSourceRepo: DeleteSourceRepo = Injekt.get(),
|
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
||||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||||
|
|
||||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||||
@@ -29,11 +29,11 @@ class ExtensionReposScreenModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
getSourceRepos.subscribe()
|
getExtensionRepos.subscribe()
|
||||||
.collectLatest { repos ->
|
.collectLatest { repos ->
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
RepoScreenState.Success(
|
RepoScreenState.Success(
|
||||||
repos = repos.toImmutableList(),
|
repos = repos.toImmutableSet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,8 +47,8 @@ class ExtensionReposScreenModel(
|
|||||||
*/
|
*/
|
||||||
fun createRepo(name: String) {
|
fun createRepo(name: String) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
when (createSourceRepo.await(name)) {
|
when (createExtensionRepo.await(name)) {
|
||||||
is CreateSourceRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
is CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ class ExtensionReposScreenModel(
|
|||||||
*/
|
*/
|
||||||
fun deleteRepo(repo: String) {
|
fun deleteRepo(repo: String) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
deleteSourceRepo.await(repo)
|
deleteExtensionRepo.await(repo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ sealed class RepoScreenState {
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Success(
|
data class Success(
|
||||||
val repos: ImmutableList<String>,
|
val repos: ImmutableSet<String>,
|
||||||
val dialog: RepoDialog? = null,
|
val dialog: RepoDialog? = null,
|
||||||
) : RepoScreenState() {
|
) : RepoScreenState() {
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
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.Label
|
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||||
|
import androidx.compose.material.icons.outlined.ContentCopy
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -19,12 +19,16 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionReposContent(
|
fun ExtensionReposContent(
|
||||||
repos: ImmutableList<String>,
|
repos: ImmutableSet<String>,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
onClickDelete: (String) -> Unit,
|
onClickDelete: (String) -> Unit,
|
||||||
@@ -36,14 +40,16 @@ fun ExtensionReposContent(
|
|||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
items(repos) { repo ->
|
repos.forEach {
|
||||||
|
item {
|
||||||
ExtensionRepoListItem(
|
ExtensionRepoListItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
repo = repo,
|
repo = it,
|
||||||
onDelete = { onClickDelete(repo) },
|
onDelete = { onClickDelete(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -52,6 +58,8 @@ private fun ExtensionRepoListItem(
|
|||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
@@ -73,8 +81,23 @@ private fun ExtensionRepoListItem(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
) {
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
val url = "$repo/index.min.json"
|
||||||
|
context.copyToClipboard(url, url)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.ContentCopy,
|
||||||
|
contentDescription = stringResource(MR.strings.action_copy_to_clipboard),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
IconButton(onClick = onDelete) {
|
IconButton(onClick = onDelete) {
|
||||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = null)
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = stringResource(MR.strings.action_delete),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -24,12 +24,12 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
fun ExtensionRepoCreateDialog(
|
fun ExtensionRepoCreateDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onCreate: (String) -> Unit,
|
onCreate: (String) -> Unit,
|
||||||
categories: ImmutableList<String>,
|
repos: ImmutableSet<String>,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
val nameAlreadyExists = remember(name) { repos.contains(name) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|||||||
@@ -42,15 +42,19 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.domain.ui.model.AppTheme
|
import eu.kanade.domain.ui.model.AppTheme
|
||||||
import eu.kanade.presentation.manga.components.MangaCover
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||||
|
import tachiyomi.core.preference.InMemoryPreferenceStore
|
||||||
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
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.fullType
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AppThemePreferenceWidget(
|
internal fun AppThemePreferenceWidget(
|
||||||
@@ -258,7 +262,8 @@ fun AppThemePreviewItem(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AppThemesListPreview() {
|
private fun AppThemesListPreview() {
|
||||||
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
|
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
|
||||||
TachiyomiTheme {
|
Injekt.addSingleton(fullType<UiPreferences>(), UiPreferences(InMemoryPreferenceStore()))
|
||||||
|
TachiyomiTheme(appTheme = appTheme) {
|
||||||
Surface {
|
Surface {
|
||||||
AppThemesList(
|
AppThemesList(
|
||||||
currentTheme = appTheme,
|
currentTheme = appTheme,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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 eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
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
|
||||||
@@ -43,7 +43,7 @@ internal fun InfoWidget(text: String) {
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun InfoWidgetPreview() {
|
private fun InfoWidgetPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
InfoWidget(text = stringResource(MR.strings.download_ahead_info))
|
InfoWidget(text = stringResource(MR.strings.download_ahead_info))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchPreferenceWidget(
|
fun SwitchPreferenceWidget(
|
||||||
@@ -40,7 +40,7 @@ fun SwitchPreferenceWidget(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun SwitchPreferenceWidgetPreview() {
|
private fun SwitchPreferenceWidgetPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
Column {
|
Column {
|
||||||
SwitchPreferenceWidget(
|
SwitchPreferenceWidget(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -62,7 +62,7 @@ fun TextPreferenceWidget(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun TextPreferenceWidgetPreview() {
|
private fun TextPreferenceWidgetPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
Column {
|
Column {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
|
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
@@ -306,7 +306,7 @@ private val FakeChapterLongTitle = previewChapter(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun TransitionTextPreview() {
|
private fun TransitionTextPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface(modifier = Modifier.padding(48.dp)) {
|
Surface(modifier = Modifier.padding(48.dp)) {
|
||||||
ChapterTransition(
|
ChapterTransition(
|
||||||
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeChapter)),
|
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeChapter)),
|
||||||
@@ -320,7 +320,7 @@ private fun TransitionTextPreview() {
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun TransitionTextLongTitlePreview() {
|
private fun TransitionTextLongTitlePreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface(modifier = Modifier.padding(48.dp)) {
|
Surface(modifier = Modifier.padding(48.dp)) {
|
||||||
ChapterTransition(
|
ChapterTransition(
|
||||||
transition = ChapterTransition.Next(ReaderChapter(FakeChapterLongTitle), ReaderChapter(FakeChapter)),
|
transition = ChapterTransition.Next(ReaderChapter(FakeChapterLongTitle), ReaderChapter(FakeChapter)),
|
||||||
@@ -334,7 +334,7 @@ private fun TransitionTextLongTitlePreview() {
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun TransitionTextWithGapPreview() {
|
private fun TransitionTextWithGapPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface(modifier = Modifier.padding(48.dp)) {
|
Surface(modifier = Modifier.padding(48.dp)) {
|
||||||
ChapterTransition(
|
ChapterTransition(
|
||||||
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeGapChapter)),
|
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeGapChapter)),
|
||||||
@@ -348,7 +348,7 @@ private fun TransitionTextWithGapPreview() {
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun TransitionTextNoNextPreview() {
|
private fun TransitionTextNoNextPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface(modifier = Modifier.padding(48.dp)) {
|
Surface(modifier = Modifier.padding(48.dp)) {
|
||||||
ChapterTransition(
|
ChapterTransition(
|
||||||
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), null),
|
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), null),
|
||||||
@@ -362,7 +362,7 @@ private fun TransitionTextNoNextPreview() {
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun TransitionTextNoPreviousPreview() {
|
private fun TransitionTextNoPreviousPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface(modifier = Modifier.padding(48.dp)) {
|
Surface(modifier = Modifier.padding(48.dp)) {
|
||||||
ChapterTransition(
|
ChapterTransition(
|
||||||
transition = ChapterTransition.Prev(ReaderChapter(FakeChapter), null),
|
transition = ChapterTransition.Prev(ReaderChapter(FakeChapter), null),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import dev.icerock.moko.resources.StringResource
|
|||||||
import eu.kanade.domain.manga.model.readerOrientation
|
import eu.kanade.domain.manga.model.readerOrientation
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.presentation.reader.components.ModeSelectionDialog
|
import eu.kanade.presentation.reader.components.ModeSelectionDialog
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -81,7 +81,7 @@ private fun DialogContent(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun DialogContentPreview() {
|
private fun DialogContentPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
Column {
|
Column {
|
||||||
DialogContent(
|
DialogContent(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import androidx.compose.ui.text.TextStyle
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PageIndicatorText(
|
fun PageIndicatorText(
|
||||||
@@ -51,7 +51,7 @@ fun PageIndicatorText(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun PageIndicatorTextPreview() {
|
private fun PageIndicatorTextPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
PageIndicatorText(currentPage = 10, totalPages = 69)
|
PageIndicatorText(currentPage = 10, totalPages = 69)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import dev.icerock.moko.resources.StringResource
|
|||||||
import eu.kanade.domain.manga.model.readingMode
|
import eu.kanade.domain.manga.model.readingMode
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.presentation.reader.components.ModeSelectionDialog
|
import eu.kanade.presentation.reader.components.ModeSelectionDialog
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -79,7 +79,7 @@ private fun DialogContent(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun DialogContentPreview() {
|
private fun DialogContentPreview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
Column {
|
Column {
|
||||||
DialogContent(
|
DialogContent(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import androidx.compose.ui.Alignment
|
|||||||
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
@@ -70,7 +70,7 @@ fun ModeSelectionDialog(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun Preview() {
|
private fun Preview() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
Column {
|
Column {
|
||||||
ModeSelectionDialog(
|
ModeSelectionDialog(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
|
|||||||
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.MonetColorScheme
|
import eu.kanade.presentation.theme.colorscheme.MonetColorScheme
|
||||||
|
import eu.kanade.presentation.theme.colorscheme.NordColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme
|
import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
|
import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.TakoColorScheme
|
import eu.kanade.presentation.theme.colorscheme.TakoColorScheme
|
||||||
@@ -27,9 +28,30 @@ fun TachiyomiTheme(
|
|||||||
appTheme: AppTheme? = null,
|
appTheme: AppTheme? = null,
|
||||||
amoled: Boolean? = null,
|
amoled: Boolean? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val uiPreferences = Injekt.get<UiPreferences>()
|
||||||
|
BaseTachiyomiTheme(
|
||||||
|
appTheme = appTheme ?: uiPreferences.appTheme().get(),
|
||||||
|
isAmoled = amoled ?: uiPreferences.themeDarkAmoled().get(),
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TachiyomiPreviewTheme(
|
||||||
|
appTheme: AppTheme = AppTheme.DEFAULT,
|
||||||
|
isAmoled: Boolean = false,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) = BaseTachiyomiTheme(appTheme, isAmoled, content)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BaseTachiyomiTheme(
|
||||||
|
appTheme: AppTheme,
|
||||||
|
isAmoled: Boolean,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = getThemeColorScheme(appTheme, amoled),
|
colorScheme = getThemeColorScheme(appTheme, isAmoled),
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -37,16 +59,16 @@ fun TachiyomiTheme(
|
|||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
private fun getThemeColorScheme(
|
private fun getThemeColorScheme(
|
||||||
appTheme: AppTheme?,
|
appTheme: AppTheme,
|
||||||
amoled: Boolean?,
|
isAmoled: Boolean,
|
||||||
): ColorScheme {
|
): ColorScheme {
|
||||||
val uiPreferences = Injekt.get<UiPreferences>()
|
val colorScheme = when (appTheme) {
|
||||||
val colorScheme = when (appTheme ?: uiPreferences.appTheme().get()) {
|
|
||||||
AppTheme.DEFAULT -> TachiyomiColorScheme
|
AppTheme.DEFAULT -> TachiyomiColorScheme
|
||||||
AppTheme.MONET -> MonetColorScheme(LocalContext.current)
|
AppTheme.MONET -> MonetColorScheme(LocalContext.current)
|
||||||
AppTheme.GREEN_APPLE -> GreenAppleColorScheme
|
AppTheme.GREEN_APPLE -> GreenAppleColorScheme
|
||||||
AppTheme.LAVENDER -> LavenderColorScheme
|
AppTheme.LAVENDER -> LavenderColorScheme
|
||||||
AppTheme.MIDNIGHT_DUSK -> MidnightDuskColorScheme
|
AppTheme.MIDNIGHT_DUSK -> MidnightDuskColorScheme
|
||||||
|
AppTheme.NORD -> NordColorScheme
|
||||||
AppTheme.STRAWBERRY_DAIQUIRI -> StrawberryColorScheme
|
AppTheme.STRAWBERRY_DAIQUIRI -> StrawberryColorScheme
|
||||||
AppTheme.TAKO -> TakoColorScheme
|
AppTheme.TAKO -> TakoColorScheme
|
||||||
AppTheme.TEALTURQUOISE -> TealTurqoiseColorScheme
|
AppTheme.TEALTURQUOISE -> TealTurqoiseColorScheme
|
||||||
@@ -57,6 +79,6 @@ private fun getThemeColorScheme(
|
|||||||
}
|
}
|
||||||
return colorScheme.getColorScheme(
|
return colorScheme.getColorScheme(
|
||||||
isSystemInDarkTheme(),
|
isSystemInDarkTheme(),
|
||||||
amoled ?: uiPreferences.themeDarkAmoled().get(),
|
isAmoled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package eu.kanade.presentation.theme.colorscheme
|
||||||
|
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors for Nord theme
|
||||||
|
* https://www.nordtheme.com/docs/colors-and-palettes
|
||||||
|
* for the light theme, the primary color is switched with the tertiary for better contrast in some case
|
||||||
|
*/
|
||||||
|
internal object NordColorScheme : BaseColorScheme() {
|
||||||
|
|
||||||
|
override val darkScheme = darkColorScheme(
|
||||||
|
primary = Color(0xFF88C0D0),
|
||||||
|
onPrimary = Color(0xFF2E3440),
|
||||||
|
primaryContainer = Color(0xFF88C0D0),
|
||||||
|
onPrimaryContainer = Color(0xFF2E3440),
|
||||||
|
inversePrimary = Color(0xFF397E91),
|
||||||
|
secondary = Color(0xFF81A1C1),
|
||||||
|
onSecondary = Color(0xFF2E3440),
|
||||||
|
secondaryContainer = Color(0xFF81A1C1),
|
||||||
|
onSecondaryContainer = Color(0xFF2E3440),
|
||||||
|
tertiary = Color(0xFF5E81AC),
|
||||||
|
onTertiary = Color(0xFF000000),
|
||||||
|
tertiaryContainer = Color(0xFF5E81AC),
|
||||||
|
onTertiaryContainer = Color(0xFF000000),
|
||||||
|
background = Color(0xFF2E3440),
|
||||||
|
onBackground = Color(0xFFECEFF4),
|
||||||
|
surface = Color(0xFF3B4252),
|
||||||
|
onSurface = Color(0xFFECEFF4),
|
||||||
|
surfaceVariant = Color(0xFF2E3440),
|
||||||
|
onSurfaceVariant = Color(0xFFECEFF4),
|
||||||
|
surfaceTint = Color(0xFF88C0D0),
|
||||||
|
inverseSurface = Color(0xFFD8DEE9),
|
||||||
|
inverseOnSurface = Color(0xFF2E3440),
|
||||||
|
outline = Color(0xFF6d717b),
|
||||||
|
outlineVariant = Color(0xFF90939a),
|
||||||
|
onError = Color(0xFF2E3440),
|
||||||
|
errorContainer = Color(0xFFBF616A),
|
||||||
|
onErrorContainer = Color(0xFF000000),
|
||||||
|
)
|
||||||
|
|
||||||
|
override val lightScheme = lightColorScheme(
|
||||||
|
primary = Color(0xFF5E81AC),
|
||||||
|
onPrimary = Color(0xFF000000),
|
||||||
|
primaryContainer = Color(0xFF5E81AC),
|
||||||
|
onPrimaryContainer = Color(0xFF000000),
|
||||||
|
inversePrimary = Color(0xFF8CA8CD),
|
||||||
|
secondary = Color(0xFF81A1C1),
|
||||||
|
onSecondary = Color(0xFF2E3440),
|
||||||
|
secondaryContainer = Color(0xFF81A1C1),
|
||||||
|
onSecondaryContainer = Color(0xFF2E3440),
|
||||||
|
tertiary = Color(0xFF88C0D0),
|
||||||
|
onTertiary = Color(0xFF2E3440),
|
||||||
|
tertiaryContainer = Color(0xFF88C0D0),
|
||||||
|
onTertiaryContainer = Color(0xFF2E3440),
|
||||||
|
background = Color(0xFFECEFF4),
|
||||||
|
onBackground = Color(0xFF2E3440),
|
||||||
|
surface = Color(0xFFE5E9F0),
|
||||||
|
onSurface = Color(0xFF2E3440),
|
||||||
|
surfaceVariant = Color(0xFFffffff),
|
||||||
|
onSurfaceVariant = Color(0xFF2E3440),
|
||||||
|
surfaceTint = Color(0xFF5E81AC),
|
||||||
|
inverseSurface = Color(0xFF3B4252),
|
||||||
|
inverseOnSurface = Color(0xFFECEFF4),
|
||||||
|
outline = Color(0xFF2E3440),
|
||||||
|
onError = Color(0xFFECEFF4),
|
||||||
|
errorContainer = Color(0xFFBF616A),
|
||||||
|
onErrorContainer = Color(0xFF000000),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||||||
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.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||||
@@ -323,7 +323,7 @@ private fun TrackInfoDialogHomePreviews(
|
|||||||
@PreviewParameter(TrackInfoDialogHomePreviewProvider::class)
|
@PreviewParameter(TrackInfoDialogHomePreviewProvider::class)
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
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.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -229,7 +229,7 @@ private fun BaseSelector(
|
|||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun TrackStatusSelectorPreviews() {
|
private fun TrackStatusSelectorPreviews() {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
TrackStatusSelector(
|
TrackStatusSelector(
|
||||||
selection = 1,
|
selection = 1,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.animation.slideInVertically
|
|||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
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
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -22,7 +23,6 @@ import androidx.compose.foundation.layout.paddingFromBaseline
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.selection.selectable
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
@@ -33,6 +33,7 @@ import androidx.compose.material.icons.filled.CheckCircle
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -40,7 +41,10 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
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.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -48,7 +52,11 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.capitalize
|
import androidx.compose.ui.text.capitalize
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
@@ -58,9 +66,11 @@ import androidx.compose.ui.text.toLowerCase
|
|||||||
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 androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.manga.components.MangaCover
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
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
|
||||||
@@ -188,13 +198,7 @@ fun TrackerSearch(
|
|||||||
key = { it.hashCode() },
|
key = { it.hashCode() },
|
||||||
) {
|
) {
|
||||||
SearchResultItem(
|
SearchResultItem(
|
||||||
title = it.title,
|
trackSearch = it,
|
||||||
coverUrl = it.cover_url,
|
|
||||||
type = it.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current),
|
|
||||||
startDate = it.start_date,
|
|
||||||
status = it.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current),
|
|
||||||
score = it.score,
|
|
||||||
description = it.summary.trim(),
|
|
||||||
selected = it == selected,
|
selected = it == selected,
|
||||||
onClick = { onSelectedChange(it) },
|
onClick = { onSelectedChange(it) },
|
||||||
)
|
)
|
||||||
@@ -214,18 +218,18 @@ fun TrackerSearch(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SearchResultItem(
|
private fun SearchResultItem(
|
||||||
title: String,
|
trackSearch: TrackSearch,
|
||||||
coverUrl: String,
|
|
||||||
type: String,
|
|
||||||
startDate: String,
|
|
||||||
status: String,
|
|
||||||
score: Float,
|
|
||||||
description: String,
|
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
|
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||||
|
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||||
|
val description = trackSearch.summary.trim()
|
||||||
val shape = RoundedCornerShape(16.dp)
|
val shape = RoundedCornerShape(16.dp)
|
||||||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||||
|
var dropDownMenuExpanded by remember { mutableStateOf(false) }
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -237,7 +241,10 @@ private fun SearchResultItem(
|
|||||||
color = borderColor,
|
color = borderColor,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
)
|
)
|
||||||
.selectable(selected = selected, onClick = onClick)
|
.combinedClickable(
|
||||||
|
onLongClick = { dropDownMenuExpanded = true },
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
) {
|
) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -251,28 +258,41 @@ private fun SearchResultItem(
|
|||||||
Column {
|
Column {
|
||||||
Row {
|
Row {
|
||||||
MangaCover.Book(
|
MangaCover.Book(
|
||||||
data = coverUrl,
|
data = trackSearch.cover_url,
|
||||||
modifier = Modifier.height(96.dp),
|
modifier = Modifier.height(96.dp),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = trackSearch.title,
|
||||||
modifier = Modifier.padding(end = 28.dp),
|
modifier = Modifier.padding(end = 28.dp),
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
|
SearchResultItemDropDownMenu(
|
||||||
|
expanded = dropDownMenuExpanded,
|
||||||
|
onCollapseMenu = { dropDownMenuExpanded = false },
|
||||||
|
onCopyName = {
|
||||||
|
clipboardManager.setText(AnnotatedString(trackSearch.title))
|
||||||
|
},
|
||||||
|
onOpenInBrowser = {
|
||||||
|
val url = trackSearch.tracking_url
|
||||||
|
if (url.isNotBlank()) {
|
||||||
|
context.openInBrowser(url)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
if (type.isNotBlank()) {
|
if (type.isNotBlank()) {
|
||||||
SearchResultItemDetails(
|
SearchResultItemDetails(
|
||||||
title = stringResource(MR.strings.track_type),
|
title = stringResource(MR.strings.track_type),
|
||||||
text = type,
|
text = type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (startDate.isNotBlank()) {
|
if (trackSearch.start_date.isNotBlank()) {
|
||||||
SearchResultItemDetails(
|
SearchResultItemDetails(
|
||||||
title = stringResource(MR.strings.label_started),
|
title = stringResource(MR.strings.label_started),
|
||||||
text = startDate,
|
text = trackSearch.start_date,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (status.isNotBlank()) {
|
if (status.isNotBlank()) {
|
||||||
@@ -281,10 +301,10 @@ private fun SearchResultItem(
|
|||||||
text = status,
|
text = status,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (score != -1f) {
|
if (trackSearch.score != -1f) {
|
||||||
SearchResultItemDetails(
|
SearchResultItemDetails(
|
||||||
title = stringResource(MR.strings.score),
|
title = stringResource(MR.strings.score),
|
||||||
text = score.toString(),
|
text = trackSearch.score.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,6 +324,33 @@ private fun SearchResultItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SearchResultItemDropDownMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
onCollapseMenu: () -> Unit,
|
||||||
|
onCopyName: () -> Unit,
|
||||||
|
onOpenInBrowser: () -> Unit,
|
||||||
|
) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onCollapseMenu,
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(MR.strings.action_copy_to_clipboard)) },
|
||||||
|
onClick = {
|
||||||
|
onCopyName()
|
||||||
|
onCollapseMenu()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(MR.strings.action_open_in_browser)) },
|
||||||
|
onClick = {
|
||||||
|
onOpenInBrowser()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SearchResultItemDetails(
|
private fun SearchResultItemDetails(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -333,5 +380,5 @@ private fun TrackerSearchPreviews(
|
|||||||
@PreviewParameter(TrackerSearchPreviewProvider::class)
|
@PreviewParameter(TrackerSearchPreviewProvider::class)
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
TachiyomiTheme { content() }
|
TachiyomiPreviewTheme { content() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
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 androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ private fun TrackLogoIconPreviews(
|
|||||||
@PreviewParameter(TrackLogoIconPreviewProvider::class)
|
@PreviewParameter(TrackLogoIconPreviewProvider::class)
|
||||||
tracker: Tracker,
|
tracker: Tracker,
|
||||||
) {
|
) {
|
||||||
TachiyomiTheme {
|
TachiyomiPreviewTheme {
|
||||||
TrackLogoIcon(
|
TrackLogoIcon(
|
||||||
tracker = tracker,
|
tracker = tracker,
|
||||||
onClick = null,
|
onClick = null,
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberRequestPackageInstallsPermissionState(): Boolean {
|
fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
var installGranted by remember { mutableStateOf(false) }
|
var installGranted by remember { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner.lifecycle) {
|
DisposableEffect(lifecycleOwner.lifecycle) {
|
||||||
val observer = object : DefaultLifecycleObserver {
|
val observer = object : DefaultLifecycleObserver {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.job.JobInfo
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -42,8 +41,6 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
|
|||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
|
||||||
import eu.kanade.tachiyomi.util.system.notify
|
import eu.kanade.tachiyomi.util.system.notify
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@@ -51,13 +48,8 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import logcat.AndroidLogcatLogger
|
import logcat.AndroidLogcatLogger
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.LogcatLogger
|
import logcat.LogcatLogger
|
||||||
import org.acra.config.httpSender
|
|
||||||
import org.acra.config.scheduler
|
|
||||||
import org.acra.ktx.initAcra
|
|
||||||
import org.acra.sender.HttpSender
|
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.core.preference.Preference
|
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.widget.WidgetManager
|
import tachiyomi.presentation.widget.WidgetManager
|
||||||
@@ -94,7 +86,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
Injekt.importModule(DomainModule())
|
Injekt.importModule(DomainModule())
|
||||||
|
|
||||||
setupAcra()
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
@@ -198,28 +189,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
return super.getPackageName()
|
return super.getPackageName()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAcra() {
|
|
||||||
if (isPreviewBuildType || isReleaseBuildType) {
|
|
||||||
initAcra {
|
|
||||||
buildConfigClass = BuildConfig::class.java
|
|
||||||
excludeMatchingSharedPreferencesKeys = listOf(
|
|
||||||
Preference.privateKey(".*"), ".*username.*", ".*password.*", ".*token.*",
|
|
||||||
)
|
|
||||||
|
|
||||||
httpSender {
|
|
||||||
uri = BuildConfig.ACRA_URI
|
|
||||||
httpMethod = HttpSender.Method.PUT
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduler {
|
|
||||||
requiresBatteryNotLow = true
|
|
||||||
requiresDeviceIdle = true
|
|
||||||
requiresNetworkType = JobInfo.NETWORK_TYPE_UNMETERED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupNotificationChannels() {
|
private fun setupNotificationChannels() {
|
||||||
try {
|
try {
|
||||||
Notifications.createChannels(this)
|
Notifications.createChannels(this)
|
||||||
|
|||||||
@@ -374,13 +374,6 @@ object Migrations {
|
|||||||
uiPreferences.relativeTime().set(false)
|
uiPreferences.relativeTime().set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 107) {
|
|
||||||
replacePreferences(
|
|
||||||
preferenceStore = preferenceStore,
|
|
||||||
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
|
|
||||||
newKey = { Preference.privateKey(it) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (oldVersion < 113) {
|
if (oldVersion < 113) {
|
||||||
val prefsToReplace = listOf(
|
val prefsToReplace = listOf(
|
||||||
"pref_download_only",
|
"pref_download_only",
|
||||||
@@ -407,7 +400,19 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 114) {
|
if (oldVersion < 114) {
|
||||||
sourcePreferences.extensionRepos().getAndSet {
|
sourcePreferences.extensionRepos().getAndSet {
|
||||||
it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet()
|
it.map { repo -> "https://raw.githubusercontent.com/$repo/repo" }.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 116) {
|
||||||
|
replacePreferences(
|
||||||
|
preferenceStore = preferenceStore,
|
||||||
|
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
|
||||||
|
newKey = { Preference.privateKey(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (oldVersion < 117) {
|
||||||
|
prefs.edit {
|
||||||
|
remove(Preference.appStateKey("trusted_signatures"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class MangaRestorer(
|
|||||||
backupManga: BackupManga,
|
backupManga: BackupManga,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
) {
|
||||||
|
handler.await(inTransaction = true) {
|
||||||
val dbManga = findExistingManga(backupManga)
|
val dbManga = findExistingManga(backupManga)
|
||||||
val manga = backupManga.getMangaImpl()
|
val manga = backupManga.getMangaImpl()
|
||||||
val restoredManga = if (dbManga == null) {
|
val restoredManga = if (dbManga == null) {
|
||||||
@@ -74,6 +75,7 @@ class MangaRestorer(
|
|||||||
tracks = backupManga.tracking,
|
tracks = backupManga.tracking,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun findExistingManga(backupManga: BackupManga): Manga? {
|
private suspend fun findExistingManga(backupManga: BackupManga): Manga? {
|
||||||
return getMangaByUrlAndSourceId.await(backupManga.url, backupManga.source)
|
return getMangaByUrlAndSourceId.await(backupManga.url, backupManga.source)
|
||||||
|
|||||||
@@ -103,12 +103,15 @@ class DownloadCache(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirLock.withLock {
|
||||||
try {
|
try {
|
||||||
|
if (diskCacheFile.exists()) {
|
||||||
val diskCache = diskCacheFile.inputStream().use {
|
val diskCache = diskCacheFile.inputStream().use {
|
||||||
ProtoBuf.decodeFromByteArray<RootDirectory>(it.readBytes())
|
ProtoBuf.decodeFromByteArray<RootDirectory>(it.readBytes())
|
||||||
}
|
}
|
||||||
rootDownloadsDir = diskCache
|
rootDownloadsDir = diskCache
|
||||||
lastRenew = System.currentTimeMillis()
|
lastRenew = System.currentTimeMillis()
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to initialize disk cache" }
|
||||||
diskCacheFile.delete()
|
diskCacheFile.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
|
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
|
||||||
@@ -18,7 +19,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.preference.plusAssign
|
|
||||||
import tachiyomi.core.util.lang.launchNow
|
import tachiyomi.core.util.lang.launchNow
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
@@ -34,13 +34,11 @@ import java.util.Locale
|
|||||||
* To avoid malicious distribution, every extension must be signed and it will only be loaded if its
|
* To avoid malicious distribution, every extension must be signed and it will only be loaded if its
|
||||||
* signature is trusted, otherwise the user will be prompted with a warning to trust it before being
|
* signature is trusted, otherwise the user will be prompted with a warning to trust it before being
|
||||||
* loaded.
|
* loaded.
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
* @param preferences The application preferences.
|
|
||||||
*/
|
*/
|
||||||
class ExtensionManager(
|
class ExtensionManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val preferences: SourcePreferences = Injekt.get(),
|
private val preferences: SourcePreferences = Injekt.get(),
|
||||||
|
private val trustExtension: TrustExtension = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var isInitialized = false
|
var isInitialized = false
|
||||||
@@ -180,14 +178,22 @@ class ExtensionManager(
|
|||||||
val pkgName = installedExt.pkgName
|
val pkgName = installedExt.pkgName
|
||||||
val availableExt = availableExtensions.find { it.pkgName == pkgName }
|
val availableExt = availableExtensions.find { it.pkgName == pkgName }
|
||||||
|
|
||||||
if (!installedExt.isUnofficial && availableExt == null && !installedExt.isObsolete) {
|
if (availableExt == null && !installedExt.isObsolete) {
|
||||||
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
|
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
|
||||||
changed = true
|
changed = true
|
||||||
} else if (availableExt != null) {
|
} else if (availableExt != null) {
|
||||||
val hasUpdate = installedExt.updateExists(availableExt)
|
val hasUpdate = installedExt.updateExists(availableExt)
|
||||||
|
|
||||||
if (installedExt.hasUpdate != hasUpdate) {
|
if (installedExt.hasUpdate != hasUpdate) {
|
||||||
mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
|
mutInstalledExtensions[index] = installedExt.copy(
|
||||||
|
hasUpdate = hasUpdate,
|
||||||
|
repoUrl = availableExt.repoUrl,
|
||||||
|
)
|
||||||
|
changed = true
|
||||||
|
} else {
|
||||||
|
mutInstalledExtensions[index] = installedExt.copy(
|
||||||
|
repoUrl = availableExt.repoUrl,
|
||||||
|
)
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,18 +255,19 @@ class ExtensionManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given signature to the list of trusted signatures. It also loads in background the
|
* Adds the given extension to the list of trusted extensions. It also loads in background the
|
||||||
* extensions that match this signature.
|
* now trusted extensions.
|
||||||
*
|
*
|
||||||
* @param signature The signature to whitelist.
|
* @param extension the extension to trust
|
||||||
*/
|
*/
|
||||||
fun trustSignature(signature: String) {
|
fun trust(extension: Extension.Untrusted) {
|
||||||
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
|
val untrustedPkgNames = _untrustedExtensionsFlow.value.map { it.pkgName }.toSet()
|
||||||
if (signature !in untrustedSignatures) return
|
if (extension.pkgName !in untrustedPkgNames) return
|
||||||
|
|
||||||
preferences.trustedSignatures() += signature
|
trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash)
|
||||||
|
|
||||||
val nowTrustedExtensions = _untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
|
val nowTrustedExtensions = _untrustedExtensionsFlow.value
|
||||||
|
.filter { it.pkgName == extension.pkgName && it.versionCode == extension.versionCode }
|
||||||
_untrustedExtensionsFlow.value -= nowTrustedExtensions
|
_untrustedExtensionsFlow.value -= nowTrustedExtensions
|
||||||
|
|
||||||
launchNow {
|
launchNow {
|
||||||
@@ -354,7 +361,7 @@ class ExtensionManager(
|
|||||||
|
|
||||||
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
||||||
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
|
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
|
||||||
if (isUnofficial || availableExt == null) return false
|
?: return false
|
||||||
|
|
||||||
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
|
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
package eu.kanade.tachiyomi.extension.api
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.domain.source.interactor.OFFICIAL_REPO_BASE_URL
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
@@ -36,25 +35,11 @@ internal class ExtensionApi {
|
|||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val extensions = buildList {
|
sourcePreferences.extensionRepos().get().flatMap { getExtensions(it) }
|
||||||
addAll(getExtensions(OFFICIAL_REPO_BASE_URL, true))
|
|
||||||
sourcePreferences.extensionRepos().get().map { addAll(getExtensions(it, false)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity check - a small number of extensions probably means something broke
|
|
||||||
// with the repo generator
|
|
||||||
if (extensions.size < 50) {
|
|
||||||
throw Exception()
|
|
||||||
}
|
|
||||||
|
|
||||||
extensions
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getExtensions(
|
private suspend fun getExtensions(repoBaseUrl: String): List<Extension.Available> {
|
||||||
repoBaseUrl: String,
|
|
||||||
isOfficialRepo: Boolean,
|
|
||||||
): List<Extension.Available> {
|
|
||||||
return try {
|
return try {
|
||||||
val response = networkService.client
|
val response = networkService.client
|
||||||
.newCall(GET("$repoBaseUrl/index.min.json"))
|
.newCall(GET("$repoBaseUrl/index.min.json"))
|
||||||
@@ -63,7 +48,7 @@ internal class ExtensionApi {
|
|||||||
with(json) {
|
with(json) {
|
||||||
response
|
response
|
||||||
.parseAs<List<ExtensionJsonObject>>()
|
.parseAs<List<ExtensionJsonObject>>()
|
||||||
.toExtensions(repoBaseUrl, isRepoSource = !isOfficialRepo)
|
.toExtensions(repoBaseUrl)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" }
|
logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" }
|
||||||
@@ -98,7 +83,7 @@ internal class ExtensionApi {
|
|||||||
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
|
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
|
||||||
val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
|
val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
|
||||||
val hasUpdatedLib = availableExt.libVersion > installedExt.libVersion
|
val hasUpdatedLib = availableExt.libVersion > installedExt.libVersion
|
||||||
val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer || hasUpdatedLib)
|
val hasUpdate = hasUpdatedVer || hasUpdatedLib
|
||||||
if (hasUpdate) {
|
if (hasUpdate) {
|
||||||
extensionsWithUpdate.add(installedExt)
|
extensionsWithUpdate.add(installedExt)
|
||||||
}
|
}
|
||||||
@@ -111,10 +96,7 @@ internal class ExtensionApi {
|
|||||||
return extensionsWithUpdate
|
return extensionsWithUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<ExtensionJsonObject>.toExtensions(
|
private fun List<ExtensionJsonObject>.toExtensions(repoUrl: String): List<Extension.Available> {
|
||||||
repoUrl: String,
|
|
||||||
isRepoSource: Boolean,
|
|
||||||
): List<Extension.Available> {
|
|
||||||
return this
|
return this
|
||||||
.filter {
|
.filter {
|
||||||
val libVersion = it.extractLibVersion()
|
val libVersion = it.extractLibVersion()
|
||||||
@@ -133,7 +115,6 @@ internal class ExtensionApi {
|
|||||||
apkName = it.apk,
|
apkName = it.apk,
|
||||||
iconUrl = "$repoUrl/icon/${it.pkg}.png",
|
iconUrl = "$repoUrl/icon/${it.pkg}.png",
|
||||||
repoUrl = repoUrl,
|
repoUrl = repoUrl,
|
||||||
isRepoSource = isRepoSource,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ sealed class Extension {
|
|||||||
val icon: Drawable?,
|
val icon: Drawable?,
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
val isUnofficial: Boolean = false,
|
|
||||||
val isShared: Boolean,
|
val isShared: Boolean,
|
||||||
val repoUrl: String? = null,
|
val repoUrl: String? = null,
|
||||||
val isRepoSource: Boolean = false,
|
|
||||||
) : Extension()
|
) : Extension()
|
||||||
|
|
||||||
data class Available(
|
data class Available(
|
||||||
@@ -45,7 +43,6 @@ sealed class Extension {
|
|||||||
val apkName: String,
|
val apkName: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val repoUrl: String,
|
val repoUrl: String,
|
||||||
val isRepoSource: Boolean,
|
|
||||||
) : Extension() {
|
) : Extension() {
|
||||||
|
|
||||||
data class Source(
|
data class Source(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.content.pm.PackageManager
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import dalvik.system.PathClassLoader
|
import dalvik.system.PathClassLoader
|
||||||
|
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
@@ -15,7 +16,6 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
|
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
|
||||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -41,6 +41,7 @@ import java.io.File
|
|||||||
internal object ExtensionLoader {
|
internal object ExtensionLoader {
|
||||||
|
|
||||||
private val preferences: SourcePreferences by injectLazy()
|
private val preferences: SourcePreferences by injectLazy()
|
||||||
|
private val trustExtension: TrustExtension by injectLazy()
|
||||||
private val loadNsfwSource by lazy {
|
private val loadNsfwSource by lazy {
|
||||||
preferences.showNsfwSource().get()
|
preferences.showNsfwSource().get()
|
||||||
}
|
}
|
||||||
@@ -49,8 +50,6 @@ internal object ExtensionLoader {
|
|||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||||
private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme"
|
|
||||||
private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog"
|
|
||||||
const val LIB_VERSION_MIN = 1.4
|
const val LIB_VERSION_MIN = 1.4
|
||||||
const val LIB_VERSION_MAX = 1.5
|
const val LIB_VERSION_MAX = 1.5
|
||||||
|
|
||||||
@@ -60,9 +59,6 @@ internal object ExtensionLoader {
|
|||||||
PackageManager.GET_SIGNATURES or
|
PackageManager.GET_SIGNATURES or
|
||||||
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0)
|
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0)
|
||||||
|
|
||||||
// inorichi's key
|
|
||||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
|
||||||
|
|
||||||
private const val PRIVATE_EXTENSION_EXTENSION = "ext"
|
private const val PRIVATE_EXTENSION_EXTENSION = "ext"
|
||||||
|
|
||||||
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
|
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
|
||||||
@@ -119,12 +115,6 @@ internal object ExtensionLoader {
|
|||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
fun loadExtensions(context: Context): List<LoadResult> {
|
fun loadExtensions(context: Context): List<LoadResult> {
|
||||||
// Always make users trust unknown extensions on cold starts in non-dev builds
|
|
||||||
// due to inherent security risks
|
|
||||||
if (!isDevFlavor) {
|
|
||||||
preferences.trustedSignatures().delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
val pkgManager = context.packageManager
|
val pkgManager = context.packageManager
|
||||||
|
|
||||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
@@ -262,7 +252,7 @@ internal object ExtensionLoader {
|
|||||||
if (signatures.isNullOrEmpty()) {
|
if (signatures.isNullOrEmpty()) {
|
||||||
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
||||||
return LoadResult.Error
|
return LoadResult.Error
|
||||||
} else if (!hasTrustedSignature(signatures)) {
|
} else if (!trustExtension.isTrusted(pkgInfo, signatures.last())) {
|
||||||
val extension = Extension.Untrusted(
|
val extension = Extension.Untrusted(
|
||||||
extName,
|
extName,
|
||||||
pkgName,
|
pkgName,
|
||||||
@@ -281,9 +271,6 @@ internal object ExtensionLoader {
|
|||||||
return LoadResult.Error
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
|
||||||
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
|
|
||||||
|
|
||||||
val classLoader = try {
|
val classLoader = try {
|
||||||
PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -333,7 +320,6 @@ internal object ExtensionLoader {
|
|||||||
isNsfw = isNsfw,
|
isNsfw = isNsfw,
|
||||||
sources = sources,
|
sources = sources,
|
||||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||||
isUnofficial = !isOfficiallySigned(signatures),
|
|
||||||
icon = appInfo.loadIcon(pkgManager),
|
icon = appInfo.loadIcon(pkgManager),
|
||||||
isShared = extensionInfo.isShared,
|
isShared = extensionInfo.isShared,
|
||||||
)
|
)
|
||||||
@@ -393,19 +379,6 @@ internal object ExtensionLoader {
|
|||||||
?.toList()
|
?.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasTrustedSignature(signatures: List<String>): Boolean {
|
|
||||||
if (officialSignature in signatures) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val trustedSignatures = preferences.trustedSignatures().get()
|
|
||||||
return trustedSignatures.any { signatures.contains(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isOfficiallySigned(signatures: List<String>): Boolean {
|
|
||||||
return signatures.all { it == officialSignature }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't
|
* On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't
|
||||||
* have sourceDir which breaks assets loading (used for getting icon here).
|
* have sourceDir which breaks assets loading (used for getting icon here).
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ interface ThemingDelegate {
|
|||||||
AppTheme.MIDNIGHT_DUSK -> {
|
AppTheme.MIDNIGHT_DUSK -> {
|
||||||
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
||||||
}
|
}
|
||||||
|
AppTheme.NORD -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Nord
|
||||||
|
}
|
||||||
AppTheme.STRAWBERRY_DAIQUIRI -> {
|
AppTheme.STRAWBERRY_DAIQUIRI -> {
|
||||||
resIds += R.style.Theme_Tachiyomi_StrawberryDaiquiri
|
resIds += R.style.Theme_Tachiyomi_StrawberryDaiquiri
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,8 +195,8 @@ class ExtensionsScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trustSignature(signatureHash: String) {
|
fun trustExtension(extension: Extension.Untrusted) {
|
||||||
extensionManager.trustSignature(signatureHash)
|
extensionManager.trust(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension
|
package eu.kanade.tachiyomi.ui.browse.extension
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Translate
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -10,6 +8,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
|||||||
import eu.kanade.presentation.browse.ExtensionScreen
|
import eu.kanade.presentation.browse.ExtensionScreen
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.TabContent
|
import eu.kanade.presentation.components.TabContent
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
||||||
@@ -29,11 +28,14 @@ fun extensionsTab(
|
|||||||
badgeNumber = state.updates.takeIf { it > 0 },
|
badgeNumber = state.updates.takeIf { it > 0 },
|
||||||
searchEnabled = true,
|
searchEnabled = true,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
AppBar.Action(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(MR.strings.action_filter),
|
title = stringResource(MR.strings.action_filter),
|
||||||
icon = Icons.Outlined.Translate,
|
|
||||||
onClick = { navigator.push(ExtensionFilterScreen()) },
|
onClick = { navigator.push(ExtensionFilterScreen()) },
|
||||||
),
|
),
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.label_extension_repos),
|
||||||
|
onClick = { navigator.push(ExtensionReposScreen()) },
|
||||||
|
),
|
||||||
),
|
),
|
||||||
content = { contentPadding, _ ->
|
content = { contentPadding, _ ->
|
||||||
ExtensionScreen(
|
ExtensionScreen(
|
||||||
@@ -61,7 +63,7 @@ fun extensionsTab(
|
|||||||
},
|
},
|
||||||
onInstallExtension = extensionsScreenModel::installExtension,
|
onInstallExtension = extensionsScreenModel::installExtension,
|
||||||
onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) },
|
onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) },
|
||||||
onTrustExtension = { extensionsScreenModel.trustSignature(it.signatureHash) },
|
onTrustExtension = { extensionsScreenModel.trustExtension(it) },
|
||||||
onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) },
|
onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) },
|
||||||
onUpdateExtension = extensionsScreenModel::updateExtension,
|
onUpdateExtension = extensionsScreenModel::updateExtension,
|
||||||
onRefresh = extensionsScreenModel::findAvailableExtensions,
|
onRefresh = extensionsScreenModel::findAvailableExtensions,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
@@ -30,13 +29,11 @@ data class ExtensionDetailsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
|
|
||||||
ExtensionDetailsScreen(
|
ExtensionDetailsScreen(
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
state = state,
|
state = state,
|
||||||
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
|
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
|
||||||
onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) },
|
|
||||||
onClickEnableAll = { screenModel.toggleSources(true) },
|
onClickEnableAll = { screenModel.toggleSources(true) },
|
||||||
onClickDisableAll = { screenModel.toggleSources(false) },
|
onClickDisableAll = { screenModel.toggleSources(false) },
|
||||||
onClickClearCookies = screenModel::clearCookies,
|
onClickClearCookies = screenModel::clearCookies,
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ import tachiyomi.core.util.system.logcat
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
private const val URL_EXTENSION_COMMITS =
|
|
||||||
"https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
|
|
||||||
|
|
||||||
class ExtensionDetailsScreenModel(
|
class ExtensionDetailsScreenModel(
|
||||||
pkgName: String,
|
pkgName: String,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -86,16 +83,6 @@ class ExtensionDetailsScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChangelogUrl(): String {
|
|
||||||
val extension = state.value.extension ?: return ""
|
|
||||||
|
|
||||||
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
|
||||||
val pkgFactory = extension.pkgFactory
|
|
||||||
|
|
||||||
// Falling back on GitHub commit history because there is no explicit changelog in extension
|
|
||||||
return createUrl(URL_EXTENSION_COMMITS, pkgName, pkgFactory)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearCookies() {
|
fun clearCookies() {
|
||||||
val extension = state.value.extension ?: return
|
val extension = state.value.extension ?: return
|
||||||
|
|
||||||
@@ -131,22 +118,6 @@ class ExtensionDetailsScreenModel(
|
|||||||
?.let { toggleSource.await(it, enable) }
|
?.let { toggleSource.await(it, enable) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUrl(
|
|
||||||
url: String,
|
|
||||||
pkgName: String,
|
|
||||||
pkgFactory: String?,
|
|
||||||
path: String = "",
|
|
||||||
): String {
|
|
||||||
return if (!pkgFactory.isNullOrEmpty()) {
|
|
||||||
when (path.isEmpty()) {
|
|
||||||
true -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory"
|
|
||||||
else -> "$url/multisrc/overrides/$pkgFactory/" + (pkgName.split(".").lastOrNull() ?: "") + path
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
url + "/src/" + pkgName.replace(".", "/") + path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val extension: Extension.Installed? = null,
|
val extension: Extension.Installed? = null,
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ class LibraryScreenModel(
|
|||||||
prefs.filterStarted,
|
prefs.filterStarted,
|
||||||
prefs.filterBookmarked,
|
prefs.filterBookmarked,
|
||||||
prefs.filterCompleted,
|
prefs.filterCompleted,
|
||||||
|
prefs.filterIntervalCustom,
|
||||||
) + trackFilter.values
|
) + trackFilter.values
|
||||||
).any { it != TriState.DISABLED }
|
).any { it != TriState.DISABLED }
|
||||||
}
|
}
|
||||||
@@ -178,12 +179,13 @@ class LibraryScreenModel(
|
|||||||
): LibraryMap {
|
): LibraryMap {
|
||||||
val prefs = getLibraryItemPreferencesFlow().first()
|
val prefs = getLibraryItemPreferencesFlow().first()
|
||||||
val downloadedOnly = prefs.globalFilterDownloaded
|
val downloadedOnly = prefs.globalFilterDownloaded
|
||||||
val filterDownloaded =
|
val skipOutsideReleasePeriod = prefs.skipOutsideReleasePeriod
|
||||||
if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded
|
val filterDownloaded = if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded
|
||||||
val filterUnread = prefs.filterUnread
|
val filterUnread = prefs.filterUnread
|
||||||
val filterStarted = prefs.filterStarted
|
val filterStarted = prefs.filterStarted
|
||||||
val filterBookmarked = prefs.filterBookmarked
|
val filterBookmarked = prefs.filterBookmarked
|
||||||
val filterCompleted = prefs.filterCompleted
|
val filterCompleted = prefs.filterCompleted
|
||||||
|
val filterIntervalCustom = prefs.filterIntervalCustom
|
||||||
|
|
||||||
val isNotLoggedInAnyTrack = loggedInTrackers.isEmpty()
|
val isNotLoggedInAnyTrack = loggedInTrackers.isEmpty()
|
||||||
|
|
||||||
@@ -215,6 +217,14 @@ class LibraryScreenModel(
|
|||||||
applyFilter(filterCompleted) { it.libraryManga.manga.status.toInt() == SManga.COMPLETED }
|
applyFilter(filterCompleted) { it.libraryManga.manga.status.toInt() == SManga.COMPLETED }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val filterFnIntervalCustom: (LibraryItem) -> Boolean = {
|
||||||
|
if (skipOutsideReleasePeriod) {
|
||||||
|
applyFilter(filterIntervalCustom) { it.libraryManga.manga.fetchInterval < 0 }
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
||||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||||
|
|
||||||
@@ -225,7 +235,7 @@ class LibraryScreenModel(
|
|||||||
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
||||||
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
|
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
|
||||||
|
|
||||||
return@tracking !isExcluded && isIncluded
|
!isExcluded && isIncluded
|
||||||
}
|
}
|
||||||
|
|
||||||
val filterFn: (LibraryItem) -> Boolean = {
|
val filterFn: (LibraryItem) -> Boolean = {
|
||||||
@@ -234,6 +244,7 @@ class LibraryScreenModel(
|
|||||||
filterFnStarted(it) &&
|
filterFnStarted(it) &&
|
||||||
filterFnBookmarked(it) &&
|
filterFnBookmarked(it) &&
|
||||||
filterFnCompleted(it) &&
|
filterFnCompleted(it) &&
|
||||||
|
filterFnIntervalCustom(it) &&
|
||||||
filterFnTracking(it)
|
filterFnTracking(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,6 +331,7 @@ class LibraryScreenModel(
|
|||||||
libraryPreferences.downloadBadge().changes(),
|
libraryPreferences.downloadBadge().changes(),
|
||||||
libraryPreferences.localBadge().changes(),
|
libraryPreferences.localBadge().changes(),
|
||||||
libraryPreferences.languageBadge().changes(),
|
libraryPreferences.languageBadge().changes(),
|
||||||
|
libraryPreferences.autoUpdateMangaRestrictions().changes(),
|
||||||
|
|
||||||
preferences.downloadedOnly().changes(),
|
preferences.downloadedOnly().changes(),
|
||||||
libraryPreferences.filterDownloaded().changes(),
|
libraryPreferences.filterDownloaded().changes(),
|
||||||
@@ -327,21 +339,23 @@ class LibraryScreenModel(
|
|||||||
libraryPreferences.filterStarted().changes(),
|
libraryPreferences.filterStarted().changes(),
|
||||||
libraryPreferences.filterBookmarked().changes(),
|
libraryPreferences.filterBookmarked().changes(),
|
||||||
libraryPreferences.filterCompleted().changes(),
|
libraryPreferences.filterCompleted().changes(),
|
||||||
transform = {
|
libraryPreferences.filterIntervalCustom().changes(),
|
||||||
|
) {
|
||||||
ItemPreferences(
|
ItemPreferences(
|
||||||
downloadBadge = it[0] as Boolean,
|
downloadBadge = it[0] as Boolean,
|
||||||
localBadge = it[1] as Boolean,
|
localBadge = it[1] as Boolean,
|
||||||
languageBadge = it[2] as Boolean,
|
languageBadge = it[2] as Boolean,
|
||||||
globalFilterDownloaded = it[3] as Boolean,
|
skipOutsideReleasePeriod = LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in (it[3] as Set<*>),
|
||||||
filterDownloaded = it[4] as TriState,
|
globalFilterDownloaded = it[4] as Boolean,
|
||||||
filterUnread = it[5] as TriState,
|
filterDownloaded = it[5] as TriState,
|
||||||
filterStarted = it[6] as TriState,
|
filterUnread = it[6] as TriState,
|
||||||
filterBookmarked = it[7] as TriState,
|
filterStarted = it[7] as TriState,
|
||||||
filterCompleted = it[8] as TriState,
|
filterBookmarked = it[8] as TriState,
|
||||||
)
|
filterCompleted = it[9] as TriState,
|
||||||
},
|
filterIntervalCustom = it[10] as TriState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the categories and all its manga from the database.
|
* Get the categories and all its manga from the database.
|
||||||
@@ -699,6 +713,7 @@ class LibraryScreenModel(
|
|||||||
val downloadBadge: Boolean,
|
val downloadBadge: Boolean,
|
||||||
val localBadge: Boolean,
|
val localBadge: Boolean,
|
||||||
val languageBadge: Boolean,
|
val languageBadge: Boolean,
|
||||||
|
val skipOutsideReleasePeriod: Boolean,
|
||||||
|
|
||||||
val globalFilterDownloaded: Boolean,
|
val globalFilterDownloaded: Boolean,
|
||||||
val filterDownloaded: TriState,
|
val filterDownloaded: TriState,
|
||||||
@@ -706,6 +721,7 @@ class LibraryScreenModel(
|
|||||||
val filterStarted: TriState,
|
val filterStarted: TriState,
|
||||||
val filterBookmarked: TriState,
|
val filterBookmarked: TriState,
|
||||||
val filterCompleted: TriState,
|
val filterCompleted: TriState,
|
||||||
|
val filterIntervalCustom: TriState,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ import eu.kanade.presentation.components.AppStateBanners
|
|||||||
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
|
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
|
||||||
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
|
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
|
||||||
import eu.kanade.presentation.components.IndexingBannerBackgroundColor
|
import eu.kanade.presentation.components.IndexingBannerBackgroundColor
|
||||||
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||||
|
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||||
import eu.kanade.presentation.util.AssistContentScreen
|
import eu.kanade.presentation.util.AssistContentScreen
|
||||||
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
|
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
@@ -63,7 +65,6 @@ import eu.kanade.tachiyomi.Migrations
|
|||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
|
|
||||||
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
|
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
@@ -72,7 +73,6 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
|||||||
import eu.kanade.tachiyomi.ui.deeplink.DeepLinkScreen
|
import eu.kanade.tachiyomi.ui.deeplink.DeepLinkScreen
|
||||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
|
|
||||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
|
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
|
||||||
@@ -91,7 +91,6 @@ import tachiyomi.core.Constants
|
|||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -312,27 +311,6 @@ class MainActivity : BaseActivity() {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun CheckForUpdates() {
|
private fun CheckForUpdates() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
|
||||||
|
|
||||||
// App updates
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
|
||||||
try {
|
|
||||||
val result = AppUpdateChecker().checkForUpdate(context)
|
|
||||||
if (result is GetApplicationRelease.Result.NewUpdate) {
|
|
||||||
val updateScreen = NewUpdateScreen(
|
|
||||||
versionName = result.release.version,
|
|
||||||
changelogInfo = result.release.info,
|
|
||||||
releaseLink = result.release.releaseLink,
|
|
||||||
downloadLink = result.release.getDownloadLink(),
|
|
||||||
)
|
|
||||||
navigator.push(updateScreen)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extensions updates
|
// Extensions updates
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -444,6 +422,21 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
Intent.ACTION_VIEW -> {
|
||||||
|
// Handling opening of backup files
|
||||||
|
if (intent.data.toString().endsWith(".tachibk")) {
|
||||||
|
navigator.popUntilRoot()
|
||||||
|
navigator.push(RestoreBackupScreen(intent.data.toString()))
|
||||||
|
}
|
||||||
|
// Deep link to add extension repo
|
||||||
|
else if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") {
|
||||||
|
intent.data?.getQueryParameter("url")?.let { repoUrl ->
|
||||||
|
navigator.popUntilRoot()
|
||||||
|
navigator.push(ExtensionReposScreen(repoUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -378,14 +378,17 @@ class MangaScreenModel(
|
|||||||
|
|
||||||
fun setFetchInterval(manga: Manga, interval: Int) {
|
fun setFetchInterval(manga: Manga, interval: Int) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
|
if (
|
||||||
updateManga.awaitUpdateFetchInterval(
|
updateManga.awaitUpdateFetchInterval(
|
||||||
// Custom intervals are negative
|
// Custom intervals are negative
|
||||||
manga.copy(fetchInterval = -interval),
|
manga.copy(fetchInterval = -interval),
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
val updatedManga = mangaRepository.getMangaById(manga.id)
|
val updatedManga = mangaRepository.getMangaById(manga.id)
|
||||||
updateSuccessState { it.copy(manga = updatedManga) }
|
updateSuccessState { it.copy(manga = updatedManga) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the manga has any downloads.
|
* Returns true if the manga has any downloads.
|
||||||
|
|||||||
@@ -462,7 +462,8 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
if (isNextChapterDownloaded) return@launchIO
|
if (!isNextChapterDownloaded) return@launchIO
|
||||||
|
|
||||||
val chaptersToDownload = getNextChapters.await(manga.id, nextChapter.id!!).run {
|
val chaptersToDownload = getNextChapters.await(manga.id, nextChapter.id!!).run {
|
||||||
if (readerPreferences.skipDupe().get()) {
|
if (readerPreferences.skipDupe().get()) {
|
||||||
removeDuplicates(nextChapter.toDomainChapter()!!)
|
removeDuplicates(nextChapter.toDomainChapter()!!)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import java.io.File
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.PipedInputStream
|
import java.io.PipedInputStream
|
||||||
import java.io.PipedOutputStream
|
import java.io.PipedOutputStream
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loader used to load a chapter from a .rar or .cbr file.
|
* Loader used to load a chapter from a .rar or .cbr file.
|
||||||
@@ -20,13 +21,18 @@ internal class RarPageLoader(file: File) : PageLoader() {
|
|||||||
|
|
||||||
override var isLocal: Boolean = true
|
override var isLocal: Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pool for copying compressed files to an input stream.
|
||||||
|
*/
|
||||||
|
private val pool = Executors.newFixedThreadPool(1)
|
||||||
|
|
||||||
override suspend fun getPages(): List<ReaderPage> {
|
override suspend fun getPages(): List<ReaderPage> {
|
||||||
return rar.fileHeaders.asSequence()
|
return rar.fileHeaders.asSequence()
|
||||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
|
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
|
||||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||||
.mapIndexed { i, header ->
|
.mapIndexed { i, header ->
|
||||||
ReaderPage(i).apply {
|
ReaderPage(i).apply {
|
||||||
stream = { getStream(rar, header) }
|
stream = { getStream(header) }
|
||||||
status = Page.State.READY
|
status = Page.State.READY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,15 +46,16 @@ internal class RarPageLoader(file: File) : PageLoader() {
|
|||||||
override fun recycle() {
|
override fun recycle() {
|
||||||
super.recycle()
|
super.recycle()
|
||||||
rar.close()
|
rar.close()
|
||||||
|
pool.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an input stream for the given [header].
|
* Returns an input stream for the given [header].
|
||||||
*/
|
*/
|
||||||
private fun getStream(rar: Archive, header: FileHeader): InputStream {
|
private fun getStream(header: FileHeader): InputStream {
|
||||||
val pipeIn = PipedInputStream()
|
val pipeIn = PipedInputStream()
|
||||||
val pipeOut = PipedOutputStream(pipeIn)
|
val pipeOut = PipedOutputStream(pipeIn)
|
||||||
synchronized(this) {
|
pool.execute {
|
||||||
try {
|
try {
|
||||||
pipeOut.use {
|
pipeOut.use {
|
||||||
rar.extractFile(header, it)
|
rar.extractFile(header, it)
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ import kotlinx.coroutines.MainScope
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.core.util.system.ImageUtil
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -136,6 +138,7 @@ class PagerPageHolder(
|
|||||||
|
|
||||||
val streamFn = page.stream ?: return
|
val streamFn = page.stream ?: return
|
||||||
|
|
||||||
|
try {
|
||||||
val (bais, isAnimated, background) = withIOContext {
|
val (bais, isAnimated, background) = withIOContext {
|
||||||
streamFn().buffered(16).use { stream ->
|
streamFn().buffered(16).use { stream ->
|
||||||
process(item, stream).use { itemStream ->
|
process(item, stream).use { itemStream ->
|
||||||
@@ -171,6 +174,12 @@ class PagerPageHolder(
|
|||||||
}
|
}
|
||||||
removeErrorLayout()
|
removeErrorLayout()
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
withUIContext {
|
||||||
|
setError()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {
|
private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {
|
||||||
@@ -235,7 +244,7 @@ class PagerPageHolder(
|
|||||||
*/
|
*/
|
||||||
private fun setError() {
|
private fun setError() {
|
||||||
progressIndicator.hide()
|
progressIndicator.hide()
|
||||||
showErrorLayout(withOpenInWebView = false)
|
showErrorLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageLoaded() {
|
override fun onImageLoaded() {
|
||||||
@@ -248,8 +257,7 @@ class PagerPageHolder(
|
|||||||
*/
|
*/
|
||||||
override fun onImageLoadError() {
|
override fun onImageLoadError() {
|
||||||
super.onImageLoadError()
|
super.onImageLoadError()
|
||||||
progressIndicator.hide()
|
setError()
|
||||||
showErrorLayout(withOpenInWebView = true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,23 +268,27 @@ class PagerPageHolder(
|
|||||||
viewer.activity.hideMenu()
|
viewer.activity.hideMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showErrorLayout(withOpenInWebView: Boolean): ReaderErrorBinding {
|
private fun showErrorLayout(): ReaderErrorBinding {
|
||||||
if (errorLayout == null) {
|
if (errorLayout == null) {
|
||||||
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true)
|
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
errorLayout?.actionRetry?.viewer = viewer
|
errorLayout?.actionRetry?.viewer = viewer
|
||||||
errorLayout?.actionRetry?.setOnClickListener {
|
errorLayout?.actionRetry?.setOnClickListener {
|
||||||
page.chapter.pageLoader?.retryPage(page)
|
page.chapter.pageLoader?.retryPage(page)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val imageUrl = page.imageUrl
|
val imageUrl = page.imageUrl
|
||||||
if (imageUrl.orEmpty().startsWith("http", true)) {
|
errorLayout?.actionOpenInWebView?.isVisible = imageUrl != null
|
||||||
|
if (imageUrl != null) {
|
||||||
|
if (imageUrl.startsWith("http", true)) {
|
||||||
errorLayout?.actionOpenInWebView?.viewer = viewer
|
errorLayout?.actionOpenInWebView?.viewer = viewer
|
||||||
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
||||||
val intent = WebViewActivity.newIntent(context, imageUrl!!)
|
val intent = WebViewActivity.newIntent(context, imageUrl)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
errorLayout?.actionOpenInWebView?.isVisible = withOpenInWebView
|
|
||||||
errorLayout?.root?.isVisible = true
|
errorLayout?.root?.isVisible = true
|
||||||
return errorLayout!!
|
return errorLayout!!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
pager.tapListener = { event ->
|
pager.tapListener = { event ->
|
||||||
val pos = PointF(event.x / pager.width, event.y / pager.height)
|
val viewPosition = IntArray(2)
|
||||||
|
pager.getLocationOnScreen(viewPosition)
|
||||||
|
val viewPositionRelativeToWindow = IntArray(2)
|
||||||
|
pager.getLocationInWindow(viewPositionRelativeToWindow)
|
||||||
|
val pos = PointF(
|
||||||
|
(event.rawX - viewPosition[0] + viewPositionRelativeToWindow[0]) / pager.width,
|
||||||
|
(event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / pager.height,
|
||||||
|
)
|
||||||
when (config.navigator.getAction(pos)) {
|
when (config.navigator.getAction(pos)) {
|
||||||
NavigationRegion.MENU -> activity.toggleMenu()
|
NavigationRegion.MENU -> activity.toggleMenu()
|
||||||
NavigationRegion.NEXT -> moveToNext()
|
NavigationRegion.NEXT -> moveToNext()
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.core.util.system.ImageUtil
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@@ -80,7 +82,7 @@ class WebtoonPageHolder(
|
|||||||
refreshLayoutParams()
|
refreshLayoutParams()
|
||||||
|
|
||||||
frame.onImageLoaded = { onImageDecoded() }
|
frame.onImageLoaded = { onImageDecoded() }
|
||||||
frame.onImageLoadError = { onImageDecodeError() }
|
frame.onImageLoadError = { setError() }
|
||||||
frame.onScaleChanged = { viewer.activity.hideMenu() }
|
frame.onScaleChanged = { viewer.activity.hideMenu() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +186,7 @@ class WebtoonPageHolder(
|
|||||||
|
|
||||||
val streamFn = page?.stream ?: return
|
val streamFn = page?.stream ?: return
|
||||||
|
|
||||||
|
try {
|
||||||
val (openStream, isAnimated) = withIOContext {
|
val (openStream, isAnimated) = withIOContext {
|
||||||
val stream = streamFn().buffered(16)
|
val stream = streamFn().buffered(16)
|
||||||
val openStream = process(stream)
|
val openStream = process(stream)
|
||||||
@@ -207,6 +210,12 @@ class WebtoonPageHolder(
|
|||||||
suspendCancellableCoroutine<Nothing> { continuation ->
|
suspendCancellableCoroutine<Nothing> { continuation ->
|
||||||
continuation.invokeOnCancellation { openStream.close() }
|
continuation.invokeOnCancellation { openStream.close() }
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
withUIContext {
|
||||||
|
setError()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun process(imageStream: BufferedInputStream): InputStream {
|
private fun process(imageStream: BufferedInputStream): InputStream {
|
||||||
@@ -240,7 +249,7 @@ class WebtoonPageHolder(
|
|||||||
*/
|
*/
|
||||||
private fun setError() {
|
private fun setError() {
|
||||||
progressContainer.isVisible = false
|
progressContainer.isVisible = false
|
||||||
initErrorLayout(withOpenInWebView = false)
|
initErrorLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -251,14 +260,6 @@ class WebtoonPageHolder(
|
|||||||
removeErrorLayout()
|
removeErrorLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the image fails to decode.
|
|
||||||
*/
|
|
||||||
private fun onImageDecodeError() {
|
|
||||||
progressContainer.isVisible = false
|
|
||||||
initErrorLayout(withOpenInWebView = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new progress bar.
|
* Creates a new progress bar.
|
||||||
*/
|
*/
|
||||||
@@ -278,22 +279,26 @@ class WebtoonPageHolder(
|
|||||||
/**
|
/**
|
||||||
* Initializes a button to retry pages.
|
* Initializes a button to retry pages.
|
||||||
*/
|
*/
|
||||||
private fun initErrorLayout(withOpenInWebView: Boolean): ReaderErrorBinding {
|
private fun initErrorLayout(): ReaderErrorBinding {
|
||||||
if (errorLayout == null) {
|
if (errorLayout == null) {
|
||||||
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true)
|
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true)
|
||||||
errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt())
|
errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt())
|
||||||
errorLayout?.actionRetry?.setOnClickListener {
|
errorLayout?.actionRetry?.setOnClickListener {
|
||||||
page?.let { it.chapter.pageLoader?.retryPage(it) }
|
page?.let { it.chapter.pageLoader?.retryPage(it) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val imageUrl = page?.imageUrl
|
val imageUrl = page?.imageUrl
|
||||||
if (imageUrl.orEmpty().startsWith("http", true)) {
|
errorLayout?.actionOpenInWebView?.isVisible = imageUrl != null
|
||||||
|
if (imageUrl != null) {
|
||||||
|
if (imageUrl.startsWith("http", true)) {
|
||||||
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
||||||
val intent = WebViewActivity.newIntent(context, imageUrl!!)
|
val intent = WebViewActivity.newIntent(context, imageUrl)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
errorLayout?.actionOpenInWebView?.isVisible = withOpenInWebView
|
|
||||||
return errorLayout!!
|
return errorLayout!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,9 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
if (!isZooming && doubleTapZoom) {
|
if (!isZooming && doubleTapZoom) {
|
||||||
if (scaleX != DEFAULT_RATE) {
|
if (scaleX != DEFAULT_RATE) {
|
||||||
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
|
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
|
||||||
|
layoutParams.height = originalHeight
|
||||||
|
halfHeight = layoutParams.height / 2
|
||||||
|
requestLayout()
|
||||||
} else {
|
} else {
|
||||||
val toScale = 2f
|
val toScale = 2f
|
||||||
val toX = (halfWidth - ev.x) * (toScale - 1)
|
val toX = (halfWidth - ev.x) * (toScale - 1)
|
||||||
|
|||||||
@@ -111,7 +111,14 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
recycler.tapListener = { event ->
|
recycler.tapListener = { event ->
|
||||||
val pos = PointF(event.x / recycler.width, event.y / recycler.height)
|
val viewPosition = IntArray(2)
|
||||||
|
recycler.getLocationOnScreen(viewPosition)
|
||||||
|
val viewPositionRelativeToWindow = IntArray(2)
|
||||||
|
recycler.getLocationInWindow(viewPositionRelativeToWindow)
|
||||||
|
val pos = PointF(
|
||||||
|
(event.rawX - viewPosition[0] + viewPositionRelativeToWindow[0]) / recycler.width,
|
||||||
|
(event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / recycler.height,
|
||||||
|
)
|
||||||
when (config.navigator.getAction(pos)) {
|
when (config.navigator.getAction(pos)) {
|
||||||
NavigationRegion.MENU -> activity.toggleMenu()
|
NavigationRegion.MENU -> activity.toggleMenu()
|
||||||
NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown()
|
NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown()
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ class CrashLogUtil(
|
|||||||
val availableExtension = availableExtensions[it.pkgName]
|
val availableExtension = availableExtensions[it.pkgName]
|
||||||
val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode
|
val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode
|
||||||
|
|
||||||
if (!hasUpdate && !it.isObsolete && !it.isUnofficial) return@mapNotNull null
|
if (!hasUpdate && !it.isObsolete) return@mapNotNull null
|
||||||
|
|
||||||
"""
|
"""
|
||||||
- ${it.name}
|
- ${it.name}
|
||||||
Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"}
|
Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"}
|
||||||
Obsolete: ${it.isObsolete} / Unofficial: ${it.isUnofficial}
|
Obsolete: ${it.isObsolete}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ fun Long.toDateKey(): Date {
|
|||||||
return Date.from(instant.truncatedTo(ChronoUnit.DAYS))
|
return Date.from(instant.truncatedTo(ChronoUnit.DAYS))
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val MILLISECONDS_IN_DAY = 86_400_000L
|
|
||||||
|
|
||||||
fun Date.toRelativeString(
|
fun Date.toRelativeString(
|
||||||
context: Context,
|
context: Context,
|
||||||
relative: Boolean = true,
|
relative: Boolean = true,
|
||||||
@@ -69,6 +67,8 @@ fun Date.toRelativeString(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val MILLISECONDS_IN_DAY = 86_400_000L
|
||||||
|
|
||||||
private val Date.timeWithOffset: Long
|
private val Date.timeWithOffset: Long
|
||||||
get() {
|
get() {
|
||||||
return Calendar.getInstance().run {
|
return Calendar.getInstance().run {
|
||||||
@@ -78,6 +78,6 @@ private val Date.timeWithOffset: Long
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Long.floorNearest(to: Long): Long {
|
private fun Long.floorNearest(to: Long): Long {
|
||||||
return this.floorDiv(to) * to
|
return this.floorDiv(to) * to
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ object LocaleHelper {
|
|||||||
} else if (b == "all") {
|
} else if (b == "all") {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
getDisplayName(a).compareTo(getDisplayName(b))
|
getLocalizedDisplayName(a).compareTo(getLocalizedDisplayName(b))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,16 +34,26 @@ object LocaleHelper {
|
|||||||
SourcesScreenModel.PINNED_KEY -> context.stringResource(MR.strings.pinned_sources)
|
SourcesScreenModel.PINNED_KEY -> context.stringResource(MR.strings.pinned_sources)
|
||||||
"other" -> context.stringResource(MR.strings.other_source)
|
"other" -> context.stringResource(MR.strings.other_source)
|
||||||
"all" -> context.stringResource(MR.strings.multi_lang)
|
"all" -> context.stringResource(MR.strings.multi_lang)
|
||||||
else -> getDisplayName(lang)
|
else -> getLocalizedDisplayName(lang)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDisplayName(lang: String): String {
|
||||||
|
val normalizedLang = when (lang) {
|
||||||
|
"zh-CN" -> "zh-Hans"
|
||||||
|
"zh-TW" -> "zh-Hant"
|
||||||
|
else -> lang
|
||||||
|
}
|
||||||
|
|
||||||
|
return Locale.forLanguageTag(normalizedLang).displayName
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns display name of a string language code.
|
* Returns display name of a string language code.
|
||||||
*
|
*
|
||||||
* @param lang empty for system language
|
* @param lang empty for system language
|
||||||
*/
|
*/
|
||||||
fun getDisplayName(lang: String?): String {
|
fun getLocalizedDisplayName(lang: String?): String {
|
||||||
if (lang == null) {
|
if (lang == null) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,6 +333,37 @@
|
|||||||
<item name="colorPrimaryInverse">@color/tidalwave_primaryInverse</item>
|
<item name="colorPrimaryInverse">@color/tidalwave_primaryInverse</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!--== Nord Theme ==-->
|
||||||
|
<style name="Theme.Tachiyomi.Nord">
|
||||||
|
<!-- Theme Colors -->
|
||||||
|
<item name="colorPrimary">@color/nord_primary</item>
|
||||||
|
<item name="colorOnPrimary">@color/nord_onPrimary</item>
|
||||||
|
<item name="colorPrimaryContainer">@color/nord_primaryContainer</item>
|
||||||
|
<item name="colorOnPrimaryContainer">@color/nord_onPrimaryContainer</item>
|
||||||
|
<item name="colorSecondary">@color/nord_secondary</item>
|
||||||
|
<item name="colorOnSecondary">@color/nord_onSecondary</item>
|
||||||
|
<item name="colorSecondaryContainer">@color/nord_secondaryContainer</item>
|
||||||
|
<item name="colorOnSecondaryContainer">@color/nord_onSecondaryContainer</item>
|
||||||
|
<item name="colorTertiary">@color/nord_tertiary</item>
|
||||||
|
<item name="colorOnTertiary">@color/nord_onTertiary</item>
|
||||||
|
<item name="colorTertiaryContainer">@color/nord_tertiaryContainer</item>
|
||||||
|
<item name="colorOnTertiaryContainer">@color/nord_onTertiaryContainer</item>
|
||||||
|
<item name="android:colorBackground">@color/nord_background</item>
|
||||||
|
<item name="colorOnBackground">@color/nord_onBackground</item>
|
||||||
|
<item name="colorSurface">@color/nord_surface</item>
|
||||||
|
<item name="colorOnSurface">@color/nord_onSurface</item>
|
||||||
|
<item name="colorSurfaceVariant">@color/nord_surfaceVariant</item>
|
||||||
|
<item name="colorOnSurfaceVariant">@color/nord_onSurfaceVariant</item>
|
||||||
|
<item name="colorOutline">@color/nord_outline</item>
|
||||||
|
<item name="colorOnSurfaceInverse">@color/nord_inverseOnSurface</item>
|
||||||
|
<item name="colorSurfaceInverse">@color/nord_inverseSurface</item>
|
||||||
|
<item name="colorPrimaryInverse">@color/nord_primaryInverse</item>
|
||||||
|
<item name="colorOnError">@color/nord_onError</item>
|
||||||
|
<item name="colorErrorContainer">@color/nord_errorContainer</item>
|
||||||
|
<item name="colorOnErrorContainer">@color/nord_onErrorContainer</item>
|
||||||
|
<item name="elevationOverlayColor">@color/nord_elevationOverlay</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!--== AMOLED Mode Overlay ==-->
|
<!--== AMOLED Mode Overlay ==-->
|
||||||
<style name="ThemeOverlay.Tachiyomi.Amoled" parent="" />
|
<style name="ThemeOverlay.Tachiyomi.Amoled" parent="" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "777921915939",
|
|
||||||
"firebase_url": "https://tachiyomi-47364.firebaseio.com",
|
|
||||||
"project_id": "tachiyomi-47364",
|
|
||||||
"storage_bucket": "tachiyomi-47364.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:777921915939:android:36544cd2d96c50c7",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "eu.kanade.tachiyomi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:777921915939:android:564fdc1d62efd1de",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "eu.kanade.tachiyomi.debug"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
|||||||
buildscript {
|
buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath(libs.android.shortcut.gradle)
|
classpath(libs.android.shortcut.gradle)
|
||||||
classpath(libs.google.services.gradle)
|
|
||||||
classpath(libs.aboutLibraries.gradle)
|
classpath(libs.aboutLibraries.gradle)
|
||||||
classpath(libs.sqldelight.gradle)
|
classpath(libs.sqldelight.gradle)
|
||||||
classpath(libs.moko.gradle)
|
classpath(libs.moko.gradle)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
|
||||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
@@ -30,9 +31,10 @@ class NetworkHelper(
|
|||||||
maxSize = 5L * 1024 * 1024, // 5 MiB
|
maxSize = 5L * 1024 * 1024, // 5 MiB
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.addInterceptor(BrotliInterceptor)
|
|
||||||
.addInterceptor(UncaughtExceptionInterceptor())
|
.addInterceptor(UncaughtExceptionInterceptor())
|
||||||
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
||||||
|
.addNetworkInterceptor(IgnoreGzipInterceptor())
|
||||||
|
.addNetworkInterceptor(BrotliInterceptor)
|
||||||
|
|
||||||
if (preferences.verboseLogging().get()) {
|
if (preferences.verboseLogging().get()) {
|
||||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To use [okhttp3.brotli.BrotliInterceptor] as a network interceptor,
|
||||||
|
* add [IgnoreGzipInterceptor] right before it.
|
||||||
|
*
|
||||||
|
* This nullifies the transparent gzip of [okhttp3.internal.http.BridgeInterceptor]
|
||||||
|
* so gzip and Brotli are explicitly handled by the [okhttp3.brotli.BrotliInterceptor].
|
||||||
|
*/
|
||||||
|
class IgnoreGzipInterceptor : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
var request = chain.request()
|
||||||
|
if (request.header("Accept-Encoding") == "gzip") {
|
||||||
|
request = request.newBuilder().removeHeader("Accept-Encoding").build()
|
||||||
|
}
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
||||||
import tachiyomi.domain.source.model.SourceWithCount
|
import tachiyomi.domain.source.model.SourceWithCount
|
||||||
@@ -38,11 +39,12 @@ class SourceRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
|
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
|
||||||
val sourceIdWithFavoriteCount =
|
return combine(
|
||||||
handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }
|
handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() },
|
||||||
return sourceIdWithFavoriteCount.map { sourceIdsWithCount ->
|
sourceManager.catalogueSources,
|
||||||
sourceIdsWithCount
|
) { sourceIdWithFavoriteCount, _ -> sourceIdWithFavoriteCount }
|
||||||
.map { (sourceId, count) ->
|
.map {
|
||||||
|
it.map { (sourceId, count) ->
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
val source = sourceManager.getOrStub(sourceId)
|
||||||
val domainSource = mapSourceToDomainSource(source).copy(
|
val domainSource = mapSourceToDomainSource(source).copy(
|
||||||
isStub = source is StubSource,
|
isStub = source is StubSource,
|
||||||
|
|||||||
@@ -85,26 +85,6 @@ class LibraryPreferences(
|
|||||||
TriState.DISABLED,
|
TriState.DISABLED,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun filterIntervalLong() = preferenceStore.getEnum(
|
|
||||||
"pref_filter_library_interval_long",
|
|
||||||
TriState.DISABLED,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun filterIntervalLate() = preferenceStore.getEnum(
|
|
||||||
"pref_filter_library_interval_late",
|
|
||||||
TriState.DISABLED,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun filterIntervalDropped() = preferenceStore.getEnum(
|
|
||||||
"pref_filter_library_interval_dropped",
|
|
||||||
TriState.DISABLED,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun filterIntervalPassed() = preferenceStore.getEnum(
|
|
||||||
"pref_filter_library_interval_passed",
|
|
||||||
TriState.DISABLED,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun filterTracking(id: Int) = preferenceStore.getEnum(
|
fun filterTracking(id: Int) = preferenceStore.getEnum(
|
||||||
"pref_filter_library_tracked_${id}_v2",
|
"pref_filter_library_tracked_${id}_v2",
|
||||||
TriState.DISABLED,
|
TriState.DISABLED,
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ class FetchInterval(
|
|||||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun toMangaUpdateOrNull(
|
suspend fun toMangaUpdate(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
dateTime: ZonedDateTime,
|
dateTime: ZonedDateTime,
|
||||||
window: Pair<Long, Long>,
|
window: Pair<Long, Long>,
|
||||||
): MangaUpdate? {
|
): MangaUpdate {
|
||||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
||||||
chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true),
|
chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true),
|
||||||
zone = dateTime.zone,
|
zone = dateTime.zone,
|
||||||
@@ -30,11 +30,7 @@ class FetchInterval(
|
|||||||
}
|
}
|
||||||
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
||||||
|
|
||||||
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
return MangaUpdate(id = manga.id, nextUpdate = nextUpdate, fetchInterval = interval)
|
||||||
null
|
|
||||||
} else {
|
|
||||||
MangaUpdate(id = manga.id, nextUpdate = nextUpdate, fetchInterval = interval)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||||
@@ -96,10 +92,10 @@ class FetchInterval(
|
|||||||
dateTime: ZonedDateTime,
|
dateTime: ZonedDateTime,
|
||||||
window: Pair<Long, Long>,
|
window: Pair<Long, Long>,
|
||||||
): Long {
|
): Long {
|
||||||
return if (
|
if (manga.nextUpdate in window.first.rangeTo(window.second + 1)) {
|
||||||
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
|
return manga.nextUpdate
|
||||||
manga.fetchInterval == 0
|
}
|
||||||
) {
|
|
||||||
val latestDate = ZonedDateTime.ofInstant(
|
val latestDate = ZonedDateTime.ofInstant(
|
||||||
if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(),
|
if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(),
|
||||||
dateTime.zone,
|
dateTime.zone,
|
||||||
@@ -109,21 +105,18 @@ class FetchInterval(
|
|||||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
||||||
val cycle = timeSinceLatest.floorDiv(
|
val cycle = timeSinceLatest.floorDiv(
|
||||||
interval.absoluteValue.takeIf { interval < 0 }
|
interval.absoluteValue.takeIf { interval < 0 }
|
||||||
?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10),
|
?: increaseInterval(interval, timeSinceLatest, increaseWhenOver = 10),
|
||||||
)
|
)
|
||||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
|
return latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
|
||||||
} else {
|
|
||||||
manga.nextUpdate
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
private fun increaseInterval(delta: Int, timeSinceLatest: Int, increaseWhenOver: Int): Int {
|
||||||
if (delta >= MAX_INTERVAL) return MAX_INTERVAL
|
if (delta >= MAX_INTERVAL) return MAX_INTERVAL
|
||||||
|
|
||||||
// double delta again if missed more than 9 check in new delta
|
// double delta again if missed more than 9 check in new delta
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
return if (cycle > doubleWhenOver) {
|
return if (cycle > increaseWhenOver) {
|
||||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
|
increaseInterval(delta * 2, timeSinceLatest, increaseWhenOver)
|
||||||
} else {
|
} else {
|
||||||
delta
|
delta
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user