Compare commits

...

32 Commits

Author SHA1 Message Date
49991d38d9 Release v0.15.3 2024-01-13 09:33:18 -05:00
33c62ab711 Clean up some unnecessary bits
- Remove analytics/crash reporting
- Remove app update check
- Remove F-Droid warning
- Remove Discord references
2024-01-13 09:27:53 -05:00
899bd26956 Translations update from Hosted Weblate (#10393)
Weblate translations












Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bg/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/bg/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/eo/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/fil/
Translation: Tachiyomi/Tachiyomi plurals.xml
Translation: Tachiyomi/Tachiyomi strings.xml

Co-authored-by: Boyan Alexiev <nneauu@gmail.com>
Co-authored-by: Denis \"Samilton <d.bogdan99@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Radoŝ Porka <animatorzPolski@gmail.com>
Co-authored-by: Shiratori <kuromaruhatake@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
2024-01-13 09:17:12 -05:00
a37f3eb709 Better message for empty extensions list 2024-01-12 09:27:59 -05:00
9ae71dfe93 Update Compose compiler and Kotlin 2024-01-11 22:36:45 -05:00
c65a9aecf5 Fixed tap controls not working when zoomed in (#10378)
Co-authored-by: Paloys <Paloys@users.noreply.github.com>
2024-01-11 18:36:40 -05:00
02e50411de Minor extension repo enhancements
- Shortcut to settings from extensions tab
- Don't show error toast anymore if nothing's loaded
- Ability to copy extension repo URL to clipboard
2024-01-11 18:25:13 -05:00
6e822dfd5b Translations update from Hosted Weblate (#10386)
Weblate translations































Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/eo/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ne/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/te/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/cs/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/ne/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/zh_Hans/
Translation: Tachiyomi/Tachiyomi plurals.xml
Translation: Tachiyomi/Tachiyomi strings.xml

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: CodeSpoof <nao.s_l_t_e_e_l@protonmail.com>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Daedren <lord.raikon@gmail.com>
Co-authored-by: DarKCroX <DarKCroX@users.noreply.hosted.weblate.org>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Gianluca Starke <gianlucastarke@gmail.com>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Co-authored-by: Khori Hutama <khori.qq@gmail.com>
Co-authored-by: Kirito ._ <kiritokunn18@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Manjul Tamrakar <manjultamrakar4@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Paavalen Lingachetti <p.lingachetti@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Radoŝ Porka <animatorzPolski@gmail.com>
Co-authored-by: Sup Kelelawar <apkfile007@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Zero O <godarms2010@live.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: dan-malprod <diabolic0240@proton.me>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: orkan gökçe alaz aşina <examplehuman@outlook.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
2024-01-11 18:24:00 -05:00
7292dadd5f [download-cache] Fixed init logic to skip when cache file is missing (#10362)
There are several possible causes of the cache file to not exist, including user
 action. By skipping these couple steps during initialization when the file is
 missing, a renew action is allowed to start and the cache will rebuild and
 hopefully work as expected.

Simple fix for #10360
2024-01-11 18:23:18 -05:00
b1067b942e Use transaction on restore to go brr. (#10375)
refactor: use transaction to go brr.

This improve the restore speed on fresh db and non fresh db.

Signed-off-by: KaiserBh <kaiserbh@proton.me>
2024-01-11 18:22:46 -05:00
d6c4af89c4 Fix extension interceptors receiving compressed responses (#10388) 2024-01-11 18:22:31 -05:00
cf6f7c521c Fixed dev UI preview (#10385)
The TachiyomiTheme introduced a dependency-injection construct that didn't
 exist at the time of rendering previews, so I've changed the preview function
 to use a preview version of the theme that uses declarative configuration
 over dependency injection
2024-01-11 18:22:21 -05:00
c6601c1f94 Release v0.15.2 2024-01-08 18:17:25 -05:00
68899aea61 Disable some non-ready stuff for stable build 2024-01-08 18:16:59 -05:00
c3edf9b5d0 Translations update from Hosted Weblate (#10336)
Weblate translations
















Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/eo/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/zh_Hans/
Translation: Tachiyomi/Tachiyomi plurals.xml
Translation: Tachiyomi/Tachiyomi strings.xml

Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Radoŝ Porka <animatorzPolski@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: Zero O <godarms2010@live.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-01-08 18:13:52 -05:00
97e04392d3 [skip ci] update issue templates 2024-01-08 17:41:11 -05:00
3d178737b1 Move extension repos interactors to proper package
Also retain ordering of added repos.
2024-01-08 17:15:48 -05:00
bf737cf95c Remove built-in official extension repo support 2024-01-07 23:06:17 -05:00
c91ec9a33b fix Can't scroll down to the bottom of the webtoon on the last chapter (#10291) 2024-01-07 22:52:10 -05:00
a8040cb21a [track-search] Added context menu for copy and open-in-web (#10352) 2024-01-07 22:49:51 -05:00
f60782f11f Avoid floating point precision issues when converting scores
Fixes #10343
Maybe we'll finally migrate off of those legacy models some day...
2024-01-07 17:50:14 -05:00
7d6e1bdafc Update dependency io.mockk:mockk to v1.13.9 (#10349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-07 17:50:09 -05:00
5854ad97e0 Do proper check for next chapter's download status when downloading ahead
Fixes #10151 (I think?)
2024-01-07 17:30:53 -05:00
4b8fa059d5 Fix external repo info banner in ExtensionDetailsScreen 2024-01-07 16:35:25 -05:00
3dc2f9a711 Add advanced setting to revoke all trusted unknown extensions 2024-01-07 16:16:26 -05:00
8033a94ee2 Trusting new extension shouldn't revoke other irrelevant extensions 2024-01-07 16:05:31 -05:00
028da099dd Add filter library by customized update frequency
Supersedes #9619

Co-authored-by: quangkieu <quangkieu@users.noreply.github.com>
2024-01-07 16:03:12 -05:00
e6c6c32d81 Fix selecting custom fetch interval not persisting sometimes 2024-01-07 15:40:53 -05:00
bce6af62fc Add Nord Theme (#10308)
* Add Nord Theme

* update

* update error color

* update comment
2024-01-07 15:20:08 -05:00
6510a9617a Allow permanently trusting unofficial extensions by version code + signature
Closes #10290
2024-01-07 13:35:44 -05:00
14510f1d26 Avoid jank in extensions list if install permissions already granted 2024-01-07 12:32:33 -05:00
f115edf2ea Allow deep linking to add external repo 2024-01-07 12:27:40 -05:00
173 changed files with 1828 additions and 1549 deletions

View File

@ -3,10 +3,10 @@
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v0.15.1)
- To the latest version of the app (stable is v0.15.3)
- All extensions
- 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 will fill out the title and the information in this template

View File

@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: ⚠️ Extension/source issue
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
url: https://github.com/tachiyomiorg/extensions/issues/new/choose
about: Issues and requests for official extensions and sources should be opened in the extensions repository instead
- name: 📦 Tachiyomi extensions
url: https://tachiyomi.org/extensions/
about: List of all available extensions with download links

View File

@ -53,7 +53,7 @@ body:
label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**.
placeholder: |
Example: "0.15.1"
Example: "0.15.3"
validations:
required: true
@ -94,11 +94,11 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/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
- label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[0.15.1](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
- label: I have updated all installed extensions.
required: true

View File

@ -31,9 +31,9 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/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
- label: I have updated the app to version **[0.15.1](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
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -59,8 +59,7 @@ representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community moderators responsible for enforcement at
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
reported to the community moderators via issues.
All complaints will be reviewed and investigated promptly and fairly.
All community moderators are obligated to respect the privacy and security of the

View File

@ -30,7 +30,7 @@ To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
## 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

View File

@ -1,6 +1,6 @@
| Build | Stable | Weekly Preview | Contribute | Support Server |
|-------|----------|---------|------------|---------|
| [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
| Build | Stable | Weekly Preview | Contribute |
|-------|--------|----------------|------------|
| [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) |
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
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>
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).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
**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).**
</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"
* 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><summary>Contributing</summary>
@ -70,7 +69,6 @@ See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
## FAQ
[See our website.](https://tachiyomi.org/)
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
## License

View File

@ -8,10 +8,6 @@ plugins {
id("com.github.zellius.shortcut-helper")
}
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
}
shortcutHelper.setFilePath("./shortcuts.xml")
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@ -22,8 +18,8 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
versionCode = 116
versionName = "0.15.1"
versionCode = 119
versionName = "0.15.3"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -31,9 +27,6 @@ android {
buildConfigField("boolean", "INCLUDE_UPDATER", "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 {
abiFilters += SUPPORTED_ABIS
}
@ -245,10 +238,6 @@ dependencies {
// Logging
implementation(libs.logcat)
// Crash reports/analytics
implementation(libs.bundles.acra)
"standardImplementation"(libs.firebase.analytics)
// Shizuku
implementation(libs.bundles.shizuku)

View File

@ -71,7 +71,3 @@
# XmlUtil
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
# Firebase
-keep class com.google.firebase.installations.** { *; }
-keep interface com.google.firebase.installations.** { *; }

View File

@ -8,7 +8,8 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!-- For background jobs -->
@ -21,18 +22,16 @@
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<!-- To view extension packages in API 30+ -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
<uses-permission
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Remove permission from Firebase dependency -->
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:name=".App"
@ -52,16 +51,30 @@
<activity
android:name=".ui.main.MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.Tachiyomi.SplashScreen"
android:exported="true">
android:theme="@style/Theme.Tachiyomi.SplashScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<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" />
@ -69,7 +82,6 @@
<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
@ -93,16 +105,16 @@
</activity>
<activity
android:process=":error_handler"
android:name=".crash.CrashActivity"
android:exported="false" />
android:exported="false"
android:process=":error_handler" />
<activity
android:name=".ui.deeplink.DeepLinkActivity"
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay"
android:exported="true"
android:label="@string/action_search"
android:exported="true">
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -126,20 +138,21 @@
<activity
android:name=".ui.reader.ReaderActivity"
android:launchMode="singleTask"
android:exported="false">
android:exported="false"
android:launchMode="singleTask">
<intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter>
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions"/>
<meta-data
android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions" />
</activity>
<activity
android:name=".ui.security.UnlockActivity"
android:theme="@style/Theme.Tachiyomi"
android:exported="false" />
android:exported="false"
android:theme="@style/Theme.Tachiyomi" />
<activity
android:name=".ui.webview.WebViewActivity"
@ -148,25 +161,25 @@
<activity
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
android:name=".ui.setting.track.TrackLoginActivity"
android:label="@string/track_activity_name"
android:exported="true">
android:exported="true"
android:label="@string/track_activity_name">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="anilist-auth"/>
<data android:host="bangumi-auth"/>
<data android:host="myanimelist-auth"/>
<data android:host="shikimori-auth"/>
<data android:scheme="tachiyomi" />
<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>
</activity>
@ -206,9 +219,9 @@
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data
@ -218,11 +231,6 @@
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<!-- Disable advertising ID collection for Firebase -->
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="false" />
</application>
</manifest>

View File

@ -4,18 +4,19 @@ import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
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.GetExtensionRepos
import eu.kanade.domain.extension.interactor.GetExtensionSources
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.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
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.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourceRepos
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleLanguage
@ -170,9 +171,10 @@ class DomainModule : InjektModule {
addFactory { ToggleLanguage(get()) }
addFactory { ToggleSource(get()) }
addFactory { ToggleSourcePin(get()) }
addFactory { TrustExtension(get()) }
addFactory { CreateSourceRepo(get()) }
addFactory { DeleteSourceRepo(get()) }
addFactory { GetSourceRepos(get()) }
addFactory { CreateExtensionRepo(get()) }
addFactory { DeleteExtensionRepo(get()) }
addFactory { GetExtensionRepos(get()) }
}
}

View File

@ -2,8 +2,6 @@ package eu.kanade.domain.base
import android.content.Context
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.PreferenceStore
import tachiyomi.i18n.MR
@ -22,8 +20,6 @@ class BasePreferences(
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {

View File

@ -1,17 +1,17 @@
package eu.kanade.domain.source.interactor
package eu.kanade.domain.extension.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.preference.plusAssign
class CreateSourceRepo(private val preferences: SourcePreferences) {
class CreateExtensionRepo(private val preferences: SourcePreferences) {
fun await(name: String): Result {
// Do not allow invalid formats
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_REPO_BASE_URL)) {
if (!name.matches(repoRegex)) {
return Result.InvalidUrl
}
preferences.extensionRepos() += name.substringBeforeLast("/index.min.json")
preferences.extensionRepos() += name.removeSuffix("/index.min.json")
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()

View File

@ -1,9 +1,9 @@
package eu.kanade.domain.source.interactor
package eu.kanade.domain.extension.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.preference.minusAssign
class DeleteSourceRepo(private val preferences: SourcePreferences) {
class DeleteExtensionRepo(private val preferences: SourcePreferences) {
fun await(repo: String) {
preferences.extensionRepos() -= repo

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

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

View File

@ -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) }
}
}

View File

@ -38,11 +38,14 @@ class SourcePreferences(
SetMigrateSorting.Direction.ASCENDING,
)
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
fun trustedExtensions() = preferenceStore.getStringSet(
Preference.appStateKey("trusted_extensions"),
emptySet(),
)
}

View File

@ -40,7 +40,9 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
lastChapterRead = last_chapter_read.toDouble(),
totalChapters = total_chapters.toLong(),
status = status.toLong(),
score = score.toDouble(),
// Jank workaround due to precision issues while converting
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
score = score.toString().toDouble(),
remoteUrl = tracking_url,
startDate = started_reading_date,
finishDate = finished_reading_date,

View File

@ -1,6 +1,8 @@
package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import tachiyomi.i18n.MR
enum class AppTheme(val titleRes: StringResource?) {
@ -9,6 +11,9 @@ enum class AppTheme(val titleRes: StringResource?) {
GREEN_APPLE(MR.strings.theme_greenapple),
LAVENDER(MR.strings.theme_lavender),
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),
TAKO(MR.strings.theme_tako),
TEALTURQUOISE(MR.strings.theme_tealturquoise),

View File

@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
@ -53,6 +53,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
@ -66,13 +67,23 @@ fun ExtensionDetailsScreen(
navigateUp: () -> Unit,
state: ExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit,
onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
) {
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
regex.find(state.extension?.repoUrl.orEmpty())
?.let {
val (user, repo) = it.destructured
"https://github.com/$user/$repo"
}
?: state.extension?.repoUrl
}
Scaffold(
topBar = { scrollBehavior ->
AppBar(
@ -82,12 +93,14 @@ fun ExtensionDetailsScreen(
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
if (state.extension?.isUnofficial == false) {
if (url != null) {
add(
AppBar.Action(
title = stringResource(MR.strings.whats_new),
icon = Icons.Outlined.History,
onClick = onClickWhatsNew,
title = stringResource(MR.strings.action_open_repo),
icon = Icons.AutoMirrored.Outlined.Launch,
onClick = {
uriHandler.openUri(url)
},
),
)
}
@ -138,7 +151,7 @@ fun ExtensionDetailsScreen(
private fun ExtensionDetails(
contentPadding: PaddingValues,
extension: Extension.Installed,
sources: List<ExtensionSourceItem>,
sources: ImmutableList<ExtensionSourceItem>,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
@ -149,30 +162,10 @@ private fun ExtensionDetails(
ScrollbarLazyColumn(
contentPadding = contentPadding,
) {
when {
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 {
WarningBanner(MR.strings.obsolete_extension_message)
}
if (extension.isObsolete) {
item {
WarningBanner(MR.strings.obsolete_extension_message)
}
}
item {

View File

@ -40,11 +40,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.browse.components.BaseBrowseItem
import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.WarningBanner
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.tachiyomi.extension.model.Extension
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.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
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.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.EmptyScreenAction
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.theme.header
import tachiyomi.presentation.core.util.plus
@ -80,6 +85,8 @@ fun ExtensionScreen(
onClickUpdateAll: () -> Unit,
onRefresh: () -> Unit,
) {
val navigator = LocalNavigator.currentOrThrow
PullRefresh(
refreshing = state.isRefreshing,
onRefresh = onRefresh,
@ -96,6 +103,13 @@ fun ExtensionScreen(
EmptyScreen(
stringRes = msg,
modifier = Modifier.padding(contentPadding),
actions = persistentListOf(
EmptyScreenAction(
stringRes = MR.strings.label_extension_repos,
icon = Icons.Outlined.Settings,
onClick = { navigator.push(ExtensionReposScreen()) },
),
),
)
}
else -> {
@ -133,13 +147,13 @@ private fun ExtensionContent(
) {
val context = LocalContext.current
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
val installGranted = rememberRequestPackageInstallsPermissionState()
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
FastScrollLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
if (!installGranted && state.installer?.requiresSystemPermission == true) {
item {
item(key = "extension-permissions-warning") {
WarningBanner(
textRes = MR.strings.ext_permission_install_apps_warning,
modifier = Modifier.clickable {
@ -342,7 +356,6 @@ private fun ExtensionItemContent(
val warning = when {
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.isNsfw -> MR.strings.ext_nsfw_short
else -> null

View File

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

View File

@ -6,7 +6,7 @@ import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
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 tachiyomi.i18n.MR
import tachiyomi.presentation.core.screens.EmptyScreen
@ -15,7 +15,7 @@ import tachiyomi.presentation.core.screens.EmptyScreenAction
@PreviewLightDark
@Composable
private fun NoActionPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
EmptyScreen(
stringRes = MR.strings.empty_screen,
@ -27,7 +27,7 @@ private fun NoActionPreview() {
@PreviewLightDark
@Composable
private fun WithActionPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
EmptyScreen(
stringRes = MR.strings.empty_screen,

View File

@ -101,6 +101,6 @@ data class TabContent(
val titleRes: StringResource,
val badgeNumber: Int? = null,
val searchEnabled: Boolean = false,
val actions: ImmutableList<AppBar.Action> = persistentListOf(),
val actions: ImmutableList<AppBar.AppBarAction> = persistentListOf(),
val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit,
)

View File

@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
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 kotlinx.coroutines.launch
import tachiyomi.i18n.MR
@ -63,7 +63,7 @@ fun CrashScreen(
@PreviewLightDark
@Composable
private fun CrashScreenPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
CrashScreen(exception = RuntimeException("Dummy")) {}
}
}

View File

@ -17,7 +17,7 @@ import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.components.relativeDateText
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 kotlinx.collections.immutable.persistentListOf
import tachiyomi.domain.history.model.HistoryWithRelations
@ -143,7 +143,7 @@ internal fun HistoryScreenPreviews(
@PreviewParameter(HistoryScreenModelStateProvider::class)
historyState: HistoryScreenModel.State,
) {
TachiyomiTheme {
TachiyomiPreviewTheme {
HistoryScreen(
state = historyState,
snackbarHostState = SnackbarHostState(),

View File

@ -12,7 +12,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.padding
@ -91,7 +91,7 @@ fun HistoryDeleteAllDialog(
@PreviewLightDark
@Composable
private fun HistoryDeleteDialogPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
HistoryDeleteDialog(
onDismissRequest = {},
onDelete = {},

View File

@ -23,7 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
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.tachiyomi.util.lang.toTimestampString
import tachiyomi.domain.history.model.HistoryWithRelations
@ -98,7 +98,7 @@ private fun HistoryItemPreviews(
@PreviewParameter(HistoryWithRelationsProvider::class)
historyWithRelations: HistoryWithRelations,
) {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
HistoryItem(
history = historyWithRelations,

View File

@ -16,6 +16,8 @@ import androidx.compose.ui.platform.LocalConfiguration
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.preference.TriState
import tachiyomi.domain.category.model.Category
@ -74,6 +76,8 @@ private fun ColumnScope.FilterPage(
) {
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateMangaRestrictions().collectAsState()
TriStateItem(
label = stringResource(MR.strings.label_downloaded),
state = if (downloadedOnly) {
@ -108,6 +112,18 @@ private fun ColumnScope.FilterPage(
state = filterCompleted,
onClick = { screenModel.toggleFilter(LibraryPreferences::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 }
when (trackers.size) {

View File

@ -6,7 +6,7 @@ import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
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
@Composable
@ -50,7 +50,7 @@ internal fun LanguageBadge(
@PreviewLightDark
@Composable
private fun BadgePreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Column {
DownloadsBadge(count = 10)
UnreadBadge(count = 10)

View File

@ -30,6 +30,7 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
@Composable
fun DeleteChaptersDialog(
@ -85,7 +86,7 @@ fun SetIntervalDialog(
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
text = {
Column {
if (nextUpdateDays != null && nextUpdateDays >= 0) {
if (nextUpdateDays != null && nextUpdateDays >= 0 && interval >= 0) {
Text(
stringResource(
MR.strings.manga_interval_expected_update,
@ -96,8 +97,8 @@ fun SetIntervalDialog(
),
pluralStringResource(
MR.plurals.day,
count = interval,
interval,
count = interval.absoluteValue,
interval.absoluteValue,
),
),
)
@ -105,7 +106,6 @@ fun SetIntervalDialog(
Spacer(Modifier.height(MaterialTheme.padding.small))
}
// TODO: selecting "1" then doesn't allow for future changes unless defaulting first?
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
Text(stringResource(MR.strings.manga_interval_custom_amount))

View File

@ -201,14 +201,14 @@ fun MangaActionRow(
onLongClick = onEditCategory,
)
MangaActionButton(
title = if (nextUpdateDays != null) {
pluralStringResource(
title = when (nextUpdateDays) {
null -> stringResource(MR.strings.not_applicable)
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
else -> pluralStringResource(
MR.plurals.day,
count = nextUpdateDays,
nextUpdateDays,
)
} else {
stringResource(MR.strings.not_applicable)
},
icon = Icons.Default.HourglassEmpty,
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,

View File

@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
@ -44,7 +44,7 @@ fun MissingChapterCountListItem(
@PreviewLightDark
@Composable
private fun Preview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
MissingChapterCountListItem(count = 42)
}

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.more
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
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.platform.LocalUriHandler
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.TextPreferenceWidget
import eu.kanade.tachiyomi.R
@ -60,14 +58,7 @@ fun MoreScreen(
),
) {
if (isFDroid) {
WarningBanner(
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",
)
},
)
// Don't really care about slow updaters now
}
}
},

View File

@ -19,7 +19,7 @@ import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
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.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@ -69,7 +69,7 @@ fun NewUpdateScreen(
@PreviewLightDark
@Composable
private fun NewUpdateScreenPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
NewUpdateScreen(
versionName = "v0.99.9",
changelogInfo = """

View File

@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@ -61,7 +61,7 @@ const val GETTING_STARTED_URL = "https://tachiyomi.org/docs/guides/getting-start
@PreviewLightDark
@Composable
private fun GuidesStepPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
GuidesStep(
onRestoreBackup = {},
).Content()

View File

@ -24,6 +24,7 @@ import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.advanced.ClearDatabaseScreen
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.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
@ -87,12 +87,6 @@ object SettingsAdvancedScreen : SearchableSettings {
return buildList {
addAll(
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(
title = stringResource(MR.strings.pref_dump_crash_logs),
subtitle = stringResource(MR.strings.pref_dump_crash_logs_summary),
@ -340,6 +334,7 @@ object SettingsAdvancedScreen : SearchableSettings {
val uriHandler = LocalUriHandler.current
val extensionInstallerPref = basePreferences.extensionInstaller()
var shizukuMissing by rememberSaveable { mutableStateOf(false) }
val trustExtension = remember { Injekt.get<TrustExtension>() }
if (shizukuMissing) {
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)
},
),
),
)
}

View File

@ -1,15 +1,12 @@
package eu.kanade.presentation.more.settings.screen.about
import android.content.Context
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -30,13 +27,10 @@ import eu.kanade.presentation.util.LocalBackPress
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.BuildConfig
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.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
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.i18n.stringResource
import tachiyomi.presentation.core.icons.CustomIcons
import tachiyomi.presentation.core.icons.Discord
import tachiyomi.presentation.core.icons.Facebook
import tachiyomi.presentation.core.icons.Github
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 {
TextPreferenceWidget(
title = stringResource(MR.strings.licenses),
@ -179,11 +117,6 @@ object AboutScreen : Screen() {
icon = Icons.Outlined.Public,
url = "https://tachiyomi.org",
)
LinkIcon(
label = "Discord",
icon = CustomIcons.Discord,
url = "https://discord.gg/tachiyomi",
)
LinkIcon(
label = "X",
icon = CustomIcons.X,

View File

@ -16,16 +16,22 @@ import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
import tachiyomi.presentation.core.screens.LoadingScreen
class ExtensionReposScreen : Screen() {
class ExtensionReposScreen(
private val url: String? = null,
) : Screen() {
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
val state by screenModel.state.collectAsState()
LaunchedEffect(url) {
url?.let { screenModel.createRepo(it) }
}
if (state is RepoScreenState.Loading) {
LoadingScreen()
return
@ -46,7 +52,7 @@ class ExtensionReposScreen : Screen() {
ExtensionRepoCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createRepo(it) },
categories = successState.repos,
repos = successState.repos,
)
}
is RepoDialog.Delete -> {

View File

@ -4,11 +4,11 @@ import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.source.interactor.CreateSourceRepo
import eu.kanade.domain.source.interactor.DeleteSourceRepo
import eu.kanade.domain.source.interactor.GetSourceRepos
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
import eu.kanade.domain.extension.interactor.GetExtensionRepos
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
@ -19,9 +19,9 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionReposScreenModel(
private val getSourceRepos: GetSourceRepos = Injekt.get(),
private val createSourceRepo: CreateSourceRepo = Injekt.get(),
private val deleteSourceRepo: DeleteSourceRepo = Injekt.get(),
private val getExtensionRepos: GetExtensionRepos = Injekt.get(),
private val createExtensionRepo: CreateExtensionRepo = Injekt.get(),
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
@ -29,11 +29,11 @@ class ExtensionReposScreenModel(
init {
screenModelScope.launchIO {
getSourceRepos.subscribe()
getExtensionRepos.subscribe()
.collectLatest { repos ->
mutableState.update {
RepoScreenState.Success(
repos = repos.toImmutableList(),
repos = repos.toImmutableSet(),
)
}
}
@ -47,8 +47,8 @@ class ExtensionReposScreenModel(
*/
fun createRepo(name: String) {
screenModelScope.launchIO {
when (createSourceRepo.await(name)) {
is CreateSourceRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
when (createExtensionRepo.await(name)) {
is CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
else -> {}
}
}
@ -61,7 +61,7 @@ class ExtensionReposScreenModel(
*/
fun deleteRepo(repo: String) {
screenModelScope.launchIO {
deleteSourceRepo.await(repo)
deleteExtensionRepo.await(repo)
}
}
@ -101,7 +101,7 @@ sealed class RepoScreenState {
@Immutable
data class Success(
val repos: ImmutableList<String>,
val repos: ImmutableSet<String>,
val dialog: RepoDialog? = null,
) : RepoScreenState() {

View File

@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
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.material3.ElevatedCard
import androidx.compose.material3.Icon
@ -19,12 +19,16 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.i18n.stringResource
@Composable
fun ExtensionReposContent(
repos: ImmutableList<String>,
repos: ImmutableSet<String>,
lazyListState: LazyListState,
paddingValues: PaddingValues,
onClickDelete: (String) -> Unit,
@ -36,12 +40,14 @@ fun ExtensionReposContent(
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
modifier = modifier,
) {
items(repos) { repo ->
ExtensionRepoListItem(
modifier = Modifier.animateItemPlacement(),
repo = repo,
onDelete = { onClickDelete(repo) },
)
repos.forEach {
item {
ExtensionRepoListItem(
modifier = Modifier.animateItemPlacement(),
repo = it,
onDelete = { onClickDelete(it) },
)
}
}
}
}
@ -52,6 +58,8 @@ private fun ExtensionRepoListItem(
onDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
ElevatedCard(
modifier = modifier,
) {
@ -73,8 +81,23 @@ private fun ExtensionRepoListItem(
modifier = Modifier.fillMaxWidth(),
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) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = null)
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(MR.strings.action_delete),
)
}
}
}

View File

@ -14,7 +14,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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 tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@ -24,12 +24,12 @@ import kotlin.time.Duration.Companion.seconds
fun ExtensionRepoCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
categories: ImmutableList<String>,
repos: ImmutableSet<String>,
) {
var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.contains(name) }
val nameAlreadyExists = remember(name) { repos.contains(name) }
AlertDialog(
onDismissRequest = onDismissRequest,

View File

@ -42,15 +42,19 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.fullType
@Composable
internal fun AppThemePreferenceWidget(
@ -258,7 +262,8 @@ fun AppThemePreviewItem(
@Composable
private fun AppThemesListPreview() {
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
TachiyomiTheme {
Injekt.addSingleton(fullType<UiPreferences>(), UiPreferences(InMemoryPreferenceStore()))
TachiyomiTheme(appTheme = appTheme) {
Surface {
AppThemesList(
currentTheme = appTheme,

View File

@ -12,7 +12,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@ -43,7 +43,7 @@ internal fun InfoWidget(text: String) {
@PreviewLightDark
@Composable
private fun InfoWidgetPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
InfoWidget(text = stringResource(MR.strings.download_ahead_info))
}

View File

@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
@Composable
fun SwitchPreferenceWidget(
@ -40,7 +40,7 @@ fun SwitchPreferenceWidget(
@PreviewLightDark
@Composable
private fun SwitchPreferenceWidgetPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
Column {
SwitchPreferenceWidget(

View File

@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
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
@Composable
@ -62,7 +62,7 @@ fun TextPreferenceWidget(
@PreviewLightDark
@Composable
private fun TextPreferenceWidgetPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
Column {
TextPreferenceWidget(

View File

@ -33,7 +33,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
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.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
@ -306,7 +306,7 @@ private val FakeChapterLongTitle = previewChapter(
@PreviewLightDark
@Composable
private fun TransitionTextPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeChapter)),
@ -320,7 +320,7 @@ private fun TransitionTextPreview() {
@PreviewLightDark
@Composable
private fun TransitionTextLongTitlePreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Next(ReaderChapter(FakeChapterLongTitle), ReaderChapter(FakeChapter)),
@ -334,7 +334,7 @@ private fun TransitionTextLongTitlePreview() {
@PreviewLightDark
@Composable
private fun TransitionTextWithGapPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeGapChapter)),
@ -348,7 +348,7 @@ private fun TransitionTextWithGapPreview() {
@PreviewLightDark
@Composable
private fun TransitionTextNoNextPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), null),
@ -362,7 +362,7 @@ private fun TransitionTextNoNextPreview() {
@PreviewLightDark
@Composable
private fun TransitionTextNoPreviousPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Prev(ReaderChapter(FakeChapter), null),

View File

@ -16,7 +16,7 @@ import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.manga.model.readerOrientation
import eu.kanade.presentation.components.AdaptiveSheet
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.ReaderSettingsScreenModel
import tachiyomi.i18n.MR
@ -81,7 +81,7 @@ private fun DialogContent(
@PreviewLightDark
@Composable
private fun DialogContentPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
Column {
DialogContent(

View File

@ -12,7 +12,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
@Composable
fun PageIndicatorText(
@ -51,7 +51,7 @@ fun PageIndicatorText(
@PreviewLightDark
@Composable
private fun PageIndicatorTextPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
PageIndicatorText(currentPage = 10, totalPages = 69)
}

View File

@ -18,7 +18,7 @@ import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.manga.model.readingMode
import eu.kanade.presentation.components.AdaptiveSheet
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.ReadingMode
import tachiyomi.i18n.MR
@ -79,7 +79,7 @@ private fun DialogContent(
@PreviewLightDark
@Composable
private fun DialogContentPreview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
Column {
DialogContent(

View File

@ -19,7 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.SettingsItemsPaddings
import tachiyomi.presentation.core.components.material.padding
@ -70,7 +70,7 @@ fun ModeSelectionDialog(
@PreviewLightDark
@Composable
private fun Preview() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
Column {
ModeSelectionDialog(

View File

@ -12,6 +12,7 @@ import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
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.TachiyomiColorScheme
import eu.kanade.presentation.theme.colorscheme.TakoColorScheme
@ -27,9 +28,30 @@ fun TachiyomiTheme(
appTheme: AppTheme? = null,
amoled: Boolean? = null,
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(
colorScheme = getThemeColorScheme(appTheme, amoled),
colorScheme = getThemeColorScheme(appTheme, isAmoled),
content = content,
)
}
@ -37,16 +59,16 @@ fun TachiyomiTheme(
@Composable
@ReadOnlyComposable
private fun getThemeColorScheme(
appTheme: AppTheme?,
amoled: Boolean?,
appTheme: AppTheme,
isAmoled: Boolean,
): ColorScheme {
val uiPreferences = Injekt.get<UiPreferences>()
val colorScheme = when (appTheme ?: uiPreferences.appTheme().get()) {
val colorScheme = when (appTheme) {
AppTheme.DEFAULT -> TachiyomiColorScheme
AppTheme.MONET -> MonetColorScheme(LocalContext.current)
AppTheme.GREEN_APPLE -> GreenAppleColorScheme
AppTheme.LAVENDER -> LavenderColorScheme
AppTheme.MIDNIGHT_DUSK -> MidnightDuskColorScheme
AppTheme.NORD -> NordColorScheme
AppTheme.STRAWBERRY_DAIQUIRI -> StrawberryColorScheme
AppTheme.TAKO -> TakoColorScheme
AppTheme.TEALTURQUOISE -> TealTurqoiseColorScheme
@ -57,6 +79,6 @@ private fun getThemeColorScheme(
}
return colorScheme.getColorScheme(
isSystemInDarkTheme(),
amoled ?: uiPreferences.themeDarkAmoled().get(),
isAmoled,
)
}

View File

@ -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),
)
}

View File

@ -48,7 +48,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.StringResource
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.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
@ -323,7 +323,7 @@ private fun TrackInfoDialogHomePreviews(
@PreviewParameter(TrackInfoDialogHomePreviewProvider::class)
content: @Composable () -> Unit,
) {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
content()
}

View File

@ -32,7 +32,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
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.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
@ -229,7 +229,7 @@ private fun BaseSelector(
@PreviewLightDark
@Composable
private fun TrackStatusSelectorPreviews() {
TachiyomiTheme {
TachiyomiPreviewTheme {
Surface {
TrackStatusSelector(
selection = 1,

View File

@ -7,6 +7,7 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.windowInsetsPadding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
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.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -40,7 +41,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.graphics.Color
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.text.AnnotatedString
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.input.ImeAction
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.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.DropdownMenu
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.util.system.openInBrowser
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
@ -188,13 +198,7 @@ fun TrackerSearch(
key = { it.hashCode() },
) {
SearchResultItem(
title = it.title,
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(),
trackSearch = it,
selected = it == selected,
onClick = { onSelectedChange(it) },
)
@ -214,18 +218,18 @@ fun TrackerSearch(
@Composable
private fun SearchResultItem(
title: String,
coverUrl: String,
type: String,
startDate: String,
status: String,
score: Float,
description: String,
trackSearch: TrackSearch,
selected: Boolean,
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 borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
var dropDownMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxWidth()
@ -237,7 +241,10 @@ private fun SearchResultItem(
color = borderColor,
shape = shape,
)
.selectable(selected = selected, onClick = onClick)
.combinedClickable(
onLongClick = { dropDownMenuExpanded = true },
onClick = onClick,
)
.padding(12.dp),
) {
if (selected) {
@ -251,28 +258,41 @@ private fun SearchResultItem(
Column {
Row {
MangaCover.Book(
data = coverUrl,
data = trackSearch.cover_url,
modifier = Modifier.height(96.dp),
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = title,
text = trackSearch.title,
modifier = Modifier.padding(end = 28.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
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()) {
SearchResultItemDetails(
title = stringResource(MR.strings.track_type),
text = type,
)
}
if (startDate.isNotBlank()) {
if (trackSearch.start_date.isNotBlank()) {
SearchResultItemDetails(
title = stringResource(MR.strings.label_started),
text = startDate,
text = trackSearch.start_date,
)
}
if (status.isNotBlank()) {
@ -281,10 +301,10 @@ private fun SearchResultItem(
text = status,
)
}
if (score != -1f) {
if (trackSearch.score != -1f) {
SearchResultItemDetails(
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
private fun SearchResultItemDetails(
title: String,
@ -333,5 +380,5 @@ private fun TrackerSearchPreviews(
@PreviewParameter(TrackerSearchPreviewProvider::class)
content: @Composable () -> Unit,
) {
TachiyomiTheme { content() }
TachiyomiPreviewTheme { content() }
}

View File

@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
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 tachiyomi.presentation.core.util.clickableNoIndication
@ -49,7 +49,7 @@ private fun TrackLogoIconPreviews(
@PreviewParameter(TrackLogoIconPreviewProvider::class)
tracker: Tracker,
) {
TachiyomiTheme {
TachiyomiPreviewTheme {
TrackLogoIcon(
tracker = tracker,
onClick = null,

View File

@ -14,11 +14,11 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@Composable
fun rememberRequestPackageInstallsPermissionState(): Boolean {
fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var installGranted by remember { mutableStateOf(false) }
var installGranted by remember { mutableStateOf(initialValue) }
DisposableEffect(lifecycleOwner.lifecycle) {
val observer = object : DefaultLifecycleObserver {

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi
import android.annotation.SuppressLint
import android.app.Application
import android.app.PendingIntent
import android.app.job.JobInfo
import android.content.BroadcastReceiver
import android.content.Context
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.animatorDurationScale
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
@ -51,13 +48,8 @@ import kotlinx.coroutines.flow.onEach
import logcat.AndroidLogcatLogger
import logcat.LogPriority
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 tachiyomi.core.i18n.stringResource
import tachiyomi.core.preference.Preference
import tachiyomi.core.util.system.logcat
import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager
@ -94,7 +86,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
Injekt.importModule(AppModule(this))
Injekt.importModule(DomainModule())
setupAcra()
setupNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
@ -198,28 +189,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
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() {
try {
Notifications.createChannels(this)

View File

@ -410,6 +410,11 @@ object Migrations {
newKey = { Preference.privateKey(it) },
)
}
if (oldVersion < 117) {
prefs.edit {
remove(Preference.appStateKey("trusted_signatures"))
}
}
return true
}

View File

@ -57,22 +57,24 @@ class MangaRestorer(
backupManga: BackupManga,
backupCategories: List<BackupCategory>,
) {
val dbManga = findExistingManga(backupManga)
val manga = backupManga.getMangaImpl()
val restoredManga = if (dbManga == null) {
restoreNewManga(manga)
} else {
restoreExistingManga(manga, dbManga)
}
handler.await(inTransaction = true) {
val dbManga = findExistingManga(backupManga)
val manga = backupManga.getMangaImpl()
val restoredManga = if (dbManga == null) {
restoreNewManga(manga)
} else {
restoreExistingManga(manga, dbManga)
}
restoreMangaDetails(
manga = restoredManga,
chapters = backupManga.chapters,
categories = backupManga.categories,
backupCategories = backupCategories,
history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
tracks = backupManga.tracking,
)
restoreMangaDetails(
manga = restoredManga,
chapters = backupManga.chapters,
categories = backupManga.categories,
backupCategories = backupCategories,
history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
tracks = backupManga.tracking,
)
}
}
private suspend fun findExistingManga(backupManga: BackupManga): Manga? {

View File

@ -103,12 +103,15 @@ class DownloadCache(
scope.launch {
rootDownloadsDirLock.withLock {
try {
val diskCache = diskCacheFile.inputStream().use {
ProtoBuf.decodeFromByteArray<RootDirectory>(it.readBytes())
if (diskCacheFile.exists()) {
val diskCache = diskCacheFile.inputStream().use {
ProtoBuf.decodeFromByteArray<RootDirectory>(it.readBytes())
}
rootDownloadsDir = diskCache
lastRenew = System.currentTimeMillis()
}
rootDownloadsDir = diskCache
lastRenew = System.currentTimeMillis()
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to initialize disk cache" }
diskCacheFile.delete()
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context
import android.graphics.drawable.Drawable
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.api.ExtensionApi
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.emptyFlow
import logcat.LogPriority
import tachiyomi.core.preference.plusAssign
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext
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
* signature is trusted, otherwise the user will be prompted with a warning to trust it before being
* loaded.
*
* @param context The application context.
* @param preferences The application preferences.
*/
class ExtensionManager(
private val context: Context,
private val preferences: SourcePreferences = Injekt.get(),
private val trustExtension: TrustExtension = Injekt.get(),
) {
var isInitialized = false
@ -180,14 +178,22 @@ class ExtensionManager(
val pkgName = installedExt.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)
changed = true
} else if (availableExt != null) {
val hasUpdate = installedExt.updateExists(availableExt)
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
}
}
@ -249,18 +255,19 @@ class ExtensionManager(
}
/**
* Adds the given signature to the list of trusted signatures. It also loads in background the
* extensions that match this signature.
* Adds the given extension to the list of trusted extensions. It also loads in background the
* now trusted extensions.
*
* @param signature The signature to whitelist.
* @param extension the extension to trust
*/
fun trustSignature(signature: String) {
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
if (signature !in untrustedSignatures) return
fun trust(extension: Extension.Untrusted) {
val untrustedPkgNames = _untrustedExtensionsFlow.value.map { it.pkgName }.toSet()
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
launchNow {
@ -354,7 +361,7 @@ class ExtensionManager(
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
if (isUnofficial || availableExt == null) return false
?: return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
}

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
import eu.kanade.domain.source.interactor.OFFICIAL_REPO_BASE_URL
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
@ -36,25 +35,11 @@ internal class ExtensionApi {
suspend fun findExtensions(): List<Extension.Available> {
return withIOContext {
val extensions = buildList {
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
sourcePreferences.extensionRepos().get().flatMap { getExtensions(it) }
}
}
private suspend fun getExtensions(
repoBaseUrl: String,
isOfficialRepo: Boolean,
): List<Extension.Available> {
private suspend fun getExtensions(repoBaseUrl: String): List<Extension.Available> {
return try {
val response = networkService.client
.newCall(GET("$repoBaseUrl/index.min.json"))
@ -63,7 +48,7 @@ internal class ExtensionApi {
with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions(repoBaseUrl, isRepoSource = !isOfficialRepo)
.toExtensions(repoBaseUrl)
}
} catch (e: Throwable) {
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 hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
val hasUpdatedLib = availableExt.libVersion > installedExt.libVersion
val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer || hasUpdatedLib)
val hasUpdate = hasUpdatedVer || hasUpdatedLib
if (hasUpdate) {
extensionsWithUpdate.add(installedExt)
}
@ -111,10 +96,7 @@ internal class ExtensionApi {
return extensionsWithUpdate
}
private fun List<ExtensionJsonObject>.toExtensions(
repoUrl: String,
isRepoSource: Boolean,
): List<Extension.Available> {
private fun List<ExtensionJsonObject>.toExtensions(repoUrl: String): List<Extension.Available> {
return this
.filter {
val libVersion = it.extractLibVersion()
@ -133,7 +115,6 @@ internal class ExtensionApi {
apkName = it.apk,
iconUrl = "$repoUrl/icon/${it.pkg}.png",
repoUrl = repoUrl,
isRepoSource = isRepoSource,
)
}
}

View File

@ -27,10 +27,8 @@ sealed class Extension {
val icon: Drawable?,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false,
val isShared: Boolean,
val repoUrl: String? = null,
val isRepoSource: Boolean = false,
) : Extension()
data class Available(
@ -45,7 +43,6 @@ sealed class Extension {
val apkName: String,
val iconUrl: String,
val repoUrl: String,
val isRepoSource: Boolean,
) : Extension() {
data class Source(

View File

@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import dalvik.system.PathClassLoader
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.model.Extension
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.util.lang.Hash
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
import eu.kanade.tachiyomi.util.system.isDevFlavor
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
@ -41,6 +41,7 @@ import java.io.File
internal object ExtensionLoader {
private val preferences: SourcePreferences by injectLazy()
private val trustExtension: TrustExtension by injectLazy()
private val loadNsfwSource by lazy {
preferences.showNsfwSource().get()
}
@ -49,8 +50,6 @@ internal object ExtensionLoader {
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
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_MAX = 1.5
@ -60,9 +59,6 @@ internal object ExtensionLoader {
PackageManager.GET_SIGNATURES or
(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 fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
@ -119,12 +115,6 @@ internal object ExtensionLoader {
* @param context The application context.
*/
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 installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -262,7 +252,7 @@ internal object ExtensionLoader {
if (signatures.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error
} else if (!hasTrustedSignature(signatures)) {
} else if (!trustExtension.isTrusted(pkgInfo, signatures.last())) {
val extension = Extension.Untrusted(
extName,
pkgName,
@ -281,9 +271,6 @@ internal object ExtensionLoader {
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 {
PathClassLoader(appInfo.sourceDir, null, context.classLoader)
} catch (e: Exception) {
@ -333,7 +320,6 @@ internal object ExtensionLoader {
isNsfw = isNsfw,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = !isOfficiallySigned(signatures),
icon = appInfo.loadIcon(pkgManager),
isShared = extensionInfo.isShared,
)
@ -393,19 +379,6 @@ internal object ExtensionLoader {
?.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
* have sourceDir which breaks assets loading (used for getting icon here).

View File

@ -26,6 +26,9 @@ interface ThemingDelegate {
AppTheme.MIDNIGHT_DUSK -> {
resIds += R.style.Theme_Tachiyomi_MidnightDusk
}
AppTheme.NORD -> {
resIds += R.style.Theme_Tachiyomi_Nord
}
AppTheme.STRAWBERRY_DAIQUIRI -> {
resIds += R.style.Theme_Tachiyomi_StrawberryDaiquiri
}

View File

@ -195,8 +195,8 @@ class ExtensionsScreenModel(
}
}
fun trustSignature(signatureHash: String) {
extensionManager.trustSignature(signatureHash)
fun trustExtension(extension: Extension.Untrusted) {
extensionManager.trust(extension)
}
@Immutable

View File

@ -1,7 +1,5 @@
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.collectAsState
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.components.AppBar
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.ui.browse.extension.details.ExtensionDetailsScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
@ -29,11 +28,14 @@ fun extensionsTab(
badgeNumber = state.updates.takeIf { it > 0 },
searchEnabled = true,
actions = persistentListOf(
AppBar.Action(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_filter),
icon = Icons.Outlined.Translate,
onClick = { navigator.push(ExtensionFilterScreen()) },
),
AppBar.OverflowAction(
title = stringResource(MR.strings.label_extension_repos),
onClick = { navigator.push(ExtensionReposScreen()) },
),
),
content = { contentPadding, _ ->
ExtensionScreen(
@ -61,7 +63,7 @@ fun extensionsTab(
},
onInstallExtension = extensionsScreenModel::installExtension,
onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) },
onTrustExtension = { extensionsScreenModel.trustSignature(it.signatureHash) },
onTrustExtension = { extensionsScreenModel.trustExtension(it) },
onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) },
onUpdateExtension = extensionsScreenModel::updateExtension,
onRefresh = extensionsScreenModel::findAvailableExtensions,

View File

@ -5,7 +5,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
@ -30,13 +29,11 @@ data class ExtensionDetailsScreen(
}
val navigator = LocalNavigator.currentOrThrow
val uriHandler = LocalUriHandler.current
ExtensionDetailsScreen(
navigateUp = navigator::pop,
state = state,
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) },
onClickEnableAll = { screenModel.toggleSources(true) },
onClickDisableAll = { screenModel.toggleSources(false) },
onClickClearCookies = screenModel::clearCookies,

View File

@ -29,9 +29,6 @@ import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
private const val URL_EXTENSION_COMMITS =
"https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
class ExtensionDetailsScreenModel(
pkgName: String,
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() {
val extension = state.value.extension ?: return
@ -131,22 +118,6 @@ class ExtensionDetailsScreenModel(
?.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
data class State(
val extension: Extension.Installed? = null,

View File

@ -157,6 +157,7 @@ class LibraryScreenModel(
prefs.filterStarted,
prefs.filterBookmarked,
prefs.filterCompleted,
prefs.filterIntervalCustom,
) + trackFilter.values
).any { it != TriState.DISABLED }
}
@ -178,12 +179,13 @@ class LibraryScreenModel(
): LibraryMap {
val prefs = getLibraryItemPreferencesFlow().first()
val downloadedOnly = prefs.globalFilterDownloaded
val filterDownloaded =
if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded
val skipOutsideReleasePeriod = prefs.skipOutsideReleasePeriod
val filterDownloaded = if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded
val filterUnread = prefs.filterUnread
val filterStarted = prefs.filterStarted
val filterBookmarked = prefs.filterBookmarked
val filterCompleted = prefs.filterCompleted
val filterIntervalCustom = prefs.filterIntervalCustom
val isNotLoggedInAnyTrack = loggedInTrackers.isEmpty()
@ -215,6 +217,14 @@ class LibraryScreenModel(
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 ->
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
@ -225,7 +235,7 @@ class LibraryScreenModel(
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
return@tracking !isExcluded && isIncluded
!isExcluded && isIncluded
}
val filterFn: (LibraryItem) -> Boolean = {
@ -234,6 +244,7 @@ class LibraryScreenModel(
filterFnStarted(it) &&
filterFnBookmarked(it) &&
filterFnCompleted(it) &&
filterFnIntervalCustom(it) &&
filterFnTracking(it)
}
@ -320,6 +331,7 @@ class LibraryScreenModel(
libraryPreferences.downloadBadge().changes(),
libraryPreferences.localBadge().changes(),
libraryPreferences.languageBadge().changes(),
libraryPreferences.autoUpdateMangaRestrictions().changes(),
preferences.downloadedOnly().changes(),
libraryPreferences.filterDownloaded().changes(),
@ -327,20 +339,22 @@ class LibraryScreenModel(
libraryPreferences.filterStarted().changes(),
libraryPreferences.filterBookmarked().changes(),
libraryPreferences.filterCompleted().changes(),
transform = {
ItemPreferences(
downloadBadge = it[0] as Boolean,
localBadge = it[1] as Boolean,
languageBadge = it[2] as Boolean,
globalFilterDownloaded = it[3] as Boolean,
filterDownloaded = it[4] as TriState,
filterUnread = it[5] as TriState,
filterStarted = it[6] as TriState,
filterBookmarked = it[7] as TriState,
filterCompleted = it[8] as TriState,
)
},
)
libraryPreferences.filterIntervalCustom().changes(),
) {
ItemPreferences(
downloadBadge = it[0] as Boolean,
localBadge = it[1] as Boolean,
languageBadge = it[2] as Boolean,
skipOutsideReleasePeriod = LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in (it[3] as Set<*>),
globalFilterDownloaded = it[4] as Boolean,
filterDownloaded = it[5] as TriState,
filterUnread = it[6] as TriState,
filterStarted = it[7] as TriState,
filterBookmarked = it[8] as TriState,
filterCompleted = it[9] as TriState,
filterIntervalCustom = it[10] as TriState,
)
}
}
/**
@ -699,6 +713,7 @@ class LibraryScreenModel(
val downloadBadge: Boolean,
val localBadge: Boolean,
val languageBadge: Boolean,
val skipOutsideReleasePeriod: Boolean,
val globalFilterDownloaded: Boolean,
val filterDownloaded: TriState,
@ -706,6 +721,7 @@ class LibraryScreenModel(
val filterStarted: TriState,
val filterBookmarked: TriState,
val filterCompleted: TriState,
val filterIntervalCustom: TriState,
)
@Immutable

View File

@ -56,6 +56,7 @@ import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
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.DefaultNavigatorScreenTransition
@ -64,7 +65,6 @@ import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache
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.extension.api.ExtensionApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
@ -73,7 +73,6 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.deeplink.DeepLinkScreen
import eu.kanade.tachiyomi.ui.home.HomeScreen
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.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
@ -92,7 +91,6 @@ import tachiyomi.core.Constants
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
@ -313,27 +311,6 @@ class MainActivity : BaseActivity() {
@Composable
private fun CheckForUpdates() {
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
LaunchedEffect(Unit) {
@ -446,10 +423,18 @@ class MainActivity : BaseActivity() {
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

View File

@ -378,12 +378,15 @@ class MangaScreenModel(
fun setFetchInterval(manga: Manga, interval: Int) {
screenModelScope.launchIO {
updateManga.awaitUpdateFetchInterval(
// Custom intervals are negative
manga.copy(fetchInterval = -interval),
)
val updatedManga = mangaRepository.getMangaById(manga.id)
updateSuccessState { it.copy(manga = updatedManga) }
if (
updateManga.awaitUpdateFetchInterval(
// Custom intervals are negative
manga.copy(fetchInterval = -interval),
)
) {
val updatedManga = mangaRepository.getMangaById(manga.id)
updateSuccessState { it.copy(manga = updatedManga) }
}
}
}

View File

@ -462,7 +462,8 @@ class ReaderViewModel @JvmOverloads constructor(
manga.title,
manga.source,
)
if (isNextChapterDownloaded) return@launchIO
if (!isNextChapterDownloaded) return@launchIO
val chaptersToDownload = getNextChapters.await(manga.id, nextChapter.id!!).run {
if (readerPreferences.skipDupe().get()) {
removeDuplicates(nextChapter.toDomainChapter()!!)

View File

@ -102,7 +102,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
},
)
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)) {
NavigationRegion.MENU -> activity.toggleMenu()
NavigationRegion.NEXT -> moveToNext()

View File

@ -214,6 +214,9 @@ class WebtoonRecyclerView @JvmOverloads constructor(
if (!isZooming && doubleTapZoom) {
if (scaleX != DEFAULT_RATE) {
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
layoutParams.height = originalHeight
halfHeight = layoutParams.height / 2
requestLayout()
} else {
val toScale = 2f
val toX = (halfWidth - ev.x) * (toScale - 1)

View File

@ -111,7 +111,14 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
},
)
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)) {
NavigationRegion.MENU -> activity.toggleMenu()
NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown()

View File

@ -56,12 +56,12 @@ class CrashLogUtil(
val availableExtension = availableExtensions[it.pkgName]
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}
Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"}
Obsolete: ${it.isObsolete} / Unofficial: ${it.isUnofficial}
Obsolete: ${it.isObsolete}
""".trimIndent()
}

View File

@ -43,8 +43,6 @@ fun Long.toDateKey(): Date {
return Date.from(instant.truncatedTo(ChronoUnit.DAYS))
}
private const val MILLISECONDS_IN_DAY = 86_400_000L
fun Date.toRelativeString(
context: Context,
relative: Boolean = true,
@ -69,6 +67,8 @@ fun Date.toRelativeString(
}
}
private const val MILLISECONDS_IN_DAY = 86_400_000L
private val Date.timeWithOffset: Long
get() {
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
}

View File

@ -333,6 +333,37 @@
<item name="colorPrimaryInverse">@color/tidalwave_primaryInverse</item>
</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 ==-->
<style name="ThemeOverlay.Tachiyomi.Amoled" parent="" />

View File

@ -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"
}

View File

@ -6,7 +6,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
buildscript {
dependencies {
classpath(libs.android.shortcut.gradle)
classpath(libs.google.services.gradle)
classpath(libs.aboutLibraries.gradle)
classpath(libs.sqldelight.gradle)
classpath(libs.moko.gradle)

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network
import android.content.Context
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.UserAgentInterceptor
import okhttp3.Cache
@ -30,9 +31,10 @@ class NetworkHelper(
maxSize = 5L * 1024 * 1024, // 5 MiB
),
)
.addInterceptor(BrotliInterceptor)
.addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
.addNetworkInterceptor(IgnoreGzipInterceptor())
.addNetworkInterceptor(BrotliInterceptor)
if (preferences.verboseLogging().get()) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {

View File

@ -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)
}
}

View File

@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.source.model.SourceWithCount
@ -38,18 +39,19 @@ class SourceRepositoryImpl(
}
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
val sourceIdWithFavoriteCount =
handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }
return sourceIdWithFavoriteCount.map { sourceIdsWithCount ->
sourceIdsWithCount
.map { (sourceId, count) ->
return combine(
handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() },
sourceManager.catalogueSources,
) { sourceIdWithFavoriteCount, _ -> sourceIdWithFavoriteCount }
.map {
it.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
val domainSource = mapSourceToDomainSource(source).copy(
isStub = source is StubSource,
)
domainSource to count
}
}
}
}
override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> {

View File

@ -85,26 +85,6 @@ class LibraryPreferences(
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(
"pref_filter_library_tracked_${id}_v2",
TriState.DISABLED,

View File

@ -14,11 +14,11 @@ class FetchInterval(
private val getChaptersByMangaId: GetChaptersByMangaId,
) {
suspend fun toMangaUpdateOrNull(
suspend fun toMangaUpdate(
manga: Manga,
dateTime: ZonedDateTime,
window: Pair<Long, Long>,
): MangaUpdate? {
): MangaUpdate {
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true),
zone = dateTime.zone,
@ -30,11 +30,7 @@ class FetchInterval(
}
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
null
} else {
MangaUpdate(id = manga.id, nextUpdate = nextUpdate, fetchInterval = interval)
}
return MangaUpdate(id = manga.id, nextUpdate = nextUpdate, fetchInterval = interval)
}
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
@ -96,34 +92,31 @@ class FetchInterval(
dateTime: ZonedDateTime,
window: Pair<Long, Long>,
): Long {
return if (
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
manga.fetchInterval == 0
) {
val latestDate = ZonedDateTime.ofInstant(
if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(),
dateTime.zone,
)
.toLocalDate()
.atStartOfDay()
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
val cycle = timeSinceLatest.floorDiv(
interval.absoluteValue.takeIf { interval < 0 }
?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10),
)
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
} else {
manga.nextUpdate
if (manga.nextUpdate in window.first.rangeTo(window.second + 1)) {
return manga.nextUpdate
}
val latestDate = ZonedDateTime.ofInstant(
if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(),
dateTime.zone,
)
.toLocalDate()
.atStartOfDay()
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
val cycle = timeSinceLatest.floorDiv(
interval.absoluteValue.takeIf { interval < 0 }
?: increaseInterval(interval, timeSinceLatest, increaseWhenOver = 10),
)
return latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
}
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
// double delta again if missed more than 9 check in new delta
val cycle = timeSinceLatest.floorDiv(delta) + 1
return if (cycle > doubleWhenOver) {
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
return if (cycle > increaseWhenOver) {
increaseInterval(delta * 2, timeSinceLatest, increaseWhenOver)
} else {
delta
}

View File

@ -1,5 +1,5 @@
[versions]
compiler = "1.5.7"
compiler = "1.5.8"
compose-bom = "2023.12.00-alpha04"
accompanist = "0.33.2-alpha"

View File

@ -1,5 +1,5 @@
[versions]
kotlin_version = "1.9.21"
kotlin_version = "1.9.22"
serialization_version = "1.6.2"
xml_serialization_version = "0.86.3"

View File

@ -1,6 +1,5 @@
[versions]
aboutlib_version = "10.10.0"
acra = "5.11.3"
leakcanary = "2.13"
moko = "0.23.0"
okhttp_version = "5.0.0-alpha.12"
@ -13,7 +12,6 @@ voyager = "1.0.0"
[libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
google-services-gradle = "com.google.gms:google-services:4.4.0"
rxjava = "io.reactivex:rxjava:1.3.8"
@ -68,10 +66,6 @@ moko-gradle = { module = "dev.icerock.moko:resources-generator", version.ref = "
logcat = "com.squareup.logcat:logcat:0.1"
acra-http = { module = "ch.acra:acra-http", version.ref = "acra" }
acra-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" }
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.0"
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" }
@ -89,7 +83,7 @@ sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref
junit = "org.junit.jupiter:junit-jupiter:5.10.1"
kotest-assertions = "io.kotest:kotest-assertions-core:5.8.0"
mockk = "io.mockk:mockk:1.13.8"
mockk = "io.mockk:mockk:1.13.9"
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
@ -99,7 +93,6 @@ voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", vers
ktlint = "org.jlleitschuh.gradle:ktlint-gradle:12.0.3"
[bundles]
acra = ["acra-http", "acra-scheduler"]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]
js-engine = ["quickjs-android"]
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="unofficial_extension_message">ይህ ቅጥያ ከኦፊሴላዊው የታቺዮሚ ቅጥያዎች ዝርዝር አይደለም ፡፡</string>
<string name="obsolete_extension_message">ይህ ቅጥያ ከእንግዲህ አይገኝም።</string>
<string name="untrusted_extension_message">ይህ ቅጥያ ባልታመነ የእውቅና ማረጋገጫ ተፈርሟል እና አልነቃም።
\n
@ -10,7 +9,6 @@
<string name="untrusted_extension">የማይታመን ቅጥያ</string>
<string name="ext_uninstall">ማራገፍ</string>
<string name="ext_untrusted">የማይታመን</string>
<string name="ext_unofficial">ኦፊሴላዊ ያልሆነ</string>
<string name="ext_trust">አደራ</string>
<string name="ext_installed">ተጭኗል</string>
<string name="ext_installing">በመጫን ላይ</string>

View File

@ -374,8 +374,6 @@
<string name="invalid_backup_file_missing_manga">النسخة الإحتياطية لا تحتوي على أيّة إدخالات المكتبة.</string>
<string name="invalid_backup_file">ملفُّ النسخ الاحتياطيِّ غير صالح</string>
<string name="tracking_info">مزامنة أحادية تُحدِّث قراءة الفصول في المتتبعات الخارجية، ولك تعيين التتبِّع لكلِّ مدخلة على حدى، وذلك من زرِّ التتبع فيهم.</string>
<string name="unofficial_extension_message">ليست هذه الإضافة من القائمة الرسمية.</string>
<string name="ext_unofficial">غير رسمي</string>
<string name="pref_library_update_refresh_metadata_summary">تحقق من وجود غلاف جديد وتفاصيل جديدة عند تحديث المكتبة</string>
<string name="pref_library_update_refresh_metadata">تحديث البيانات الوصفية تلقائياً</string>
<string name="sort_by_upload_date">حسب تاريخ الرفع</string>
@ -708,10 +706,6 @@
<string name="pref_library_columns_per_row">%d كلَّ صفٍّ</string>
<string name="action_set_interval">عيِّد المدة</string>
<string name="action_filter_interval_custom">مدة جلب مخصَّصة</string>
<string name="action_filter_interval_long">اجلب كلَّ شهر (٢٨ يومًا)</string>
<string name="action_filter_interval_passed">مرت مدة الالتماس</string>
<string name="action_filter_interval_late">١٠+ التماسات متأخرة</string>
<string name="action_filter_interval_dropped">ربما أُلغيت؟ ٢٠ التماسًا متأخرًا وشهران</string>
<string name="manga_display_interval_title">قدِّر كلَّ</string>
<string name="pref_update_only_in_release_period">ليس ضمن مدة الإصدار المتوقعة</string>
<string name="skipped_reason_not_in_release_period">تُخُطِّيت بسبب عدم توقع صدور اليوم</string>

View File

@ -54,11 +54,7 @@
<string name="action_filter_bookmarked">Bookmarked</string>
<string name="action_filter_tracked">Tracked</string>
<string name="action_filter_unread">Unread</string>
<string name="action_filter_interval_custom">Customized fetch interval</string>
<string name="action_filter_interval_long">Fetch monthly (28 days)</string>
<string name="action_filter_interval_late">Late 10+ check</string>
<string name="action_filter_interval_dropped">Dropped? Late 20+ and 2 months</string>
<string name="action_filter_interval_passed">Passed check period</string>
<string name="action_filter_interval_custom">Customized update frequency</string>
<!-- reserved for #4048 -->
<string name="action_filter_empty">Remove filter</string>
<string name="action_sort_alpha">Alphabetically</string>
@ -225,6 +221,7 @@
<string name="theme_greenapple">Green Apple</string>
<string name="theme_lavender">Lavender</string>
<string name="theme_midnightdusk">Midnight Dusk</string>
<string name="theme_nord">Nord</string>
<string name="theme_strawberrydaiquiri">Strawberry Daiquiri</string>
<string name="theme_tako">Tako</string>
<string name="theme_tealturquoise">Teal &amp; Turquoise</string>
@ -314,15 +311,13 @@
<string name="ext_installing">Installing</string>
<string name="ext_installed">Installed</string>
<string name="ext_trust">Trust</string>
<string name="ext_unofficial">Unofficial</string>
<string name="ext_untrusted">Untrusted</string>
<string name="ext_uninstall">Uninstall</string>
<string name="ext_app_info">App info</string>
<string name="untrusted_extension">Untrusted extension</string>
<string name="untrusted_extension_message">This extension was signed by any unknown author and wasn\'t loaded.\n\nMalicious extensions can read any stored login credentials or execute arbitrary code.\n\nBy trusting this extension\'s certificate, you accept these risks.</string>
<string name="untrusted_extension_message">Malicious extensions can read any stored login credentials or execute arbitrary code.\n\nBy trusting this extension, you accept these risks.</string>
<string name="obsolete_extension_message">This extension is no longer available. It may not function properly and can cause issues with the app. Uninstalling it is recommended.</string>
<string name="unofficial_extension_message">This extension is not from the official list.</string>
<string name="extension_api_error">Failed to get extensions list</string>
<string name="extension_api_error">Failed to fetch available extensions</string>
<string name="ext_info_version">Version</string>
<string name="ext_info_language">Language</string>
<string name="ext_info_age_rating">Age rating</string>
@ -337,6 +332,7 @@
<string name="ext_installer_private" translatable="false">Private</string>
<string name="ext_installer_shizuku_stopped">Shizuku is not running</string>
<string name="ext_installer_shizuku_unavailable_dialog">Install and start Shizuku to use Shizuku as extension installer.</string>
<string name="ext_revoke_trust">Revoke trusted unknown extensions</string>
<!-- Extension repos -->
<string name="label_extension_repos">Extension repos</string>
@ -348,7 +344,7 @@
<string name="action_delete_repo">Delete repo</string>
<string name="invalid_repo_name">Invalid repo URL</string>
<string name="delete_repo_confirmation">Do you wish to delete the repo \"%s\"?</string>
<string name="repo_extension_message">This extension is from an external repo. Tap to view the repo.</string>
<string name="action_open_repo">Open source repo</string>
<!-- Reader section -->
<string name="pref_fullscreen">Fullscreen</string>
@ -682,9 +678,9 @@
<string name="display_mode_chapter">Chapter %1$s</string>
<string name="manga_display_interval_title">Estimate every</string>
<string name="manga_display_modified_interval_title">Set to update every</string>
<string name="manga_interval_header">Next update</string>
<!-- "... around 2 days" -->
<string name="manga_interval_expected_update">Next update expected in around %1$s, checking around every %2$s</string>
<string name="manga_interval_expected_update">New chapters predicted to be released in around %1$s, checking around every %2$s.</string>
<string name="manga_interval_expected_update_soon">Soon</string>
<string name="manga_interval_custom_amount">Custom update frequency:</string>
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
<string name="chapter_error">Error</string>
@ -813,7 +809,7 @@
<!-- Crash screen -->
<string name="crash_screen_title">Whoops!</string>
<string name="crash_screen_description">%s ran into an unexpected error. We suggest you share the crash logs in our support channel on Discord.</string>
<string name="crash_screen_description">%s ran into an unexpected error.</string>
<string name="crash_screen_restart_application">Restart the application</string>
<!-- Stats screen -->

View File

@ -89,7 +89,6 @@
<string name="categories">Катэгорыі</string>
<string name="ext_uninstall">Выдаліць</string>
<string name="ext_untrusted">Ненадзейны</string>
<string name="ext_unofficial">Неафіцыйны</string>
<string name="ext_trust">Надзейны</string>
<string name="ext_installed">Усталяваны</string>
<string name="ext_installing">Устаноўка</string>
@ -185,7 +184,6 @@
<string name="pref_fullscreen">Поўнаэкранны рэжым</string>
<string name="ext_nsfw_warning">Можа ўтрымліваць кантэнт NSFW (18+)</string>
<string name="ext_nsfw_short">18+</string>
<string name="unofficial_extension_message">Гэта пашырэнне не ўваходзіць у афіцыйны спіс пашырэньні Tachiyomi.</string>
<string name="obsolete_extension_message">Гэта пашырэнне больш недаступна.</string>
<string name="untrusted_extension_message">Гэта пашырэнне было падпісана ненадзейным сертыфікатам і не было актывавана.
\n

View File

@ -52,4 +52,20 @@
<item quantity="one">Следващата непрочетена глава</item>
<item quantity="other">Следващите %d непрочетени глави</item>
</plurals>
<plurals name="day">
<item quantity="one">1 ден</item>
<item quantity="other">%d дни</item>
</plurals>
<plurals name="missing_chapters">
<item quantity="one">Липсваща %1$s глава</item>
<item quantity="other">Липсващи %1$s глави</item>
</plurals>
<plurals name="download_amount">
<item quantity="one">Следваща глава</item>
<item quantity="other">Следващи %d глави</item>
</plurals>
<plurals name="num_repos">
<item quantity="one">%d хранилище</item>
<item quantity="other">%d хранилища</item>
</plurals>
</resources>

View File

@ -329,7 +329,7 @@
<string name="restoring_backup_error">Възстановяването неуспешно</string>
<string name="restore_in_progress">Възстановяването е в процес</string>
<string name="creating_backup_error">Съхраняването неуспешно</string>
<string name="backup_in_progress">Копието вече се запазва</string>
<string name="backup_in_progress">Архивирането вече е в ход</string>
<string name="restore_duration">%02d мин, %02d сек</string>
<string name="pref_webtoon_side_padding">Странично разстояние</string>
<string name="pref_category_reading">Четене</string>
@ -344,7 +344,7 @@
<string name="lock_never">Никога</string>
<string name="lock_always">Винаги</string>
<string name="lock_when_idle">Заключи при неактивност</string>
<string name="lock_with_biometrics">Изисквай отключване</string>
<string name="lock_with_biometrics">Изискване на отключване</string>
<string name="pref_category_security">Сигурност</string>
<string name="pref_manage_notifications">Уведомления</string>
<string name="theme_system">Система на абонаментите</string>
@ -383,8 +383,6 @@
<string name="invalid_backup_file">Невалиден резервен файл</string>
<string name="pref_show_reading_mode_summary">Накратко показване на текущия режим при отваряне на четеца</string>
<string name="pref_show_reading_mode">Покажи режим на четене</string>
<string name="unofficial_extension_message">Това разширение не е част от официалните разширения на Тачийоми.</string>
<string name="ext_unofficial">Неофициален</string>
<string name="pref_library_update_refresh_metadata_summary">Провери за нова корица и подробности при актуализиране на библиотеката</string>
<string name="pref_library_update_refresh_metadata">Автоматично обновяване на метаданните</string>
<string name="action_disable">Деактивирай</string>
@ -577,7 +575,7 @@
<string name="are_you_sure">Сигурни ли сте\?</string>
<string name="popular">Популярни</string>
<string name="unfinished_list">Списък с незавършени</string>
<string name="crash_screen_description">%s се сблъска с неочаквана грешка. Препоръчваме да заснемете това съобщение, да запазите записите от крашовете и да ги споделите в нашия канал за поддръжка в Discord.</string>
<string name="crash_screen_description">%s се сблъска с неочаквана грешка.</string>
<string name="updates_last_update_info">Последно обновяване на библиотеката: %1$s</string>
<string name="reading_list">Списък за четене</string>
<string name="cant_open_last_read_chapter">Не може да се отвори последната прочетена глава</string>

View File

@ -365,8 +365,6 @@
<string name="pref_cutout_short">বিষয়বস্তু কাটআউটে দেখুন</string>
<string name="ext_nsfw_warning">প্রাপ্তবয়স্কদের (18+) কন্টেন্ট থাকতে পারে</string>
<string name="ext_nsfw_short">১৮+</string>
<string name="unofficial_extension_message">এক্সটেনশনটি তাচিওমির অনুমোদিত এক্সটেনশন তালিকায় নেই।</string>
<string name="ext_unofficial">অনুমোদনহীন</string>
<string name="ext_updates_pending">অনিষ্পন্ন হালনাগাদ</string>
<string name="pref_library_update_refresh_metadata_summary">সংগ্রহশালা হালনাগেদের সময় নতুন মোড়ক এবং বর্ণনা খুঁজুন</string>
<string name="pinned_sources">গাঁথুনিকৃত</string>
@ -625,7 +623,6 @@
<string name="pref_onboarding_guide">অনবর্ডিং গাইড</string>
<string name="onboarding_action_finish">শুরু করুন</string>
<string name="action_apply">এপ্লাই</string>
<string name="action_filter_interval_passed">চেক করার সময় পার হয়েছে</string>
<string name="onboarding_storage_selection_required">একটি ফোল্ডার অবশ্যই ঠিক করতে হবে</string>
<string name="onboarding_permission_notifications">নোটিফিকেশনের পারমিশন</string>
<string name="action_revert_to_default">সকল সেটিং ডিফল্ট করুন</string>
@ -643,5 +640,4 @@
<string name="sort_category_confirmation">আপনি কি বর্ণানুক্রমিকভাবে ফিল্টার করতে চান</string>
<string name="action_ok">ঠিক আছে</string>
<string name="action_sort_next_updated">পরবর্তী আপডেটের সম্ভাব্য সময়</string>
<string name="action_filter_interval_long">প্রতিমাসে রিফ্রেস করুন (২৮ দিন)</string>
</resources>

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