mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
49991d38d9 | |||
33c62ab711 | |||
899bd26956 | |||
a37f3eb709 | |||
9ae71dfe93 | |||
c65a9aecf5 | |||
02e50411de | |||
6e822dfd5b | |||
7292dadd5f | |||
b1067b942e | |||
d6c4af89c4 | |||
cf6f7c521c | |||
c6601c1f94 | |||
68899aea61 | |||
c3edf9b5d0 | |||
97e04392d3 | |||
3d178737b1 | |||
bf737cf95c | |||
c91ec9a33b | |||
a8040cb21a | |||
f60782f11f | |||
7d6e1bdafc | |||
5854ad97e0 | |||
4b8fa059d5 | |||
3dc2f9a711 | |||
8033a94ee2 | |||
028da099dd | |||
e6c6c32d81 | |||
bce6af62fc | |||
6510a9617a | |||
14510f1d26 | |||
f115edf2ea |
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@ -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
|
||||
|
||||
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -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
|
||||
|
6
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
6
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -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
|
||||
|
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
12
README.md
12
README.md
@ -1,6 +1,6 @@
|
||||
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
||||
|-------|----------|---------|------------|---------|
|
||||
| [](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||
| Build | Stable | Weekly Preview | Contribute |
|
||||
|-------|--------|----------------|------------|
|
||||
| [](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) |
|
||||
|
||||
# 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: [](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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@ -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.** { *; }
|
@ -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>
|
||||
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
@ -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
|
@ -0,0 +1,11 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetExtensionRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun subscribe(): Flow<Set<String>> {
|
||||
return preferences.extensionRepos().changes()
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.getAndSet
|
||||
|
||||
class TrustExtension(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean {
|
||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash"
|
||||
return key in preferences.trustedExtensions().get()
|
||||
}
|
||||
|
||||
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||
preferences.trustedExtensions().getAndSet { exts ->
|
||||
// Remove previously trusted versions
|
||||
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
|
||||
|
||||
removed.also {
|
||||
it += "$pkgName:$versionCode:$signatureHash"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun revokeAll() {
|
||||
preferences.trustedExtensions().delete()
|
||||
}
|
||||
}
|
@ -81,9 +81,9 @@ class UpdateManga(
|
||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
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 {
|
||||
|
@ -1,13 +0,0 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetSourceRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun subscribe(): Flow<List<String>> {
|
||||
return preferences.extensionRepos().changes()
|
||||
.map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||
}
|
||||
}
|
@ -38,11 +38,14 @@ class SourcePreferences(
|
||||
SetMigrateSorting.Direction.ASCENDING,
|
||||
)
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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")) {}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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 = {},
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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 = """
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 -> {
|
||||
|
@ -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() {
|
||||
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,72 @@
|
||||
package eu.kanade.presentation.theme.colorscheme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Colors for Nord theme
|
||||
* https://www.nordtheme.com/docs/colors-and-palettes
|
||||
* for the light theme, the primary color is switched with the tertiary for better contrast in some case
|
||||
*/
|
||||
internal object NordColorScheme : BaseColorScheme() {
|
||||
|
||||
override val darkScheme = darkColorScheme(
|
||||
primary = Color(0xFF88C0D0),
|
||||
onPrimary = Color(0xFF2E3440),
|
||||
primaryContainer = Color(0xFF88C0D0),
|
||||
onPrimaryContainer = Color(0xFF2E3440),
|
||||
inversePrimary = Color(0xFF397E91),
|
||||
secondary = Color(0xFF81A1C1),
|
||||
onSecondary = Color(0xFF2E3440),
|
||||
secondaryContainer = Color(0xFF81A1C1),
|
||||
onSecondaryContainer = Color(0xFF2E3440),
|
||||
tertiary = Color(0xFF5E81AC),
|
||||
onTertiary = Color(0xFF000000),
|
||||
tertiaryContainer = Color(0xFF5E81AC),
|
||||
onTertiaryContainer = Color(0xFF000000),
|
||||
background = Color(0xFF2E3440),
|
||||
onBackground = Color(0xFFECEFF4),
|
||||
surface = Color(0xFF3B4252),
|
||||
onSurface = Color(0xFFECEFF4),
|
||||
surfaceVariant = Color(0xFF2E3440),
|
||||
onSurfaceVariant = Color(0xFFECEFF4),
|
||||
surfaceTint = Color(0xFF88C0D0),
|
||||
inverseSurface = Color(0xFFD8DEE9),
|
||||
inverseOnSurface = Color(0xFF2E3440),
|
||||
outline = Color(0xFF6d717b),
|
||||
outlineVariant = Color(0xFF90939a),
|
||||
onError = Color(0xFF2E3440),
|
||||
errorContainer = Color(0xFFBF616A),
|
||||
onErrorContainer = Color(0xFF000000),
|
||||
)
|
||||
|
||||
override val lightScheme = lightColorScheme(
|
||||
primary = Color(0xFF5E81AC),
|
||||
onPrimary = Color(0xFF000000),
|
||||
primaryContainer = Color(0xFF5E81AC),
|
||||
onPrimaryContainer = Color(0xFF000000),
|
||||
inversePrimary = Color(0xFF8CA8CD),
|
||||
secondary = Color(0xFF81A1C1),
|
||||
onSecondary = Color(0xFF2E3440),
|
||||
secondaryContainer = Color(0xFF81A1C1),
|
||||
onSecondaryContainer = Color(0xFF2E3440),
|
||||
tertiary = Color(0xFF88C0D0),
|
||||
onTertiary = Color(0xFF2E3440),
|
||||
tertiaryContainer = Color(0xFF88C0D0),
|
||||
onTertiaryContainer = Color(0xFF2E3440),
|
||||
background = Color(0xFFECEFF4),
|
||||
onBackground = Color(0xFF2E3440),
|
||||
surface = Color(0xFFE5E9F0),
|
||||
onSurface = Color(0xFF2E3440),
|
||||
surfaceVariant = Color(0xFFffffff),
|
||||
onSurfaceVariant = Color(0xFF2E3440),
|
||||
surfaceTint = Color(0xFF5E81AC),
|
||||
inverseSurface = Color(0xFF3B4252),
|
||||
inverseOnSurface = Color(0xFFECEFF4),
|
||||
outline = Color(0xFF2E3440),
|
||||
onError = Color(0xFFECEFF4),
|
||||
errorContainer = Color(0xFFBF616A),
|
||||
onErrorContainer = Color(0xFF000000),
|
||||
)
|
||||
}
|
@ -48,7 +48,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import 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()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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() }
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -410,6 +410,11 @@ object Migrations {
|
||||
newKey = { Preference.privateKey(it) },
|
||||
)
|
||||
}
|
||||
if (oldVersion < 117) {
|
||||
prefs.edit {
|
||||
remove(Preference.appStateKey("trusted_signatures"))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -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? {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -195,8 +195,8 @@ class ExtensionsScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun trustSignature(signatureHash: String) {
|
||||
extensionManager.trustSignature(signatureHash)
|
||||
fun trustExtension(extension: Extension.Untrusted) {
|
||||
extensionManager.trust(extension)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()!!)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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="" />
|
||||
|
||||
|
@ -1,69 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "777921915939",
|
||||
"firebase_url": "https://tachiyomi-47364.firebaseio.com",
|
||||
"project_id": "tachiyomi-47364",
|
||||
"storage_bucket": "tachiyomi-47364.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:777921915939:android:36544cd2d96c50c7",
|
||||
"android_client_info": {
|
||||
"package_name": "eu.kanade.tachiyomi"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:777921915939:android:564fdc1d62efd1de",
|
||||
"android_client_info": {
|
||||
"package_name": "eu.kanade.tachiyomi.debug"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
@ -6,7 +6,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(libs.android.shortcut.gradle)
|
||||
classpath(libs.google.services.gradle)
|
||||
classpath(libs.aboutLibraries.gradle)
|
||||
classpath(libs.sqldelight.gradle)
|
||||
classpath(libs.moko.gradle)
|
||||
|
@ -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 {
|
||||
|
@ -0,0 +1,21 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
/**
|
||||
* To use [okhttp3.brotli.BrotliInterceptor] as a network interceptor,
|
||||
* add [IgnoreGzipInterceptor] right before it.
|
||||
*
|
||||
* This nullifies the transparent gzip of [okhttp3.internal.http.BridgeInterceptor]
|
||||
* so gzip and Brotli are explicitly handled by the [okhttp3.brotli.BrotliInterceptor].
|
||||
*/
|
||||
class IgnoreGzipInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
var request = chain.request()
|
||||
if (request.header("Accept-Encoding") == "gzip") {
|
||||
request = request.newBuilder().removeHeader("Accept-Encoding").build()
|
||||
}
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.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>> {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
compiler = "1.5.7"
|
||||
compiler = "1.5.8"
|
||||
compose-bom = "2023.12.00-alpha04"
|
||||
accompanist = "0.33.2-alpha"
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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"]
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 & 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 -->
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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
Reference in New Issue
Block a user