Compare commits

..

No commits in common. "master" and "minsdk26" have entirely different histories.

659 changed files with 13735 additions and 19058 deletions

View File

@ -1,5 +1,4 @@
[*.{kt,kts}] [*.{kt,kts}]
max_line_length = 120
indent_size=4 indent_size=4
insert_final_newline=true insert_final_newline=true
ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma=true

View File

@ -3,10 +3,10 @@
I acknowledge that: I acknowledge that:
- I have updated: - I have updated:
- To the latest version of the app (stable is v0.15.3) - To the latest version of the app (stable is v0.14.7)
- All extensions - All extensions
- I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/) - I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/)
- If this is an issue with an official extension, that I should be opening an issue in https://github.com/tachiyomiorg/extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue - I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
- I will fill out the title and the information in this template - I will fill out the title and the information in this template

View File

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

View File

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

View File

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

View File

@ -3,8 +3,7 @@ on:
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.md' - '**.md'
- 'i18n/src/commonMain/resources/**/strings.xml' - 'i18n/src/main/res/**/strings.xml'
- 'i18n/src/commonMain/resources/**/plurals.xml'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }} group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@ -29,7 +28,7 @@ jobs:
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v3
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v3
with: with:
java-version: 17 java-version: 17
distribution: adopt distribution: adopt

View File

@ -22,12 +22,8 @@ jobs:
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
- name: Setup Android SDK
run: |
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v3
with: with:
java-version: 17 java-version: 17
distribution: adopt distribution: adopt

View File

@ -12,7 +12,7 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v5 - uses: dessant/lock-threads@v4
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: '2' issue-inactive-days: '2'

View File

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

View File

@ -30,7 +30,7 @@ To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
## Getting help ## Getting help
No support is currently provided. - Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
# Translations # Translations

View File

@ -1,6 +1,7 @@
| Build | Stable | Weekly Preview | Contribute | | Build | Stable | Weekly Preview | Contribute | Support Server |
|-------|--------|----------------|------------| |-------|----------|---------|------------|---------|
| [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | | [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi # ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
Tachiyomi is a free and open source manga reader for Android 6.0 and above. Tachiyomi is a free and open source manga reader for Android 6.0 and above.
@ -28,7 +29,8 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Issues</summary> <details><summary>Issues</summary>
**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).** 1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://tachiyomi.org/changelogs/) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
</details> </details>
@ -53,7 +55,7 @@ DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does" * Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed) * Include screenshot (if needed)
Source requests are not accepted. Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
</details> </details>
<details><summary>Contributing</summary> <details><summary>Contributing</summary>
@ -69,6 +71,7 @@ See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
## FAQ ## FAQ
[See our website.](https://tachiyomi.org/) [See our website.](https://tachiyomi.org/)
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
## License ## License

View File

@ -8,6 +8,10 @@ plugins {
id("com.github.zellius.shortcut-helper") id("com.github.zellius.shortcut-helper")
} }
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
}
shortcutHelper.setFilePath("./shortcuts.xml") shortcutHelper.setFilePath("./shortcuts.xml")
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@ -18,8 +22,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi" applicationId = "eu.kanade.tachiyomi"
versionCode = 119 versionCode = 108
versionName = "0.15.3" versionName = "0.14.7"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -27,6 +31,9 @@ android {
buildConfigField("boolean", "INCLUDE_UPDATER", "false") buildConfigField("boolean", "INCLUDE_UPDATER", "false")
buildConfigField("boolean", "PREVIEW", "false") buildConfigField("boolean", "PREVIEW", "false")
// Please disable ACRA or use your own instance in forked versions of the project
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
ndk { ndk {
abiFilters += SUPPORTED_ABIS abiFilters += SUPPORTED_ABIS
} }
@ -90,7 +97,7 @@ android {
} }
create("dev") { create("dev") {
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales // Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
// resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi")) resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
dimension = "default" dimension = "default"
} }
} }
@ -116,7 +123,6 @@ android {
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
compose = true compose = true
buildConfig = true
// Disable some unused things // Disable some unused things
aidl = false aidl = false
@ -158,8 +164,9 @@ dependencies {
implementation(compose.ui.tooling.preview) implementation(compose.ui.tooling.preview)
implementation(compose.ui.util) implementation(compose.ui.util)
implementation(compose.accompanist.webview) implementation(compose.accompanist.webview)
implementation(compose.accompanist.permissions)
implementation(compose.accompanist.themeadapter)
implementation(compose.accompanist.systemuicontroller) implementation(compose.accompanist.systemuicontroller)
lintChecks(compose.lintchecks)
implementation(androidx.paging.runtime) implementation(androidx.paging.runtime)
implementation(androidx.paging.compose) implementation(androidx.paging.compose)
@ -167,7 +174,6 @@ dependencies {
implementation(libs.bundles.sqlite) implementation(libs.bundles.sqlite)
implementation(kotlinx.reflect) implementation(kotlinx.reflect)
implementation(kotlinx.immutables)
implementation(platform(kotlinx.coroutines.bom)) implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines) implementation(kotlinx.bundles.coroutines)
@ -190,6 +196,7 @@ dependencies {
// RxJava // RxJava
implementation(libs.rxjava) implementation(libs.rxjava)
implementation(libs.flowreactivenetwork)
// Networking // Networking
implementation(libs.bundles.okhttp) implementation(libs.bundles.okhttp)
@ -238,6 +245,10 @@ dependencies {
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)
// Crash reports/analytics
implementation(libs.acra.http)
"standardImplementation"(libs.firebase.analytics)
// Shizuku // Shizuku
implementation(libs.bundles.shizuku) implementation(libs.bundles.shizuku)

View File

@ -45,7 +45,7 @@
##---------------Begin: proguard configuration for kotlinx.serialization ---------- ##---------------Begin: proguard configuration for kotlinx.serialization ----------
-keepattributes *Annotation*, InnerClasses -keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.** # core serialization annotations -dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer # kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** { -keepclassmembers class kotlinx.serialization.json.** {
@ -71,3 +71,7 @@
# XmlUtil # XmlUtil
-keep public enum nl.adaptivity.xmlutil.EventType { *; } -keep public enum nl.adaptivity.xmlutil.EventType { *; }
# Firebase
-keep class com.google.firebase.installations.** { *; }
-keep interface com.google.firebase.installations.** { *; }

View File

@ -8,9 +8,7 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Storage --> <!-- Storage -->
<uses-permission <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!-- For background jobs --> <!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@ -22,16 +20,14 @@
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<!-- To view extension packages in API 30+ --> <!-- To view extension packages in API 30+ -->
<uses-permission <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission <uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
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" />
<application <application
android:name=".App" android:name=".App"
@ -43,7 +39,6 @@
android:largeHeap="true" android:largeHeap="true"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:preserveLegacyExternalStorage="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
@ -51,53 +46,13 @@
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/Theme.Tachiyomi.SplashScreen"> android:theme="@style/Theme.Tachiyomi.SplashScreen"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- Deep link to add repos -->
<intent-filter android:label="@string/action_add_repo">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tachiyomi" />
<data android:host="add-repo" />
</intent-filter>
<!-- Open backup files -->
<intent-filter android:label="@string/pref_restore_backup">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:host="*" />
<data android:mimeType="*/*" />
<!--
Work around Android's ugly primitive PatternMatcher
implementation that can't cope with finding a . early in
the path unless it's explicitly matched.
See https://stackoverflow.com/a/31028507
-->
<data android:pathPattern=".*\\.tachibk" />
<data android:pathPattern=".*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
</intent-filter>
<!--suppress AndroidDomInspection --> <!--suppress AndroidDomInspection -->
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
@ -105,16 +60,16 @@
</activity> </activity>
<activity <activity
android:process=":error_handler"
android:name=".crash.CrashActivity" android:name=".crash.CrashActivity"
android:exported="false" android:exported="false" />
android:process=":error_handler" />
<activity <activity
android:name=".ui.deeplink.DeepLinkActivity" android:name=".ui.deeplink.DeepLinkActivity"
android:exported="true"
android:label="@string/action_search"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_search"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" /> <action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -138,21 +93,20 @@
<activity <activity
android:name=".ui.reader.ReaderActivity" android:name=".ui.reader.ReaderActivity"
android:exported="false" android:launchMode="singleTask"
android:launchMode="singleTask"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" /> <action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter> </intent-filter>
<meta-data <meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions"/> android:resource="@xml/s_pen_actions"/>
</activity> </activity>
<activity <activity
android:name=".ui.security.UnlockActivity" android:name=".ui.security.UnlockActivity"
android:exported="false" android:theme="@style/Theme.Tachiyomi"
android:theme="@style/Theme.Tachiyomi" /> android:exported="false" />
<activity <activity
android:name=".ui.webview.WebViewActivity" android:name=".ui.webview.WebViewActivity"
@ -161,25 +115,25 @@
<activity <activity
android:name=".extension.util.ExtensionInstallActivity" android:name=".extension.util.ExtensionInstallActivity"
android:exported="false" android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:exported="false" />
<activity <activity
android:name=".ui.setting.track.TrackLoginActivity" android:name=".ui.setting.track.TrackLoginActivity"
android:exported="true" android:label="@string/track_activity_name"
android:label="@string/track_activity_name"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tachiyomi" />
<data android:host="anilist-auth"/> <data android:host="anilist-auth"/>
<data android:host="bangumi-auth"/> <data android:host="bangumi-auth"/>
<data android:host="myanimelist-auth"/> <data android:host="myanimelist-auth"/>
<data android:host="shikimori-auth"/> <data android:host="shikimori-auth"/>
<data android:scheme="tachiyomi"/>
</intent-filter> </intent-filter>
</activity> </activity>
@ -187,10 +141,13 @@
android:name=".data.notification.NotificationReceiver" android:name=".data.notification.NotificationReceiver"
android:exported="false" /> android:exported="false" />
<service
android:name=".data.download.DownloadService"
android:exported="false" />
<service <service
android:name=".extension.util.ExtensionInstallService" android:name=".extension.util.ExtensionInstallService"
android:exported="false" android:exported="false" />
android:foregroundServiceType="shortService" />
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
@ -201,11 +158,6 @@
android:value="true" /> android:value="true" />
</service> </service>
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@ -219,9 +171,9 @@
<provider <provider
android:name="rikka.shizuku.ShizukuProvider" android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku" android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true" android:enabled="true"
android:exported="true" android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" /> android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data <meta-data
@ -231,6 +183,11 @@
android:name="android.webkit.WebView.MetricsOptOut" android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" /> android:value="true" />
<!-- Disable advertising ID collection for Firebase -->
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="false" />
</application> </application>
</manifest> </manifest>

View File

@ -1,18 +1,11 @@
package eu.kanade.domain package eu.kanade.domain
import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.chapter.interactor.SetReadStatus import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.download.interactor.DeleteDownload import eu.kanade.domain.download.interactor.DeleteDownload
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
import eu.kanade.domain.extension.interactor.GetExtensionLanguages import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.extension.interactor.GetExtensionRepos
import eu.kanade.domain.extension.interactor.GetExtensionSources import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.extension.interactor.GetExtensionsByType import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
@ -119,8 +112,6 @@ class DomainModule : InjektModule {
addFactory { NetworkToLocalManga(get()) } addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get(), get()) } addFactory { UpdateManga(get(), get()) }
addFactory { SetMangaCategories(get()) } addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) } addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) } addFactory { GetApplicationRelease(get(), get()) }
@ -142,8 +133,7 @@ class DomainModule : InjektModule {
addFactory { UpdateChapter(get()) } addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() } addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) } addFactory { GetHistory(get()) }
@ -171,10 +161,5 @@ class DomainModule : InjektModule {
addFactory { ToggleLanguage(get()) } addFactory { ToggleLanguage(get()) }
addFactory { ToggleSource(get()) } addFactory { ToggleSource(get()) }
addFactory { ToggleSourcePin(get()) } addFactory { ToggleSourcePin(get()) }
addFactory { TrustExtension(get()) }
addFactory { CreateExtensionRepo(get()) }
addFactory { DeleteExtensionRepo(get()) }
addFactory { GetExtensionRepos(get()) }
} }
} }

View File

@ -1,10 +1,12 @@
package eu.kanade.domain.base package eu.kanade.domain.base
import android.content.Context import android.content.Context
import dev.icerock.moko.resources.StringResource import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.i18n.MR
class BasePreferences( class BasePreferences(
val context: Context, val context: Context,
@ -20,12 +22,12 @@ class BasePreferences(
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore) fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false) fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) { enum class ExtensionInstaller(@StringRes val titleResId: Int) {
LEGACY(MR.strings.ext_installer_legacy, true), LEGACY(R.string.ext_installer_legacy),
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true), PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
SHIZUKU(MR.strings.ext_installer_shizuku, false), SHIZUKU(R.string.ext_installer_shizuku),
PRIVATE(MR.strings.ext_installer_private, false), PRIVATE(R.string.ext_installer_private),
} }
} }

View File

@ -1,24 +0,0 @@
package eu.kanade.domain.chapter.interactor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.domain.chapter.repository.ChapterRepository
class GetAvailableScanlators(
private val repository: ChapterRepository,
) {
private fun List<String>.cleanupAvailableScanlators(): Set<String> {
return mapNotNull { it.ifBlank { null } }.toSet()
}
suspend fun await(mangaId: Long): Set<String> {
return repository.getScanlatorsByMangaId(mangaId)
.cleanupAvailableScanlators()
}
fun subscribe(mangaId: Long): Flow<Set<String>> {
return repository.getScanlatorsByMangaIdAsFlow(mangaId)
.map { it.cleanupAvailableScanlators() }
}
}

View File

@ -2,7 +2,6 @@ package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.copyFromSChapter import eu.kanade.domain.chapter.model.copyFromSChapter
import eu.kanade.domain.chapter.model.toSChapter import eu.kanade.domain.chapter.model.toSChapter
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
@ -23,6 +22,7 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import java.lang.Long.max import java.lang.Long.max
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.Date
import java.util.TreeSet import java.util.TreeSet
class SyncChaptersWithSource( class SyncChaptersWithSource(
@ -33,7 +33,6 @@ class SyncChaptersWithSource(
private val updateManga: UpdateManga, private val updateManga: UpdateManga,
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators,
) { ) {
/** /**
@ -56,7 +55,6 @@ class SyncChaptersWithSource(
} }
val now = ZonedDateTime.now() val now = ZonedDateTime.now()
val nowMillis = now.toInstant().toEpochMilli()
val sourceChapters = rawSourceChapters val sourceChapters = rawSourceChapters
.distinctBy { it.url } .distinctBy { it.url }
@ -67,27 +65,36 @@ class SyncChaptersWithSource(
.copy(mangaId = manga.id, sourceOrder = i.toLong()) .copy(mangaId = manga.id, sourceOrder = i.toLong())
} }
// Chapters from db.
val dbChapters = getChaptersByMangaId.await(manga.id) val dbChapters = getChaptersByMangaId.await(manga.id)
val newChapters = mutableListOf<Chapter>() // Chapters from the source not in db.
val updatedChapters = mutableListOf<Chapter>() val toAdd = mutableListOf<Chapter>()
val removedChapters = dbChapters.filterNot { dbChapter ->
// Chapters whose metadata have changed.
val toChange = mutableListOf<Chapter>()
// Chapters from the db not in source.
val toDelete = dbChapters.filterNot { dbChapter ->
sourceChapters.any { sourceChapter -> sourceChapters.any { sourceChapter ->
dbChapter.url == sourceChapter.url dbChapter.url == sourceChapter.url
} }
} }
val rightNow = Date().time
// Used to not set upload date of older chapters // Used to not set upload date of older chapters
// to a higher value than newer chapters // to a higher value than newer chapters
var maxSeenUploadDate = 0L var maxSeenUploadDate = 0L
val sManga = manga.toSManga()
for (sourceChapter in sourceChapters) { for (sourceChapter in sourceChapters) {
var chapter = sourceChapter var chapter = sourceChapter
// Update metadata from source if necessary. // Update metadata from source if necessary.
if (source is HttpSource) { if (source is HttpSource) {
val sChapter = chapter.toSChapter() val sChapter = chapter.toSChapter()
source.prepareNewChapter(sChapter, manga.toSManga()) source.prepareNewChapter(sChapter, sManga)
chapter = chapter.copyFromSChapter(sChapter) chapter = chapter.copyFromSChapter(sChapter)
} }
@ -99,19 +106,17 @@ class SyncChaptersWithSource(
if (dbChapter == null) { if (dbChapter == null) {
val toAddChapter = if (chapter.dateUpload == 0L) { val toAddChapter = if (chapter.dateUpload == 0L) {
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
chapter.copy(dateUpload = altDateUpload) chapter.copy(dateUpload = altDateUpload)
} else { } else {
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload) maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
chapter chapter
} }
newChapters.add(toAddChapter) toAdd.add(toAddChapter)
} else { } else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) { if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) && val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
downloadManager.isChapterDownloaded( downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
dbChapter.name, dbChapter.scanlator, manga.title, manga.source,
)
if (shouldRenameChapter) { if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter) downloadManager.renameChapter(source, manga, dbChapter, chapter)
@ -125,13 +130,13 @@ class SyncChaptersWithSource(
if (chapter.dateUpload != 0L) { if (chapter.dateUpload != 0L) {
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload) toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
} }
updatedChapters.add(toChangeChapter) toChange.add(toChangeChapter)
} }
} }
} }
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions. // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) { if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) { if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval( updateManga.awaitUpdateFetchInterval(
manga, manga,
@ -148,20 +153,20 @@ class SyncChaptersWithSource(
val deletedReadChapterNumbers = TreeSet<Double>() val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>() val deletedBookmarkedChapterNumbers = TreeSet<Double>()
removedChapters.forEach { chapter -> toDelete.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber) if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
deletedChapterNumbers.add(chapter.chapterNumber) deletedChapterNumbers.add(chapter.chapterNumber)
} }
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to it.dateFetch } .associate { it.chapterNumber to it.dateFetch }
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the chapters from most to less recent, which is common. // Sources MUST return the chapters from most to less recent, which is common.
var itemCount = newChapters.size var itemCount = toAdd.size
var updatedToAdd = newChapters.map { toAddItem -> var updatedToAdd = toAdd.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
@ -180,8 +185,8 @@ class SyncChaptersWithSource(
chapter chapter
} }
if (removedChapters.isNotEmpty()) { if (toDelete.isNotEmpty()) {
val toDeleteIds = removedChapters.map { it.id } val toDeleteIds = toDelete.map { it.id }
chapterRepository.removeChaptersWithIds(toDeleteIds) chapterRepository.removeChaptersWithIds(toDeleteIds)
} }
@ -189,8 +194,8 @@ class SyncChaptersWithSource(
updatedToAdd = chapterRepository.addAll(updatedToAdd) updatedToAdd = chapterRepository.addAll(updatedToAdd)
} }
if (updatedChapters.isNotEmpty()) { if (toChange.isNotEmpty()) {
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() } val chapterUpdates = toChange.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates) updateChapter.awaitAll(chapterUpdates)
} }
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow) updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
@ -201,10 +206,6 @@ class SyncChaptersWithSource(
val reAddedUrls = reAdded.map { it.url }.toHashSet() val reAddedUrls = reAdded.map { it.url }.toHashSet()
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet() return updatedToAdd.filterNot { it.url in reAddedUrls }
return updatedToAdd.filterNot {
it.url in reAddedUrls || it.scanlator in excludedScanlators
}
} }
} }

View File

@ -22,7 +22,7 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
url = sChapter.url, url = sChapter.url,
dateUpload = sChapter.date_upload, dateUpload = sChapter.date_upload,
chapterNumber = sChapter.chapter_number.toDouble(), chapterNumber = sChapter.chapter_number.toDouble(),
scanlator = sChapter.scanlator?.ifBlank { null }?.trim(), scanlator = sChapter.scanlator?.ifBlank { null },
) )
} }

View File

@ -23,12 +23,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
.filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } } .filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } }
.filter { chapter -> .filter { chapter ->
applyFilter(downloadedFilter) { applyFilter(downloadedFilter) {
val downloaded = downloadManager.isChapterDownloaded( val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
chapter.name,
chapter.scanlator,
manga.title,
manga.source,
)
downloaded || isLocalManga downloaded || isLocalManga
} }
} }

View File

@ -1,25 +0,0 @@
package eu.kanade.domain.extension.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.preference.plusAssign
class CreateExtensionRepo(private val preferences: SourcePreferences) {
fun await(name: String): Result {
// Do not allow invalid formats
if (!name.matches(repoRegex)) {
return Result.InvalidUrl
}
preferences.extensionRepos() += name.removeSuffix("/index.min.json")
return Result.Success
}
sealed interface Result {
data object InvalidUrl : Result
data object Success : Result
}
}
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()

View File

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

View File

@ -1,11 +0,0 @@
package eu.kanade.domain.extension.interactor
import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow
class GetExtensionRepos(private val preferences: SourcePreferences) {
fun subscribe(): Flow<Set<String>> {
return preferences.extensionRepos().changes()
}
}

View File

@ -1,31 +0,0 @@
package eu.kanade.domain.extension.interactor
import android.content.pm.PackageInfo
import androidx.core.content.pm.PackageInfoCompat
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.preference.getAndSet
class TrustExtension(
private val preferences: SourcePreferences,
) {
fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean {
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash"
return key in preferences.trustedExtensions().get()
}
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
preferences.trustedExtensions().getAndSet { exts ->
// Remove previously trusted versions
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
removed.also {
it += "$pkgName:$versionCode:$signatureHash"
}
}
}
fun revokeAll() {
preferences.trustedExtensions().delete()
}
}

View File

@ -1,24 +0,0 @@
package eu.kanade.domain.manga.interactor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.DatabaseHandler
class GetExcludedScanlators(
private val handler: DatabaseHandler,
) {
suspend fun await(mangaId: Long): Set<String> {
return handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
.toSet()
}
fun subscribe(mangaId: Long): Flow<Set<String>> {
return handler.subscribeToList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
.map { it.toSet() }
}
}

View File

@ -1,22 +0,0 @@
package eu.kanade.domain.manga.interactor
import tachiyomi.data.DatabaseHandler
class SetExcludedScanlators(
private val handler: DatabaseHandler,
) {
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
handler.await(inTransaction = true) {
val currentExcluded = handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}.toSet()
val toAdd = excludedScanlators.minus(currentExcluded)
for (scanlator in toAdd) {
excluded_scanlatorsQueries.insert(mangaId, scanlator)
}
val toRemove = currentExcluded.minus(excludedScanlators)
excluded_scanlatorsQueries.remove(mangaId, toRemove)
}
}
}

View File

@ -1,7 +1,7 @@
package eu.kanade.domain.manga.interactor package eu.kanade.domain.manga.interactor
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.manga.repository.MangaRepository
@ -14,7 +14,7 @@ class SetMangaViewerFlags(
mangaRepository.update( mangaRepository.update(
MangaUpdate( MangaUpdate(
id = id, id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()), viewerFlags = manga.viewerFlags.setFlag(flag, ReadingModeType.MASK.toLong()),
), ),
) )
} }
@ -24,7 +24,7 @@ class SetMangaViewerFlags(
mangaRepository.update( mangaRepository.update(
MangaUpdate( MangaUpdate(
id = id, id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()), viewerFlags = manga.viewerFlags.setFlag(flag, OrientationType.MASK.toLong()),
), ),
) )
} }

View File

@ -10,8 +10,8 @@ import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.Date
class UpdateManga( class UpdateManga(
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
@ -46,14 +46,14 @@ class UpdateManga(
// Never refresh covers if the url is empty to avoid "losing" existing covers // Never refresh covers if the url is empty to avoid "losing" existing covers
remoteManga.thumbnail_url.isNullOrEmpty() -> null remoteManga.thumbnail_url.isNullOrEmpty() -> null
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null !manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
localManga.isLocal() -> Instant.now().toEpochMilli() localManga.isLocal() -> Date().time
localManga.hasCustomCover(coverCache) -> { localManga.hasCustomCover(coverCache) -> {
coverCache.deleteFromCache(localManga, false) coverCache.deleteFromCache(localManga, false)
null null
} }
else -> { else -> {
coverCache.deleteFromCache(localManga, false) coverCache.deleteFromCache(localManga, false)
Instant.now().toEpochMilli() Date().time
} }
} }
@ -81,22 +81,22 @@ class UpdateManga(
dateTime: ZonedDateTime = ZonedDateTime.now(), dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime), window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
): Boolean { ): Boolean {
return mangaRepository.update( return fetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
fetchInterval.toMangaUpdate(manga, dateTime, window), ?.let { mangaRepository.update(it) }
) ?: false
} }
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli())) return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time))
} }
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean { suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli())) return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Date().time))
} }
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean { suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
val dateAdded = when (favorite) { val dateAdded = when (favorite) {
true -> Instant.now().toEpochMilli() true -> Date().time
false -> 0 false -> 0
} }
return mangaRepository.update( return mangaRepository.update(

View File

@ -3,8 +3,8 @@ package eu.kanade.domain.manga.model
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
@ -14,11 +14,11 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
// TODO: move these into the domain model // TODO: move these into the domain model
val Manga.readingMode: Long val Manga.readingModeType: Long
get() = viewerFlags and ReadingMode.MASK.toLong() get() = viewerFlags and ReadingModeType.MASK.toLong()
val Manga.readerOrientation: Long val Manga.orientationType: Long
get() = viewerFlags and ReaderOrientation.MASK.toLong() get() = viewerFlags and OrientationType.MASK.toLong()
val Manga.downloadedFilter: TriState val Manga.downloadedFilter: TriState
get() { get() {

View File

@ -11,12 +11,7 @@ class SourcePreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun sourceDisplayMode() = preferenceStore.getObject( fun sourceDisplayMode() = preferenceStore.getObject("pref_display_mode_catalogue", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
"pref_display_mode_catalogue",
LibraryDisplayMode.default,
LibraryDisplayMode.Serializer::serialize,
LibraryDisplayMode.Serializer::deserialize,
)
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages()) fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
@ -33,19 +28,11 @@ class SourcePreferences(
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL) fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
fun migrationSortingDirection() = preferenceStore.getEnum( fun migrationSortingDirection() = preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING)
"pref_migration_direction",
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 extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
fun trustedExtensions() = preferenceStore.getStringSet( fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
Preference.appStateKey("trusted_extensions"),
emptySet(), fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
)
} }

View File

@ -61,10 +61,7 @@ class AddTracks(
?.readAt ?.readAt
firstReadChapterDate?.let { firstReadChapterDate?.let {
val startDate = firstReadChapterDate.time.convertEpochMillisZone( val startDate = firstReadChapterDate.time.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC)
ZoneOffset.systemDefault(),
ZoneOffset.UTC,
)
track = track.copy( track = track.copy(
startDate = startDate, startDate = startDate,
) )

View File

@ -25,7 +25,7 @@ class RefreshTracks(
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> { suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
return supervisorScope { return supervisorScope {
return@supervisorScope getTracks.await(mangaId) return@supervisorScope getTracks.await(mangaId)
.map { it to trackerManager.get(it.trackerId) } .map { it to trackerManager.get(it.syncId) }
.filter { (_, service) -> service?.isLoggedIn == true } .filter { (_, service) -> service?.isLoggedIn == true }
.map { (track, service) -> .map { (track, service) ->
async { async {

View File

@ -27,7 +27,7 @@ class TrackChapter(
if (tracks.isEmpty()) return@withNonCancellableContext if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track -> tracks.mapNotNull { track ->
val service = trackerManager.get(track.trackerId) val service = trackerManager.get(track.syncId)
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) { if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
return@mapNotNull null return@mapNotNull null
} }

View File

@ -13,10 +13,10 @@ fun Track.copyPersonalFrom(other: Track): Track {
) )
} }
fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also { fun Track.toDbTrack(): DbTrack = DbTrack.create(syncId).also {
it.id = id it.id = id
it.manga_id = mangaId it.manga_id = mangaId
it.remote_id = remoteId it.media_id = remoteId
it.library_id = libraryId it.library_id = libraryId
it.title = title it.title = title
it.last_chapter_read = lastChapterRead.toFloat() it.last_chapter_read = lastChapterRead.toFloat()
@ -33,16 +33,14 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
return Track( return Track(
id = trackId, id = trackId,
mangaId = manga_id, mangaId = manga_id,
trackerId = tracker_id.toLong(), syncId = sync_id.toLong(),
remoteId = remote_id, remoteId = media_id,
libraryId = library_id, libraryId = library_id,
title = title, title = title,
lastChapterRead = last_chapter_read.toDouble(), lastChapterRead = last_chapter_read.toDouble(),
totalChapters = total_chapters.toLong(), totalChapters = total_chapters.toLong(),
status = status.toLong(), status = status.toLong(),
// Jank workaround due to precision issues while converting score = score.toDouble(),
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
score = score.toString().toDouble(),
remoteUrl = tracking_url, remoteUrl = tracking_url,
startDate = started_reading_date, startDate = started_reading_date,
finishDate = finished_reading_date, finishDate = finished_reading_date,

View File

@ -17,7 +17,8 @@ import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) : class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
@ -42,9 +43,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
track?.copy(lastChapterRead = it.lastChapterRead.toDouble()) track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
} }
.forEach { track -> .forEach { track ->
logcat(LogPriority.DEBUG) { logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" }
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
}
trackChapter.await(context, track.mangaId, track.lastChapterRead) trackChapter.await(context, track.mangaId, track.lastChapterRead)
} }
} }
@ -62,7 +61,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>() val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>()
.setConstraints(constraints) .setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration())
.addTag(TAG) .addTag(TAG)
.build() .build()

View File

@ -9,24 +9,26 @@ class TrackPreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun trackUsername(tracker: Tracker) = preferenceStore.getString( fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
Preference.privateKey("pref_mangasync_username_${tracker.id}"),
"",
)
fun trackPassword(tracker: Tracker) = preferenceStore.getString( fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
"",
)
fun setCredentials(tracker: Tracker, username: String, password: String) { fun setCredentials(sync: Tracker, username: String, password: String) {
trackUsername(tracker).set(username) trackUsername(sync).set(username)
trackPassword(tracker).set(password) trackPassword(sync).set(password)
} }
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "") fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10) fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true) fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
companion object {
fun trackUsername(syncId: Long) = Preference.privateKey("pref_mangasync_username_$syncId")
private fun trackPassword(syncId: Long) = Preference.privateKey("pref_mangasync_password_$syncId")
private fun trackToken(syncId: Long) = Preference.privateKey("track_token_$syncId")
}
} }

View File

@ -1,6 +1,5 @@
package eu.kanade.domain.ui package eu.kanade.domain.ui
import android.os.Build
import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.TabletUiMode import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode import eu.kanade.domain.ui.model.ThemeMode
@ -16,10 +15,7 @@ class UiPreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun themeMode() = preferenceStore.getEnum( fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM)
"pref_theme_mode_key",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ThemeMode.SYSTEM } else { ThemeMode.LIGHT },
)
fun appTheme() = preferenceStore.getEnum( fun appTheme() = preferenceStore.getEnum(
"pref_app_theme", "pref_app_theme",

View File

@ -1,25 +1,19 @@
package eu.kanade.domain.ui.model package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import tachiyomi.i18n.MR
enum class AppTheme(val titleRes: StringResource?) { enum class AppTheme(val titleResId: Int?) {
DEFAULT(MR.strings.label_default), DEFAULT(R.string.label_default),
MONET(MR.strings.theme_monet), MONET(R.string.theme_monet),
GREEN_APPLE(MR.strings.theme_greenapple), GREEN_APPLE(R.string.theme_greenapple),
LAVENDER(MR.strings.theme_lavender), LAVENDER(R.string.theme_lavender),
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk), MIDNIGHT_DUSK(R.string.theme_midnightdusk),
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
// TODO: re-enable for preview TAKO(R.string.theme_tako),
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }), TEALTURQUOISE(R.string.theme_tealturquoise),
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri), TIDAL_WAVE(R.string.theme_tidalwave),
TAKO(MR.strings.theme_tako), YINYANG(R.string.theme_yinyang),
TEALTURQUOISE(MR.strings.theme_tealturquoise), YOTSUBA(R.string.theme_yotsuba),
TIDAL_WAVE(MR.strings.theme_tidalwave),
YINYANG(MR.strings.theme_yinyang),
YOTSUBA(MR.strings.theme_yotsuba),
// Deprecated // Deprecated
DARK_BLUE(null), DARK_BLUE(null),

View File

@ -1,11 +1,10 @@
package eu.kanade.domain.ui.model package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.R
import tachiyomi.i18n.MR
enum class TabletUiMode(val titleRes: StringResource) { enum class TabletUiMode(val titleResId: Int) {
AUTOMATIC(MR.strings.automatic_background), AUTOMATIC(R.string.automatic_background),
ALWAYS(MR.strings.lock_always), ALWAYS(R.string.lock_always),
LANDSCAPE(MR.strings.landscape), LANDSCAPE(R.string.landscape),
NEVER(MR.strings.lock_never), NEVER(R.string.lock_never),
} }

View File

@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
@ -21,16 +22,13 @@ import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
import eu.kanade.presentation.browse.components.BrowseSourceList import eu.kanade.presentation.browse.components.BrowseSourceList
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.formattedMessage import eu.kanade.presentation.util.formattedMessage
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import tachiyomi.core.i18n.stringResource
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.EmptyScreenAction import tachiyomi.presentation.core.screens.EmptyScreenAction
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
@ -63,7 +61,7 @@ fun BrowseSourceContent(
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) { if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = getErrorMessage(errorState), message = getErrorMessage(errorState),
actionLabel = context.stringResource(MR.strings.action_retry), actionLabel = context.getString(R.string.action_retry),
duration = SnackbarDuration.Indefinite, duration = SnackbarDuration.Indefinite,
) )
when (result) { when (result) {
@ -78,28 +76,28 @@ fun BrowseSourceContent(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
message = getErrorMessage(errorState), message = getErrorMessage(errorState),
actions = if (source is LocalSource) { actions = if (source is LocalSource) {
persistentListOf( listOf(
EmptyScreenAction( EmptyScreenAction(
stringRes = MR.strings.local_source_help_guide, stringResId = R.string.local_source_help_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick, onClick = onLocalSourceHelpClick,
), ),
) )
} else { } else {
persistentListOf( listOf(
EmptyScreenAction( EmptyScreenAction(
stringRes = MR.strings.action_retry, stringResId = R.string.action_retry,
icon = Icons.Outlined.Refresh, icon = Icons.Outlined.Refresh,
onClick = mangaList::refresh, onClick = mangaList::refresh,
), ),
EmptyScreenAction( EmptyScreenAction(
stringRes = MR.strings.action_open_in_web_view, stringResId = R.string.action_open_in_web_view,
icon = Icons.Outlined.Public, icon = Icons.Outlined.Public,
onClick = onWebViewClick, onClick = onWebViewClick,
), ),
EmptyScreenAction( EmptyScreenAction(
stringRes = MR.strings.label_help, stringResId = R.string.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = onHelpClick, onClick = onHelpClick,
), ),
) )
@ -147,7 +145,7 @@ fun BrowseSourceContent(
} }
@Composable @Composable
internal fun MissingSourceScreen( fun MissingSourceScreen(
source: StubSource, source: StubSource,
navigateUp: () -> Unit, navigateUp: () -> Unit,
) { ) {
@ -161,7 +159,7 @@ internal fun MissingSourceScreen(
}, },
) { paddingValues -> ) { paddingValues ->
EmptyScreen( EmptyScreen(
message = stringResource(MR.strings.source_not_installed, source.toString()), message = stringResource(R.string.source_not_installed, source.toString()),
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
) )
} }

View File

@ -16,7 +16,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -37,7 +38,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -49,17 +50,14 @@ import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
@Composable @Composable
@ -67,61 +65,55 @@ fun ExtensionDetailsScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
state: ExtensionDetailsScreenModel.State, state: ExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit,
onClickEnableAll: () -> Unit, onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit, onClickDisableAll: () -> Unit,
onClickClearCookies: () -> Unit, onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
) { ) {
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
regex.find(state.extension?.repoUrl.orEmpty())
?.let {
val (user, repo) = it.destructured
"https://github.com/$user/$repo"
}
?: state.extension?.repoUrl
}
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
title = stringResource(MR.strings.label_extension_info), title = stringResource(R.string.label_extension_info),
navigateUp = navigateUp, navigateUp = navigateUp,
actions = { actions = {
AppBarActions( AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder() actions = buildList {
.apply { if (state.extension?.isUnofficial == false) {
if (url != null) {
add( add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_open_repo), title = stringResource(R.string.whats_new),
icon = Icons.AutoMirrored.Outlined.Launch, icon = Icons.Outlined.History,
onClick = { onClick = onClickWhatsNew,
uriHandler.openUri(url) ),
}, )
add(
AppBar.Action(
title = stringResource(R.string.action_faq_and_guides),
icon = Icons.Outlined.HelpOutline,
onClick = onClickReadme,
), ),
) )
} }
addAll( addAll(
listOf( listOf(
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_enable_all), title = stringResource(R.string.action_enable_all),
onClick = onClickEnableAll, onClick = onClickEnableAll,
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_disable_all), title = stringResource(R.string.action_disable_all),
onClick = onClickDisableAll, onClick = onClickDisableAll,
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.pref_clear_cookies), title = stringResource(R.string.pref_clear_cookies),
onClick = onClickClearCookies, onClick = onClickClearCookies,
), ),
), ),
) )
} },
.build(),
) )
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
@ -130,7 +122,7 @@ fun ExtensionDetailsScreen(
) { paddingValues -> ) { paddingValues ->
if (state.extension == null) { if (state.extension == null) {
EmptyScreen( EmptyScreen(
MR.strings.empty_screen, textResource = R.string.empty_screen,
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
) )
return@Scaffold return@Scaffold
@ -151,7 +143,7 @@ fun ExtensionDetailsScreen(
private fun ExtensionDetails( private fun ExtensionDetails(
contentPadding: PaddingValues, contentPadding: PaddingValues,
extension: Extension.Installed, extension: Extension.Installed,
sources: ImmutableList<ExtensionSourceItem>, sources: List<ExtensionSourceItem>,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
@ -162,9 +154,14 @@ private fun ExtensionDetails(
ScrollbarLazyColumn( ScrollbarLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
if (extension.isObsolete) { when {
extension.isUnofficial ->
item { item {
WarningBanner(MR.strings.obsolete_extension_message) WarningBanner(R.string.unofficial_extension_message)
}
extension.isObsolete ->
item {
WarningBanner(R.string.obsolete_extension_message)
} }
} }
@ -261,7 +258,7 @@ private fun DetailsHeader(
InfoText( InfoText(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
primaryText = extension.versionName, primaryText = extension.versionName,
secondaryText = stringResource(MR.strings.ext_info_version), secondaryText = stringResource(R.string.ext_info_version),
) )
InfoDivider() InfoDivider()
@ -269,7 +266,7 @@ private fun DetailsHeader(
InfoText( InfoText(
modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f), modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context), primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
secondaryText = stringResource(MR.strings.ext_info_language), secondaryText = stringResource(R.string.ext_info_language),
) )
if (extension.isNsfw) { if (extension.isNsfw) {
@ -277,12 +274,12 @@ private fun DetailsHeader(
InfoText( InfoText(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
primaryText = stringResource(MR.strings.ext_nsfw_short), primaryText = stringResource(R.string.ext_nsfw_short),
primaryTextStyle = MaterialTheme.typography.bodyLarge.copy( primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
), ),
secondaryText = stringResource(MR.strings.ext_info_age_rating), secondaryText = stringResource(R.string.ext_info_age_rating),
onClick = onClickAgeRating, onClick = onClickAgeRating,
) )
} }
@ -295,13 +292,13 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small, top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium, bottom = MaterialTheme.padding.medium,
), ),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
OutlinedButton( OutlinedButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = onClickUninstall, onClick = onClickUninstall,
) { ) {
Text(stringResource(MR.strings.ext_uninstall)) Text(stringResource(R.string.ext_uninstall))
} }
if (onClickAppInfo != null) { if (onClickAppInfo != null) {
@ -310,7 +307,7 @@ private fun DetailsHeader(
onClick = onClickAppInfo, onClick = onClickAppInfo,
) { ) {
Text( Text(
text = stringResource(MR.strings.ext_app_info), text = stringResource(R.string.ext_app_info),
color = MaterialTheme.colorScheme.onPrimary, color = MaterialTheme.colorScheme.onPrimary,
) )
} }
@ -323,10 +320,10 @@ private fun DetailsHeader(
@Composable @Composable
private fun InfoText( private fun InfoText(
modifier: Modifier,
primaryText: String, primaryText: String,
secondaryText: String,
modifier: Modifier = Modifier,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge, primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
secondaryText: String,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@ -366,10 +363,10 @@ private fun InfoDivider() {
@Composable @Composable
private fun SourceSwitchPreference( private fun SourceSwitchPreference(
modifier: Modifier = Modifier,
source: ExtensionSourceItem, source: ExtensionSourceItem,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -388,7 +385,7 @@ private fun SourceSwitchPreference(
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) { IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
Icon( Icon(
imageVector = Icons.Outlined.Settings, imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.label_settings), contentDescription = stringResource(R.string.label_settings),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }
@ -411,11 +408,11 @@ private fun NsfwWarningDialog(
) { ) {
AlertDialog( AlertDialog(
text = { text = {
Text(text = stringResource(MR.strings.ext_nsfw_warning)) Text(text = stringResource(R.string.ext_nsfw_warning))
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onClickConfirm) { TextButton(onClick = onClickConfirm) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
onDismissRequest = onClickConfirm, onDismissRequest = onClickConfirm,

View File

@ -7,13 +7,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
@Composable @Composable
@ -25,7 +25,7 @@ fun ExtensionFilterScreen(
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
title = stringResource(MR.strings.label_extensions), title = stringResource(R.string.label_extensions),
navigateUp = navigateUp, navigateUp = navigateUp,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
@ -33,7 +33,7 @@ fun ExtensionFilterScreen(
) { contentPadding -> ) { contentPadding ->
if (state.isEmpty) { if (state.isEmpty) {
EmptyScreen( EmptyScreen(
stringRes = MR.strings.empty_screen, textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
return@Scaffold return@Scaffold

View File

@ -1,7 +1,7 @@
package eu.kanade.presentation.browse package eu.kanade.presentation.browse
import androidx.annotation.StringRes
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -15,11 +15,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.VerifiedUser
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -38,32 +33,23 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.browse.components.BaseBrowseItem import eu.kanade.presentation.browse.components.BaseBrowseItem
import eu.kanade.presentation.browse.components.ExtensionIcon import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen import eu.kanade.tachiyomi.R
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.material.topSmallPaddingValues import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.EmptyScreenAction
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.theme.header import tachiyomi.presentation.core.theme.header
import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.plus
@ -76,7 +62,6 @@ fun ExtensionScreen(
searchQuery: String?, searchQuery: String?,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
onOpenWebView: (Extension.Available) -> Unit,
onInstallExtension: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit, onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit,
@ -85,31 +70,22 @@ fun ExtensionScreen(
onClickUpdateAll: () -> Unit, onClickUpdateAll: () -> Unit,
onRefresh: () -> Unit, onRefresh: () -> Unit,
) { ) {
val navigator = LocalNavigator.currentOrThrow
PullRefresh( PullRefresh(
refreshing = state.isRefreshing, refreshing = state.isRefreshing,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = { !state.isLoading }, enabled = !state.isLoading,
) { ) {
when { when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> { state.isEmpty -> {
val msg = if (!searchQuery.isNullOrEmpty()) { val msg = if (!searchQuery.isNullOrEmpty()) {
MR.strings.no_results_found R.string.no_results_found
} else { } else {
MR.strings.empty_screen R.string.empty_screen
} }
EmptyScreen( EmptyScreen(
stringRes = msg, textResource = msg,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
actions = persistentListOf(
EmptyScreenAction(
stringRes = MR.strings.label_extension_repos,
icon = Icons.Outlined.Settings,
onClick = { navigator.push(ExtensionReposScreen()) },
),
),
) )
} }
else -> { else -> {
@ -118,7 +94,6 @@ fun ExtensionScreen(
contentPadding = contentPadding, contentPadding = contentPadding,
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel, onClickItemCancel = onClickItemCancel,
onOpenWebView = onOpenWebView,
onInstallExtension = onInstallExtension, onInstallExtension = onInstallExtension,
onUninstallExtension = onUninstallExtension, onUninstallExtension = onUninstallExtension,
onUpdateExtension = onUpdateExtension, onUpdateExtension = onUpdateExtension,
@ -137,7 +112,6 @@ private fun ExtensionContent(
contentPadding: PaddingValues, contentPadding: PaddingValues,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
onOpenWebView: (Extension.Available) -> Unit,
onInstallExtension: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit, onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit,
@ -145,24 +119,11 @@ private fun ExtensionContent(
onOpenExtension: (Extension.Installed) -> Unit, onOpenExtension: (Extension.Installed) -> Unit,
onClickUpdateAll: () -> Unit, onClickUpdateAll: () -> Unit,
) { ) {
val context = LocalContext.current
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) } var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues, contentPadding = contentPadding + topSmallPaddingValues,
) { ) {
if (!installGranted && state.installer?.requiresSystemPermission == true) {
item(key = "extension-permissions-warning") {
WarningBanner(
textRes = MR.strings.ext_permission_install_apps_warning,
modifier = Modifier.clickable {
context.launchRequestPackageInstallsPermission()
},
)
}
}
state.items.forEach { (header, items) -> state.items.forEach { (header, items) ->
item( item(
contentType = "header", contentType = "header",
@ -171,11 +132,11 @@ private fun ExtensionContent(
when (header) { when (header) {
is ExtensionUiModel.Header.Resource -> { is ExtensionUiModel.Header.Resource -> {
val action: @Composable RowScope.() -> Unit = val action: @Composable RowScope.() -> Unit =
if (header.textRes == MR.strings.ext_updates_pending) { if (header.textRes == R.string.ext_updates_pending) {
{ {
Button(onClick = { onClickUpdateAll() }) { Button(onClick = { onClickUpdateAll() }) {
Text( Text(
text = stringResource(MR.strings.ext_update_all), text = stringResource(R.string.ext_update_all),
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.onPrimary, color = MaterialTheme.colorScheme.onPrimary,
), ),
@ -216,13 +177,6 @@ private fun ExtensionContent(
} }
}, },
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
onClickItemSecondaryAction = {
when (it) {
is Extension.Available -> onOpenWebView(it)
is Extension.Installed -> onOpenExtension(it)
else -> {}
}
},
onClickItemCancel = onClickItemCancel, onClickItemCancel = onClickItemCancel,
onClickItemAction = { onClickItemAction = {
when (it) { when (it) {
@ -260,13 +214,12 @@ private fun ExtensionContent(
@Composable @Composable
private fun ExtensionItem( private fun ExtensionItem(
modifier: Modifier = Modifier,
item: ExtensionUiModel.Item, item: ExtensionUiModel.Item,
onClickItem: (Extension) -> Unit, onClickItem: (Extension) -> Unit,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
onClickItemAction: (Extension) -> Unit, onClickItemAction: (Extension) -> Unit,
onClickItemSecondaryAction: (Extension) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val (extension, installStep) = item val (extension, installStep) = item
BaseBrowseItem( BaseBrowseItem(
@ -309,7 +262,6 @@ private fun ExtensionItem(
installStep = installStep, installStep = installStep,
onClickItemCancel = onClickItemCancel, onClickItemCancel = onClickItemCancel,
onClickItemAction = onClickItemAction, onClickItemAction = onClickItemAction,
onClickItemSecondaryAction = onClickItemSecondaryAction,
) )
}, },
) { ) {
@ -339,7 +291,7 @@ private fun ExtensionItemContent(
// Won't look good but it's not like we can ellipsize overflowing content // Won't look good but it's not like we can ellipsize overflowing content
FlowRow( FlowRow(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is Extension.Installed && extension.lang.isNotEmpty()) { if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
@ -355,9 +307,10 @@ private fun ExtensionItemContent(
} }
val warning = when { val warning = when {
extension is Extension.Untrusted -> MR.strings.ext_untrusted extension is Extension.Untrusted -> R.string.ext_untrusted
extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete extension is Extension.Installed && extension.isUnofficial -> R.string.ext_unofficial
extension.isNsfw -> MR.strings.ext_nsfw_short extension is Extension.Installed && extension.isObsolete -> R.string.ext_obsolete
extension.isNsfw -> R.string.ext_nsfw_short
else -> null else -> null
} }
if (warning != null) { if (warning != null) {
@ -373,9 +326,9 @@ private fun ExtensionItemContent(
DotSeparatorNoSpaceText() DotSeparatorNoSpaceText()
Text( Text(
text = when (installStep) { text = when (installStep) {
InstallStep.Pending -> stringResource(MR.strings.ext_pending) InstallStep.Pending -> stringResource(R.string.ext_pending)
InstallStep.Downloading -> stringResource(MR.strings.ext_downloading) InstallStep.Downloading -> stringResource(R.string.ext_downloading)
InstallStep.Installing -> stringResource(MR.strings.ext_installing) InstallStep.Installing -> stringResource(R.string.ext_installing)
else -> error("Must not show non-install process text") else -> error("Must not show non-install process text")
}, },
) )
@ -392,86 +345,48 @@ private fun ExtensionItemActions(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClickItemCancel: (Extension) -> Unit = {}, onClickItemCancel: (Extension) -> Unit = {},
onClickItemAction: (Extension) -> Unit = {}, onClickItemAction: (Extension) -> Unit = {},
onClickItemSecondaryAction: (Extension) -> Unit = {},
) { ) {
val isIdle = installStep.isCompleted() val isIdle = installStep.isCompleted()
Row(modifier = modifier) {
Row( if (isIdle) {
modifier = modifier, TextButton(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), onClick = { onClickItemAction(extension) },
) { ) {
when { Text(
!isIdle -> { text = when (installStep) {
InstallStep.Installed -> stringResource(R.string.ext_installed)
InstallStep.Error -> stringResource(R.string.action_retry)
InstallStep.Idle -> {
when (extension) {
is Extension.Installed -> {
if (extension.hasUpdate) {
stringResource(R.string.ext_update)
} else {
stringResource(R.string.action_settings)
}
}
is Extension.Untrusted -> stringResource(R.string.ext_trust)
is Extension.Available -> stringResource(R.string.ext_install)
}
}
else -> error("Must not show install process text")
},
)
}
} else {
IconButton(onClick = { onClickItemCancel(extension) }) { IconButton(onClick = { onClickItemCancel(extension) }) {
Icon( Icon(
imageVector = Icons.Outlined.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(MR.strings.action_cancel), contentDescription = stringResource(R.string.action_cancel),
) )
} }
} }
installStep == InstallStep.Error -> {
IconButton(onClick = { onClickItemAction(extension) }) {
Icon(
imageVector = Icons.Outlined.Refresh,
contentDescription = stringResource(MR.strings.action_retry),
)
}
}
installStep == InstallStep.Idle -> {
when (extension) {
is Extension.Installed -> {
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.action_settings),
)
}
if (extension.hasUpdate) {
IconButton(onClick = { onClickItemAction(extension) }) {
Icon(
imageVector = Icons.Outlined.GetApp,
contentDescription = stringResource(MR.strings.ext_update),
)
}
}
}
is Extension.Untrusted -> {
IconButton(onClick = { onClickItemAction(extension) }) {
Icon(
imageVector = Icons.Outlined.VerifiedUser,
contentDescription = stringResource(MR.strings.ext_trust),
)
}
}
is Extension.Available -> {
if (extension.sources.isNotEmpty()) {
IconButton(
onClick = { onClickItemSecondaryAction(extension) },
) {
Icon(
imageVector = Icons.Outlined.Public,
contentDescription = stringResource(MR.strings.action_open_in_web_view),
)
}
}
IconButton(onClick = { onClickItemAction(extension) }) {
Icon(
imageVector = Icons.Outlined.GetApp,
contentDescription = stringResource(MR.strings.ext_install),
)
}
}
}
}
}
} }
} }
@Composable @Composable
private fun ExtensionHeader( private fun ExtensionHeader(
textRes: StringResource, @StringRes textRes: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
action: @Composable RowScope.() -> Unit = {}, action: @Composable RowScope.() -> Unit = {},
) { ) {
@ -511,19 +426,19 @@ private fun ExtensionTrustDialog(
) { ) {
AlertDialog( AlertDialog(
title = { title = {
Text(text = stringResource(MR.strings.untrusted_extension)) Text(text = stringResource(R.string.untrusted_extension))
}, },
text = { text = {
Text(text = stringResource(MR.strings.untrusted_extension_message)) Text(text = stringResource(R.string.untrusted_extension_message))
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onClickConfirm) { TextButton(onClick = onClickConfirm) {
Text(text = stringResource(MR.strings.ext_trust)) Text(text = stringResource(R.string.ext_trust))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onClickDismiss) { TextButton(onClick = onClickDismiss) {
Text(text = stringResource(MR.strings.ext_uninstall)) Text(text = stringResource(R.string.ext_uninstall))
} }
}, },
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,

View File

@ -60,13 +60,13 @@ fun GlobalSearchScreen(
@Composable @Composable
internal fun GlobalSearchContent( internal fun GlobalSearchContent(
fromSourceId: Long? = null,
items: Map<CatalogueSource, SearchItemResult>, items: Map<CatalogueSource, SearchItemResult>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
getManga: @Composable (Manga) -> State<Manga>, getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit, onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit,
fromSourceId: Long? = null,
) { ) {
LazyColumn( LazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
@ -74,10 +74,8 @@ internal fun GlobalSearchContent(
items.forEach { (source, result) -> items.forEach { (source, result) ->
item(key = source.id) { item(key = source.id) {
GlobalSearchResultItem( GlobalSearchResultItem(
title = fromSourceId?.let { title = fromSourceId?.let { "${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
"${source.name}".takeIf { source.id == fromSourceId } subtitle = LocaleHelper.getDisplayName(source.lang),
} ?: source.name,
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
onClick = { onClickSource(source) }, onClick = { onClickSource(source) },
) { ) {
when (result) { when (result) {

View File

@ -7,9 +7,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.manga.components.BaseMangaListItem import eu.kanade.presentation.manga.components.BaseMangaListItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
@ -33,7 +33,7 @@ fun MigrateMangaScreen(
) { contentPadding -> ) { contentPadding ->
if (state.isEmpty) { if (state.isEmpty) {
EmptyScreen( EmptyScreen(
stringRes = MR.strings.empty_screen, textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
return@Scaffold return@Scaffold
@ -70,10 +70,10 @@ private fun MigrateMangaContent(
@Composable @Composable
private fun MigrateMangaItem( private fun MigrateMangaItem(
modifier: Modifier = Modifier,
manga: Manga, manga: Manga,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit, onClickCover: (Manga) -> Unit,
modifier: Modifier = Modifier,
) { ) {
BaseMangaListItem( BaseMangaListItem(
modifier = modifier, modifier = modifier,

View File

@ -20,22 +20,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.components.BadgeGroup import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.material.topSmallPaddingValues import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.theme.header import tachiyomi.presentation.core.theme.header
@ -54,7 +53,7 @@ fun MigrateSourceScreen(
when { when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen( state.isEmpty -> EmptyScreen(
stringRes = MR.strings.information_empty_library, textResource = R.string.information_empty_library,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
else -> else ->
@ -76,7 +75,7 @@ fun MigrateSourceScreen(
@Composable @Composable
private fun MigrateSourceList( private fun MigrateSourceList(
list: ImmutableList<Pair<Source, Long>>, list: List<Pair<Source, Long>>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,
@ -96,33 +95,21 @@ private fun MigrateSourceList(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(MR.strings.migration_selection_prompt), text = stringResource(R.string.migration_selection_prompt),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
style = MaterialTheme.typography.header, style = MaterialTheme.typography.header,
) )
IconButton(onClick = onToggleSortingMode) { IconButton(onClick = onToggleSortingMode) {
when (sortingMode) { when (sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> Icon( SetMigrateSorting.Mode.ALPHABETICAL -> Icon(Icons.Outlined.SortByAlpha, contentDescription = stringResource(R.string.action_sort_alpha))
Icons.Outlined.SortByAlpha, SetMigrateSorting.Mode.TOTAL -> Icon(Icons.Outlined.Numbers, contentDescription = stringResource(R.string.action_sort_count))
contentDescription = stringResource(MR.strings.action_sort_alpha),
)
SetMigrateSorting.Mode.TOTAL -> Icon(
Icons.Outlined.Numbers,
contentDescription = stringResource(MR.strings.action_sort_count),
)
} }
} }
IconButton(onClick = onToggleSortingDirection) { IconButton(onClick = onToggleSortingDirection) {
when (sortingDirection) { when (sortingDirection) {
SetMigrateSorting.Direction.ASCENDING -> Icon( SetMigrateSorting.Direction.ASCENDING -> Icon(Icons.Outlined.ArrowUpward, contentDescription = stringResource(R.string.action_asc))
Icons.Outlined.ArrowUpward, SetMigrateSorting.Direction.DESCENDING -> Icon(Icons.Outlined.ArrowDownward, contentDescription = stringResource(R.string.action_desc))
contentDescription = stringResource(MR.strings.action_asc),
)
SetMigrateSorting.Direction.DESCENDING -> Icon(
Icons.Outlined.ArrowDownward,
contentDescription = stringResource(MR.strings.action_desc),
)
} }
} }
} }
@ -145,11 +132,11 @@ private fun MigrateSourceList(
@Composable @Composable
private fun MigrateSourceItem( private fun MigrateSourceItem(
modifier: Modifier = Modifier,
source: Source, source: Source,
count: Long, count: Long,
onClickItem: () -> Unit, onClickItem: () -> Unit,
onLongClickItem: () -> Unit, onLongClickItem: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
BaseSourceItem( BaseSourceItem(
modifier = modifier, modifier = modifier,
@ -191,7 +178,7 @@ private fun MigrateSourceItem(
if (source.isStub) { if (source.isStub) {
Text( Text(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
text = stringResource(MR.strings.not_installed), text = stringResource(R.string.not_installed),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,

View File

@ -7,16 +7,16 @@ import androidx.compose.material3.Checkbox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
@Composable @Composable
@ -29,7 +29,7 @@ fun SourcesFilterScreen(
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
title = stringResource(MR.strings.label_sources), title = stringResource(R.string.label_sources),
navigateUp = navigateUp, navigateUp = navigateUp,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
@ -37,7 +37,7 @@ fun SourcesFilterScreen(
) { contentPadding -> ) { contentPadding ->
if (state.isEmpty) { if (state.isEmpty) {
EmptyScreen( EmptyScreen(
stringRes = MR.strings.source_filter_empty_screen, textResource = R.string.source_filter_empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
return@Scaffold return@Scaffold
@ -94,10 +94,10 @@ private fun SourcesFilterContent(
@Composable @Composable
private fun SourcesFilterHeader( private fun SourcesFilterHeader(
modifier: Modifier,
language: String, language: String,
enabled: Boolean, enabled: Boolean,
onClickItem: (String) -> Unit, onClickItem: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
SwitchPreferenceWidget( SwitchPreferenceWidget(
modifier = modifier, modifier = modifier,
@ -109,10 +109,10 @@ private fun SourcesFilterHeader(
@Composable @Composable
private fun SourcesFilterItem( private fun SourcesFilterItem(
modifier: Modifier,
source: Source, source: Source,
enabled: Boolean, enabled: Boolean,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
modifier: Modifier = Modifier,
) { ) {
BaseSourceItem( BaseSourceItem(
modifier = modifier, modifier = modifier,

View File

@ -19,19 +19,19 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.model.Pin import tachiyomi.domain.source.model.Pin
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.material.topSmallPaddingValues import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.theme.header import tachiyomi.presentation.core.theme.header
@ -49,7 +49,7 @@ fun SourcesScreen(
when { when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen( state.isEmpty -> EmptyScreen(
stringRes = MR.strings.source_empty_screen, textResource = R.string.source_empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
else -> { else -> {
@ -94,8 +94,8 @@ fun SourcesScreen(
@Composable @Composable
private fun SourceHeader( private fun SourceHeader(
language: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
language: String,
) { ) {
val context = LocalContext.current val context = LocalContext.current
Text( Text(
@ -108,11 +108,11 @@ private fun SourceHeader(
@Composable @Composable
private fun SourceItem( private fun SourceItem(
modifier: Modifier = Modifier,
source: Source, source: Source,
onClickItem: (Source, Listing) -> Unit, onClickItem: (Source, Listing) -> Unit,
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
modifier: Modifier = Modifier,
) { ) {
BaseSourceItem( BaseSourceItem(
modifier = modifier, modifier = modifier,
@ -123,7 +123,7 @@ private fun SourceItem(
if (source.supportsLatest) { if (source.supportsLatest) {
TextButton(onClick = { onClickItem(source, Listing.Latest) }) { TextButton(onClick = { onClickItem(source, Listing.Latest) }) {
Text( Text(
text = stringResource(MR.strings.latest), text = stringResource(R.string.latest),
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
), ),
@ -144,14 +144,8 @@ private fun SourcePinButton(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
val tint = if (isPinned) { val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground.copy(alpha = SecondaryItemAlpha)
MaterialTheme.colorScheme.primary val description = if (isPinned) R.string.action_unpin else R.string.action_pin
} else {
MaterialTheme.colorScheme.onBackground.copy(
alpha = SecondaryItemAlpha,
)
}
val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin
IconButton(onClick = onClick) { IconButton(onClick = onClick) {
Icon( Icon(
imageVector = icon, imageVector = icon,
@ -174,7 +168,7 @@ fun SourceOptionsDialog(
}, },
text = { text = {
Column { Column {
val textId = if (Pin.Pinned in source.pin) MR.strings.action_unpin else MR.strings.action_pin val textId = if (Pin.Pinned in source.pin) R.string.action_unpin else R.string.action_pin
Text( Text(
text = stringResource(textId), text = stringResource(textId),
modifier = Modifier modifier = Modifier
@ -184,7 +178,7 @@ fun SourceOptionsDialog(
) )
if (!source.isLocal()) { if (!source.isLocal()) {
Text( Text(
text = stringResource(MR.strings.action_disable), text = stringResource(R.string.action_disable),
modifier = Modifier modifier = Modifier
.clickable(onClick = onClickDisable) .clickable(onClick = onClickDisable)
.fillMaxWidth() .fillMaxWidth()

View File

@ -16,8 +16,8 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
fun BaseSourceItem( fun BaseSourceItem(
source: Source,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
source: Source,
showLanguageInContent: Boolean = true, showLanguageInContent: Boolean = true,
onClickItem: () -> Unit = {}, onClickItem: () -> Unit = {},
onLongClickItem: () -> Unit = {}, onLongClickItem: () -> Unit = {},
@ -25,9 +25,7 @@ fun BaseSourceItem(
action: @Composable RowScope.(Source) -> Unit = {}, action: @Composable RowScope.(Source) -> Unit = {},
content: @Composable RowScope.(Source, String?) -> Unit = defaultContent, content: @Composable RowScope.(Source, String?) -> Unit = defaultContent,
) { ) {
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { showLanguageInContent }
showLanguageInContent
}
BaseBrowseItem( BaseBrowseItem(
modifier = modifier, modifier = modifier,
onClickItem = onClickItem, onClickItem = onClickItem,

View File

@ -4,9 +4,11 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun RemoveMangaDialog( fun RemoveMangaDialog(
@ -18,7 +20,7 @@ fun RemoveMangaDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
confirmButton = { confirmButton = {
@ -28,14 +30,14 @@ fun RemoveMangaDialog(
onConfirm() onConfirm()
}, },
) { ) {
Text(text = stringResource(MR.strings.action_remove)) Text(text = stringResource(R.string.action_remove))
} }
}, },
title = { title = {
Text(text = stringResource(MR.strings.are_you_sure)) Text(text = stringResource(R.string.are_you_sure))
}, },
text = { text = {
Text(text = stringResource(MR.strings.remove_manga, mangaToRemove.title)) Text(text = stringResource(R.string.remove_manga, mangaToRemove.title))
}, },
) )
} }

View File

@ -1,7 +1,7 @@
package eu.kanade.presentation.browse.components package eu.kanade.presentation.browse.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material.icons.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
@ -10,18 +10,17 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.RadioMenuItem import eu.kanade.presentation.components.RadioMenuItem
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.source.local.LocalSource import tachiyomi.source.local.LocalSource
@Composable @Composable
@ -54,66 +53,50 @@ fun BrowseSourceToolbar(
onClickCloseSearch = navigateUp, onClickCloseSearch = navigateUp,
actions = { actions = {
AppBarActions( AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder() actions = listOfNotNull(
.apply {
add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_display_mode), title = stringResource(R.string.action_display_mode),
icon = if (displayMode == LibraryDisplayMode.List) { icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
Icons.AutoMirrored.Filled.ViewList
} else {
Icons.Filled.ViewModule
},
onClick = { selectingDisplayMode = true }, onClick = { selectingDisplayMode = true },
), ),
)
if (isLocalSource) { if (isLocalSource) {
add(
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.label_help), title = stringResource(R.string.label_help),
onClick = onHelpClick, onClick = onHelpClick,
),
) )
} else { } else {
add(
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_web_view), title = stringResource(R.string.action_open_in_web_view),
onClick = onWebViewClick, onClick = onWebViewClick,
),
) )
} },
if (isConfigurableSource) {
add(
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_settings), title = stringResource(R.string.action_settings),
onClick = onSettingsClick, onClick = onSettingsClick,
).takeIf { isConfigurableSource },
), ),
) )
}
}
.build(),
)
DropdownMenu( DropdownMenu(
expanded = selectingDisplayMode, expanded = selectingDisplayMode,
onDismissRequest = { selectingDisplayMode = false }, onDismissRequest = { selectingDisplayMode = false },
) { ) {
RadioMenuItem( RadioMenuItem(
text = { Text(text = stringResource(MR.strings.action_display_comfortable_grid)) }, text = { Text(text = stringResource(R.string.action_display_comfortable_grid)) },
isChecked = displayMode == LibraryDisplayMode.ComfortableGrid, isChecked = displayMode == LibraryDisplayMode.ComfortableGrid,
) { ) {
selectingDisplayMode = false selectingDisplayMode = false
onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) onDisplayModeChange(LibraryDisplayMode.ComfortableGrid)
} }
RadioMenuItem( RadioMenuItem(
text = { Text(text = stringResource(MR.strings.action_display_grid)) }, text = { Text(text = stringResource(R.string.action_display_grid)) },
isChecked = displayMode == LibraryDisplayMode.CompactGrid, isChecked = displayMode == LibraryDisplayMode.CompactGrid,
) { ) {
selectingDisplayMode = false selectingDisplayMode = false
onDisplayModeChange(LibraryDisplayMode.CompactGrid) onDisplayModeChange(LibraryDisplayMode.CompactGrid)
} }
RadioMenuItem( RadioMenuItem(
text = { Text(text = stringResource(MR.strings.action_display_list)) }, text = { Text(text = stringResource(R.string.action_display_list)) },
isChecked = displayMode == LibraryDisplayMode.List, isChecked = displayMode == LibraryDisplayMode.List,
) { ) {
selectingDisplayMode = false selectingDisplayMode = false

View File

@ -13,15 +13,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.library.components.CommonMangaItemDefaults import eu.kanade.presentation.library.components.CommonMangaItemDefaults
import eu.kanade.presentation.library.components.MangaComfortableGridItem import eu.kanade.presentation.library.components.MangaComfortableGridItem
import eu.kanade.tachiyomi.R
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.manga.model.asMangaCover import tachiyomi.domain.manga.model.asMangaCover
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun GlobalSearchCardRow( fun GlobalSearchCardRow(
@ -37,7 +37,7 @@ fun GlobalSearchCardRow(
LazyRow( LazyRow(
contentPadding = PaddingValues(MaterialTheme.padding.small), contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
) { ) {
items(titles) { items(titles) {
val title by getManga(it) val title by getManga(it)
@ -78,7 +78,7 @@ private fun MangaItem(
@Composable @Composable
private fun EmptyResultItem() { private fun EmptyResultItem() {
Text( Text(
text = stringResource(MR.strings.no_results_found), text = stringResource(R.string.no_results_found),
modifier = Modifier modifier = Modifier
.padding( .padding(
horizontal = MaterialTheme.padding.medium, horizontal = MaterialTheme.padding.medium,

View File

@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Error import androidx.compose.material.icons.outlined.Error
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -21,11 +21,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun GlobalSearchResultItem( fun GlobalSearchResultItem(
@ -39,7 +39,7 @@ fun GlobalSearchResultItem(
modifier = Modifier modifier = Modifier
.padding( .padding(
start = MaterialTheme.padding.medium, start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.extraSmall, end = MaterialTheme.padding.tiny,
) )
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick), .clickable(onClick = onClick),
@ -54,7 +54,7 @@ fun GlobalSearchResultItem(
Text(text = subtitle) Text(text = subtitle)
} }
IconButton(onClick = onClick) { IconButton(onClick = onClick) {
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
} }
} }
content() content()
@ -92,7 +92,7 @@ fun GlobalSearchErrorResultItem(message: String?) {
Icon(imageVector = Icons.Outlined.Error, contentDescription = null) Icon(imageVector = Icons.Outlined.Error, contentDescription = null)
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = message ?: stringResource(MR.strings.unknown_error), text = message ?: stringResource(R.string.unknown_error),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
} }

View File

@ -26,11 +26,11 @@ import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun GlobalSearchToolbar( fun GlobalSearchToolbar(
@ -58,7 +58,7 @@ fun GlobalSearchToolbar(
) )
if (progress in 1..<total) { if (progress in 1..<total) {
LinearProgressIndicator( LinearProgressIndicator(
progress = { progress / total.toFloat() }, progress = progress / total.toFloat(),
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.fillMaxWidth(), .fillMaxWidth(),
@ -85,7 +85,7 @@ fun GlobalSearchToolbar(
) )
}, },
label = { label = {
Text(text = stringResource(MR.strings.pinned_sources)) Text(text = stringResource(id = R.string.pinned_sources))
}, },
) )
FilterChip( FilterChip(
@ -100,7 +100,7 @@ fun GlobalSearchToolbar(
) )
}, },
label = { label = {
Text(text = stringResource(MR.strings.all)) Text(text = stringResource(id = R.string.all))
}, },
) )
@ -118,7 +118,7 @@ fun GlobalSearchToolbar(
) )
}, },
label = { label = {
Text(text = stringResource(MR.strings.has_results)) Text(text = stringResource(id = R.string.has_results))
}, },
) )
} }

View File

@ -2,20 +2,19 @@ package eu.kanade.presentation.category
import android.content.Context import android.content.Context
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import tachiyomi.core.i18n.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
val Category.visualName: String val Category.visualName: String
@Composable @Composable
get() = when { get() = when {
isSystemCategory -> stringResource(MR.strings.label_default) isSystemCategory -> stringResource(R.string.label_default)
else -> name else -> name
} }
fun Category.visualName(context: Context): String = fun Category.visualName(context: Context): String =
when { when {
isSystemCategory -> context.stringResource(MR.strings.label_default) isSystemCategory -> context.getString(R.string.label_default)
else -> name else -> name
} }

View File

@ -12,18 +12,17 @@ import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryListItem import eu.kanade.presentation.category.components.CategoryListItem
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.category.CategoryScreenState import eu.kanade.tachiyomi.ui.category.CategoryScreenState
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.material.topSmallPaddingValues import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.plus
@ -42,13 +41,13 @@ fun CategoryScreen(
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
title = stringResource(MR.strings.action_edit_categories), title = stringResource(R.string.action_edit_categories),
navigateUp = navigateUp, navigateUp = navigateUp,
actions = { actions = {
AppBarActions( AppBarActions(
persistentListOf( listOf(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_sort), title = stringResource(R.string.action_sort),
icon = Icons.Outlined.SortByAlpha, icon = Icons.Outlined.SortByAlpha,
onClick = onClickSortAlphabetically, onClick = onClickSortAlphabetically,
), ),
@ -67,7 +66,7 @@ fun CategoryScreen(
) { paddingValues -> ) { paddingValues ->
if (state.isEmpty) { if (state.isEmpty) {
EmptyScreen( EmptyScreen(
stringRes = MR.strings.information_empty_category, textResource = R.string.information_empty_category,
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
) )
return@Scaffold return@Scaffold
@ -76,9 +75,7 @@ fun CategoryScreen(
CategoryContent( CategoryContent(
categories = state.categories, categories = state.categories,
lazyListState = lazyListState, lazyListState = lazyListState,
paddingValues = paddingValues + paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium),
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
onClickRename = onClickRename, onClickRename = onClickRename,
onClickDelete = onClickDelete, onClickDelete = onClickDelete,
onMoveUp = onClickMoveUp, onMoveUp = onClickMoveUp,

View File

@ -25,28 +25,26 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import eu.kanade.core.preference.asToggleableState import eu.kanade.core.preference.asToggleableState
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import kotlinx.collections.immutable.ImmutableList import eu.kanade.tachiyomi.R
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import tachiyomi.core.preference.CheckboxState import tachiyomi.core.preference.CheckboxState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun CategoryCreateDialog( fun CategoryCreateDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onCreate: (String) -> Unit, onCreate: (String) -> Unit,
categories: ImmutableList<String>, categories: List<Category>,
) { ) {
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.contains(name) } val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -58,32 +56,25 @@ fun CategoryCreateDialog(
onDismissRequest() onDismissRequest()
}, },
) { ) {
Text(text = stringResource(MR.strings.action_add)) Text(text = stringResource(R.string.action_add))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
title = { title = {
Text(text = stringResource(MR.strings.action_add_category)) Text(text = stringResource(R.string.action_add_category))
}, },
text = { text = {
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier.focusRequester(focusRequester),
.focusRequester(focusRequester),
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { label = { Text(text = stringResource(R.string.name)) },
Text(text = stringResource(MR.strings.name))
},
supportingText = { supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) { val msgRes = if (name.isNotEmpty() && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
MR.strings.error_category_exists
} else {
MR.strings.information_required_plain
}
Text(text = stringResource(msgRes)) Text(text = stringResource(msgRes))
}, },
isError = name.isNotEmpty() && nameAlreadyExists, isError = name.isNotEmpty() && nameAlreadyExists,
@ -103,14 +94,14 @@ fun CategoryCreateDialog(
fun CategoryRenameDialog( fun CategoryRenameDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onRename: (String) -> Unit, onRename: (String) -> Unit,
categories: ImmutableList<String>, categories: List<Category>,
category: String, category: Category,
) { ) {
var name by remember { mutableStateOf(category) } var name by remember { mutableStateOf(category.name) }
var valueHasChanged by remember { mutableStateOf(false) } var valueHasChanged by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.contains(name) } val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -122,16 +113,16 @@ fun CategoryRenameDialog(
onDismissRequest() onDismissRequest()
}, },
) { ) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
title = { title = {
Text(text = stringResource(MR.strings.action_rename_category)) Text(text = stringResource(R.string.action_rename_category))
}, },
text = { text = {
OutlinedTextField( OutlinedTextField(
@ -141,13 +132,9 @@ fun CategoryRenameDialog(
valueHasChanged = name != it valueHasChanged = name != it
name = it name = it
}, },
label = { Text(text = stringResource(MR.strings.name)) }, label = { Text(text = stringResource(R.string.name)) },
supportingText = { supportingText = {
val msgRes = if (valueHasChanged && nameAlreadyExists) { val msgRes = if (valueHasChanged && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
MR.strings.error_category_exists
} else {
MR.strings.information_required_plain
}
Text(text = stringResource(msgRes)) Text(text = stringResource(msgRes))
}, },
isError = valueHasChanged && nameAlreadyExists, isError = valueHasChanged && nameAlreadyExists,
@ -167,7 +154,7 @@ fun CategoryRenameDialog(
fun CategoryDeleteDialog( fun CategoryDeleteDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
category: String, category: Category,
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -176,19 +163,19 @@ fun CategoryDeleteDialog(
onDelete() onDelete()
onDismissRequest() onDismissRequest()
}) { }) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
title = { title = {
Text(text = stringResource(MR.strings.delete_category)) Text(text = stringResource(R.string.delete_category))
}, },
text = { text = {
Text(text = stringResource(MR.strings.delete_category_confirmation, category)) Text(text = stringResource(R.string.delete_category_confirmation, category.name))
}, },
) )
} }
@ -205,26 +192,26 @@ fun CategorySortAlphabeticallyDialog(
onSort() onSort()
onDismissRequest() onDismissRequest()
}) { }) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
title = { title = {
Text(text = stringResource(MR.strings.action_sort_category)) Text(text = stringResource(R.string.action_sort_category))
}, },
text = { text = {
Text(text = stringResource(MR.strings.sort_category_confirmation)) Text(text = stringResource(R.string.sort_category_confirmation))
}, },
) )
} }
@Composable @Composable
fun ChangeCategoryDialog( fun ChangeCategoryDialog(
initialSelection: ImmutableList<CheckboxState<Category>>, initialSelection: List<CheckboxState<Category>>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onEditCategories: () -> Unit, onEditCategories: () -> Unit,
onConfirm: (List<Long>, List<Long>) -> Unit, onConfirm: (List<Long>, List<Long>) -> Unit,
@ -239,14 +226,14 @@ fun ChangeCategoryDialog(
onEditCategories() onEditCategories()
}, },
) { ) {
Text(text = stringResource(MR.strings.action_edit_categories)) Text(text = stringResource(R.string.action_edit_categories))
} }
}, },
title = { title = {
Text(text = stringResource(MR.strings.action_move_category)) Text(text = stringResource(R.string.action_move_category))
}, },
text = { text = {
Text(text = stringResource(MR.strings.information_empty_category_dialog)) Text(text = stringResource(R.string.information_empty_category_dialog))
}, },
) )
return return
@ -260,31 +247,27 @@ fun ChangeCategoryDialog(
onDismissRequest() onDismissRequest()
onEditCategories() onEditCategories()
}) { }) {
Text(text = stringResource(MR.strings.action_edit)) Text(text = stringResource(R.string.action_edit))
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
tachiyomi.presentation.core.components.material.TextButton(onClick = onDismissRequest) { tachiyomi.presentation.core.components.material.TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
tachiyomi.presentation.core.components.material.TextButton( tachiyomi.presentation.core.components.material.TextButton(
onClick = { onClick = {
onDismissRequest() onDismissRequest()
onConfirm( onConfirm(
selection selection.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }.map { it.value.id },
.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include } selection.filter { it is CheckboxState.State.None || it is CheckboxState.TriState.None }.map { it.value.id },
.map { it.value.id },
selection
.filter { it is CheckboxState.State.None || it is CheckboxState.TriState.None }
.map { it.value.id },
) )
}, },
) { ) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
} }
}, },
title = { title = {
Text(text = stringResource(MR.strings.action_move_category)) Text(text = stringResource(R.string.action_move_category))
}, },
text = { text = {
Column( Column(
@ -296,7 +279,7 @@ fun ChangeCategoryDialog(
if (index != -1) { if (index != -1) {
val mutableList = selection.toMutableList() val mutableList = selection.toMutableList()
mutableList[index] = it.next() mutableList[index] = it.next()
selection = mutableList.toList().toImmutableList() selection = mutableList.toList()
} }
} }
Row( Row(
@ -330,3 +313,7 @@ fun ChangeCategoryDialog(
}, },
) )
} }
private fun List<Category>.anyWithName(name: String): Boolean {
return any { name == it.name }
}

View File

@ -6,10 +6,9 @@ import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource
import tachiyomi.i18n.MR import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.presentation.core.util.isScrollingUp
@ -17,13 +16,11 @@ import tachiyomi.presentation.core.util.isScrollingUp
fun CategoryFloatingActionButton( fun CategoryFloatingActionButton(
lazyListState: LazyListState, lazyListState: LazyListState,
onCreate: () -> Unit, onCreate: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_add)) }, text = { Text(text = stringResource(R.string.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
onClick = onCreate, onClick = onCreate,
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(), expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
modifier = modifier,
) )
} }

View File

@ -6,11 +6,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.ArrowDropUp import androidx.compose.material.icons.outlined.ArrowDropUp
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -19,13 +19,14 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun CategoryListItem( fun CategoryListItem(
modifier: Modifier = Modifier,
category: Category, category: Category,
canMoveUp: Boolean, canMoveUp: Boolean,
canMoveDown: Boolean, canMoveDown: Boolean,
@ -33,7 +34,6 @@ fun CategoryListItem(
onMoveDown: (Category) -> Unit, onMoveDown: (Category) -> Unit,
onRename: () -> Unit, onRename: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
ElevatedCard( ElevatedCard(
modifier = modifier, modifier = modifier,
@ -49,7 +49,7 @@ fun CategoryListItem(
), ),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null) Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
Text( Text(
text = category.name, text = category.name,
modifier = Modifier modifier = Modifier
@ -61,23 +61,20 @@ fun CategoryListItem(
onClick = { onMoveUp(category) }, onClick = { onMoveUp(category) },
enabled = canMoveUp, enabled = canMoveUp,
) { ) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null) Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
} }
IconButton( IconButton(
onClick = { onMoveDown(category) }, onClick = { onMoveDown(category) },
enabled = canMoveDown, enabled = canMoveDown,
) { ) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null) Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) { IconButton(onClick = onRename) {
Icon( Icon(imageVector = Icons.Outlined.Edit, contentDescription = stringResource(R.string.action_rename_category))
imageVector = Icons.Outlined.Edit,
contentDescription = stringResource(MR.strings.action_rename_category),
)
} }
IconButton(onClick = onDelete) { IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete)) Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(R.string.action_delete))
} }
} }
} }

View File

@ -71,10 +71,10 @@ fun NavigatorAdaptiveSheet(
*/ */
@Composable @Composable
fun AdaptiveSheet( fun AdaptiveSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tonalElevation: Dp = 1.dp, tonalElevation: Dp = 1.dp,
enableSwipeDismiss: Boolean = true, enableSwipeDismiss: Boolean = true,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val isTabletUi = isTabletUi() val isTabletUi = isTabletUi()

View File

@ -10,7 +10,8 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
@ -19,14 +20,11 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip import androidx.compose.material3.PlainTooltipBox
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTooltipState
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -42,16 +40,17 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.collections.immutable.ImmutableList import eu.kanade.tachiyomi.R
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
import tachiyomi.presentation.core.util.runOnEnterKeyPressed import tachiyomi.presentation.core.util.runOnEnterKeyPressed
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
@ -61,11 +60,10 @@ const val SEARCH_DEBOUNCE_MILLIS = 250L
@Composable @Composable
fun AppBar( fun AppBar(
title: String?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
backgroundColor: Color? = null, backgroundColor: Color? = null,
// Text // Text
title: String?,
subtitle: String? = null, subtitle: String? = null,
// Up button // Up button
navigateUp: (() -> Unit)? = null, navigateUp: (() -> Unit)? = null,
@ -90,7 +88,7 @@ fun AppBar(
if (isActionMode) { if (isActionMode) {
AppBarTitle(actionModeCounter.toString()) AppBarTitle(actionModeCounter.toString())
} else { } else {
AppBarTitle(title, subtitle = subtitle) AppBarTitle(title, subtitle)
} }
}, },
navigateUp = navigateUp, navigateUp = navigateUp,
@ -110,11 +108,10 @@ fun AppBar(
@Composable @Composable
fun AppBar( fun AppBar(
// Title
titleContent: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
backgroundColor: Color? = null, backgroundColor: Color? = null,
// Title
titleContent: @Composable () -> Unit,
// Up button // Up button
navigateUp: (() -> Unit)? = null, navigateUp: (() -> Unit)? = null,
navigationIcon: ImageVector? = null, navigationIcon: ImageVector? = null,
@ -135,13 +132,13 @@ fun AppBar(
IconButton(onClick = onCancelActionMode) { IconButton(onClick = onCancelActionMode) {
Icon( Icon(
imageVector = Icons.Outlined.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(MR.strings.action_cancel), contentDescription = stringResource(R.string.action_cancel),
) )
} }
} else { } else {
navigateUp?.let { navigateUp?.let {
IconButton(onClick = it) { IconButton(onClick = it) {
UpIcon(navigationIcon = navigationIcon) UpIcon(navigationIcon)
} }
} }
} }
@ -161,10 +158,9 @@ fun AppBar(
@Composable @Composable
fun AppBarTitle( fun AppBarTitle(
title: String?, title: String?,
modifier: Modifier = Modifier,
subtitle: String? = null, subtitle: String? = null,
) { ) {
Column(modifier = modifier) { Column {
title?.let { title?.let {
Text( Text(
text = it, text = it,
@ -188,23 +184,18 @@ fun AppBarTitle(
@Composable @Composable
fun AppBarActions( fun AppBarActions(
actions: ImmutableList<AppBar.AppBarAction>, actions: List<AppBar.AppBarAction>,
) { ) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
actions.filterIsInstance<AppBar.Action>().map { actions.filterIsInstance<AppBar.Action>().map {
TooltipBox( PlainTooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), tooltip = { Text(it.title) },
tooltip = {
PlainTooltip {
Text(it.title)
}
},
state = rememberTooltipState(),
) { ) {
IconButton( IconButton(
onClick = it.onClick, onClick = it.onClick,
enabled = it.enabled, enabled = it.enabled,
modifier = Modifier.tooltipTrigger(),
) { ) {
Icon( Icon(
imageVector = it.icon, imageVector = it.icon,
@ -217,21 +208,16 @@ fun AppBarActions(
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>() val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
if (overflowActions.isNotEmpty()) { if (overflowActions.isNotEmpty()) {
TooltipBox( PlainTooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), tooltip = { Text(stringResource(R.string.abc_action_menu_overflow_description)) },
tooltip = {
PlainTooltip {
Text(stringResource(MR.strings.action_menu_overflow_description))
}
},
state = rememberTooltipState(),
) { ) {
IconButton( IconButton(
onClick = { showMenu = !showMenu }, onClick = { showMenu = !showMenu },
modifier = Modifier.tooltipTrigger(),
) { ) {
Icon( Icon(
Icons.Outlined.MoreVert, Icons.Outlined.MoreVert,
contentDescription = stringResource(MR.strings.action_menu_overflow_description), contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
) )
} }
} }
@ -256,16 +242,15 @@ fun AppBarActions(
/** /**
* @param searchEnabled Set to false if you don't want to show search action. * @param searchEnabled Set to false if you don't want to show search action.
* @param searchQuery If null, use normal toolbar. * @param searchQuery If null, use normal toolbar.
* @param placeholderText If null, [MR.strings.action_search_hint] is used. * @param placeholderText If null, [R.string.action_search_hint] is used.
*/ */
@Composable @Composable
fun SearchToolbar( fun SearchToolbar(
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
modifier: Modifier = Modifier,
titleContent: @Composable () -> Unit = {}, titleContent: @Composable () -> Unit = {},
navigateUp: (() -> Unit)? = null, navigateUp: (() -> Unit)? = null,
searchEnabled: Boolean = true, searchEnabled: Boolean = true,
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
placeholderText: String? = null, placeholderText: String? = null,
onSearch: (String) -> Unit = {}, onSearch: (String) -> Unit = {},
onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) }, onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) },
@ -277,7 +262,6 @@ fun SearchToolbar(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
AppBar( AppBar(
modifier = modifier,
titleContent = { titleContent = {
if (searchQuery == null) return@AppBar titleContent() if (searchQuery == null) return@AppBar titleContent()
@ -322,7 +306,7 @@ fun SearchToolbar(
placeholder = { placeholder = {
Text( Text(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
text = (placeholderText ?: stringResource(MR.strings.action_search_hint)), text = (placeholderText ?: stringResource(R.string.action_search_hint)),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleMedium.copy( style = MaterialTheme.typography.titleMedium.copy(
@ -343,43 +327,33 @@ fun SearchToolbar(
if (!searchEnabled) { if (!searchEnabled) {
// Don't show search action // Don't show search action
} else if (searchQuery == null) { } else if (searchQuery == null) {
TooltipBox( PlainTooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), tooltip = { Text(stringResource(R.string.action_search)) },
tooltip = {
PlainTooltip {
Text(stringResource(MR.strings.action_search))
}
},
state = rememberTooltipState(),
) { ) {
IconButton( IconButton(
onClick = onClick, onClick = onClick,
modifier = Modifier.tooltipTrigger(),
) { ) {
Icon( Icon(
Icons.Outlined.Search, Icons.Outlined.Search,
contentDescription = stringResource(MR.strings.action_search), contentDescription = stringResource(R.string.action_search),
) )
} }
} }
} else if (searchQuery.isNotEmpty()) { } else if (searchQuery.isNotEmpty()) {
TooltipBox( PlainTooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), tooltip = { Text(stringResource(R.string.action_reset)) },
tooltip = {
PlainTooltip {
Text(stringResource(MR.strings.action_reset))
}
},
state = rememberTooltipState(),
) { ) {
IconButton( IconButton(
onClick = { onClick = {
onClick() onClick()
focusRequester.requestFocus() focusRequester.requestFocus()
}, },
modifier = Modifier.tooltipTrigger(),
) { ) {
Icon( Icon(
Icons.Outlined.Close, Icons.Outlined.Close,
contentDescription = stringResource(MR.strings.action_reset), contentDescription = stringResource(R.string.action_reset),
) )
} }
} }
@ -394,16 +368,12 @@ fun SearchToolbar(
} }
@Composable @Composable
fun UpIcon( fun UpIcon(navigationIcon: ImageVector? = null) {
modifier: Modifier = Modifier,
navigationIcon: ImageVector? = null,
) {
val icon = navigationIcon val icon = navigationIcon
?: Icons.AutoMirrored.Outlined.ArrowBack ?: if (LocalLayoutDirection.current == LayoutDirection.Ltr) Icons.Outlined.ArrowBack else Icons.Outlined.ArrowForward
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = stringResource(MR.strings.action_bar_up_description), contentDescription = stringResource(R.string.abc_action_bar_up_description),
modifier = modifier,
) )
} }

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
@ -25,14 +26,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy import androidx.compose.ui.util.fastMaxBy
import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.R
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
val DownloadedOnlyBannerBackgroundColor val DownloadedOnlyBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.tertiary @Composable get() = MaterialTheme.colorScheme.tertiary
@ -43,7 +43,7 @@ val IndexingBannerBackgroundColor
@Composable @Composable
fun WarningBanner( fun WarningBanner(
textRes: StringResource, @StringRes textRes: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Text( Text(
@ -127,7 +127,7 @@ fun AppStateBanners(
@Composable @Composable
private fun DownloadedOnlyModeBanner(modifier: Modifier = Modifier) { private fun DownloadedOnlyModeBanner(modifier: Modifier = Modifier) {
Text( Text(
text = stringResource(MR.strings.label_downloaded_only), text = stringResource(R.string.label_downloaded_only),
modifier = Modifier modifier = Modifier
.background(DownloadedOnlyBannerBackgroundColor) .background(DownloadedOnlyBannerBackgroundColor)
.fillMaxWidth() .fillMaxWidth()
@ -142,7 +142,7 @@ private fun DownloadedOnlyModeBanner(modifier: Modifier = Modifier) {
@Composable @Composable
private fun IncognitoModeBanner(modifier: Modifier = Modifier) { private fun IncognitoModeBanner(modifier: Modifier = Modifier) {
Text( Text(
text = stringResource(MR.strings.pref_incognito_mode), text = stringResource(R.string.pref_incognito_mode),
modifier = Modifier modifier = Modifier
.background(IncognitoModeBannerBackgroundColor) .background(IncognitoModeBannerBackgroundColor)
.fillMaxWidth() .fillMaxWidth()
@ -173,7 +173,7 @@ private fun IndexingDownloadBanner(modifier: Modifier = Modifier) {
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = stringResource(MR.strings.download_notifier_cache_renewal), text = stringResource(R.string.download_notifier_cache_renewal),
color = MaterialTheme.colorScheme.onSecondary, color = MaterialTheme.colorScheme.onSecondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,

View File

@ -1,40 +0,0 @@
package eu.kanade.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.util.lang.toRelativeString
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@Composable
fun relativeDateText(
dateEpochMillis: Long,
): String {
return relativeDateText(
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L },
)
}
@Composable
fun relativeDateText(
date: Date?,
): String {
val context = LocalContext.current
val preferences = remember { Injekt.get<UiPreferences>() }
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
return date
?.toRelativeString(
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
?: stringResource(MR.strings.not_applicable)
}

View File

@ -3,34 +3,28 @@ package eu.kanade.presentation.components
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf import eu.kanade.tachiyomi.R
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun DownloadDropdownMenu( fun DownloadDropdownMenu(
expanded: Boolean, expanded: Boolean,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit, onDownloadClicked: (DownloadAction) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val options = persistentListOf(
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
DownloadAction.NEXT_5_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 5, 5),
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
)
DropdownMenu( DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
modifier = modifier,
) { ) {
options.map { (downloadAction, string) -> listOfNotNull(
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(R.plurals.download_amount, 1, 1),
DownloadAction.NEXT_5_CHAPTERS to pluralStringResource(R.plurals.download_amount, 5, 5),
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(R.plurals.download_amount, 10, 10),
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(R.plurals.download_amount, 25, 25),
DownloadAction.UNREAD_CHAPTERS to stringResource(R.string.download_unread),
).map { (downloadAction, string) ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = string) }, text = { Text(text = string) },
onClick = { onClick = {

View File

@ -1,12 +1,10 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.outlined.ArrowLeft
import androidx.compose.material.icons.outlined.ArrowRight
import androidx.compose.material.icons.outlined.RadioButtonChecked import androidx.compose.material.icons.outlined.RadioButtonChecked
import androidx.compose.material.icons.outlined.RadioButtonUnchecked import androidx.compose.material.icons.outlined.RadioButtonUnchecked
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@ -18,24 +16,21 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import tachiyomi.i18n.MR import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.i18n.stringResource
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
/**
* DropdownMenu but overlaps anchor and has width constraints to better
* match non-Compose implementation.
*/
@Composable @Composable
fun DropdownMenu( fun DropdownMenu(
expanded: Boolean, expanded: Boolean,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(8.dp, (-56).dp), offset: DpOffset = DpOffset(8.dp, (-56).dp),
scrollState: ScrollState = rememberScrollState(),
properties: PopupProperties = PopupProperties(focusable = true), properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
@ -44,7 +39,6 @@ fun DropdownMenu(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp), modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
offset = offset, offset = offset,
scrollState = scrollState,
properties = properties, properties = properties,
content = content, content = content,
) )
@ -54,7 +48,6 @@ fun DropdownMenu(
fun RadioMenuItem( fun RadioMenuItem(
text: @Composable () -> Unit, text: @Composable () -> Unit,
isChecked: Boolean, isChecked: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
DropdownMenuItem( DropdownMenuItem(
@ -64,17 +57,16 @@ fun RadioMenuItem(
if (isChecked) { if (isChecked) {
Icon( Icon(
imageVector = Icons.Outlined.RadioButtonChecked, imageVector = Icons.Outlined.RadioButtonChecked,
contentDescription = stringResource(MR.strings.selected), contentDescription = stringResource(R.string.selected),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
} else { } else {
Icon( Icon(
imageVector = Icons.Outlined.RadioButtonUnchecked, imageVector = Icons.Outlined.RadioButtonUnchecked,
contentDescription = stringResource(MR.strings.not_selected), contentDescription = stringResource(R.string.not_selected),
) )
} }
}, },
modifier = modifier,
) )
} }
@ -82,18 +74,17 @@ fun RadioMenuItem(
fun NestedMenuItem( fun NestedMenuItem(
text: @Composable () -> Unit, text: @Composable () -> Unit,
children: @Composable ColumnScope.(() -> Unit) -> Unit, children: @Composable ColumnScope.(() -> Unit) -> Unit,
modifier: Modifier = Modifier,
) { ) {
var nestedExpanded by remember { mutableStateOf(false) } var nestedExpanded by remember { mutableStateOf(false) }
val closeMenu = { nestedExpanded = false } val closeMenu = { nestedExpanded = false }
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
Box {
DropdownMenuItem( DropdownMenuItem(
text = text, text = text,
onClick = { nestedExpanded = true }, onClick = { nestedExpanded = true },
trailingIcon = { trailingIcon = {
Icon( Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowRight, imageVector = if (isLtr) Icons.Outlined.ArrowRight else Icons.Outlined.ArrowLeft,
contentDescription = null, contentDescription = null,
) )
}, },
@ -102,9 +93,7 @@ fun NestedMenuItem(
DropdownMenu( DropdownMenu(
expanded = nestedExpanded, expanded = nestedExpanded,
onDismissRequest = closeMenu, onDismissRequest = closeMenu,
modifier = modifier,
) { ) {
children(closeMenu) children(closeMenu)
} }
} }
}

View File

@ -1,45 +1,44 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewLightDark import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.tachiyomi.R
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.EmptyScreenAction import tachiyomi.presentation.core.screens.EmptyScreenAction
import tachiyomi.presentation.core.util.ThemePreviews
@PreviewLightDark @ThemePreviews
@Composable @Composable
private fun NoActionPreview() { private fun NoActionPreview() {
TachiyomiPreviewTheme { TachiyomiTheme {
Surface { Surface {
EmptyScreen( EmptyScreen(
stringRes = MR.strings.empty_screen, textResource = R.string.empty_screen,
) )
} }
} }
} }
@PreviewLightDark @ThemePreviews
@Composable @Composable
private fun WithActionPreview() { private fun WithActionPreview() {
TachiyomiPreviewTheme { TachiyomiTheme {
Surface { Surface {
EmptyScreen( EmptyScreen(
stringRes = MR.strings.empty_screen, textResource = R.string.empty_screen,
actions = persistentListOf( actions = listOf(
EmptyScreenAction( EmptyScreenAction(
stringRes = MR.strings.action_retry, stringResId = R.string.action_retry,
icon = Icons.Outlined.Refresh, icon = Icons.Outlined.Refresh,
onClick = {}, onClick = {},
), ),
EmptyScreenAction( EmptyScreenAction(
stringRes = MR.strings.getting_started_guide, stringResId = R.string.getting_started_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = {}, onClick = {},
), ),
), ),

View File

@ -0,0 +1,30 @@
package eu.kanade.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import eu.kanade.tachiyomi.util.lang.toRelativeString
import tachiyomi.presentation.core.components.ListGroupHeader
import java.text.DateFormat
import java.util.Date
@Composable
fun RelativeDateHeader(
modifier: Modifier = Modifier,
date: Date,
relativeTime: Boolean,
dateFormat: DateFormat,
) {
val context = LocalContext.current
ListGroupHeader(
modifier = modifier,
text = remember {
date.toRelativeString(
context,
relativeTime,
dateFormat,
)
},
)
}

View File

@ -14,8 +14,8 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -24,14 +24,14 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastForEachIndexed
import kotlinx.collections.immutable.ImmutableList import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.HorizontalPager import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.TabIndicator
import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.components.material.TabText
import tachiyomi.presentation.core.i18n.stringResource
object TabbedDialogPaddings { object TabbedDialogPaddings {
val Horizontal = 24.dp val Horizontal = 24.dp
@ -40,9 +40,9 @@ object TabbedDialogPaddings {
@Composable @Composable
fun TabbedDialog( fun TabbedDialog(
onDismissRequest: () -> Unit,
tabTitles: ImmutableList<String>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onDismissRequest: () -> Unit,
tabTitles: List<String>,
tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null, tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null,
pagerState: PagerState = rememberPagerState { tabTitles.size }, pagerState: PagerState = rememberPagerState { tabTitles.size },
content: @Composable (Int) -> Unit, content: @Composable (Int) -> Unit,
@ -55,9 +55,10 @@ fun TabbedDialog(
Column { Column {
Row { Row {
PrimaryTabRow( TabRow(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },
divider = {}, divider = {},
) { ) {
tabTitles.fastForEachIndexed { index, tab -> tabTitles.fastForEachIndexed { index, tab ->
@ -94,7 +95,7 @@ private fun MoreMenu(
IconButton(onClick = { expanded = true }) { IconButton(onClick = { expanded = true }) {
Icon( Icon(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(MR.strings.label_more), contentDescription = stringResource(R.string.label_more),
) )
} }
DropdownMenu( DropdownMenu(

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateEndPadding
@ -8,10 +9,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -19,20 +20,17 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.zIndex import androidx.compose.ui.res.stringResource
import dev.icerock.moko.resources.StringResource
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.HorizontalPager import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.TabIndicator
import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.components.material.TabText
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun TabbedScreen( fun TabbedScreen(
titleRes: StringResource, @StringRes titleRes: Int,
tabs: ImmutableList<TabContent>, tabs: List<TabContent>,
startIndex: Int? = null, startIndex: Int? = null,
searchQuery: String? = null, searchQuery: String? = null,
onChangeSearchQuery: (String?) -> Unit = {}, onChangeSearchQuery: (String?) -> Unit = {},
@ -69,9 +67,9 @@ fun TabbedScreen(
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
), ),
) { ) {
PrimaryTabRow( TabRow(
selectedTabIndex = state.currentPage, selectedTabIndex = state.currentPage,
modifier = Modifier.zIndex(1f), indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) },
) { ) {
tabs.forEachIndexed { index, tab -> tabs.forEachIndexed { index, tab ->
Tab( Tab(
@ -98,9 +96,9 @@ fun TabbedScreen(
} }
data class TabContent( data class TabContent(
val titleRes: StringResource, @StringRes val titleRes: Int,
val badgeNumber: Int? = null, val badgeNumber: Int? = null,
val searchEnabled: Boolean = false, val searchEnabled: Boolean = false,
val actions: ImmutableList<AppBar.AppBarAction> = persistentListOf(), val actions: List<AppBar.Action> = emptyList(),
val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit, val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit,
) )

View File

@ -2,7 +2,7 @@ package eu.kanade.presentation.crash
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.outlined.BugReport
@ -13,14 +13,14 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.InfoScreen import tachiyomi.presentation.core.screens.InfoScreen
import tachiyomi.presentation.core.util.ThemePreviews
@Composable @Composable
fun CrashScreen( fun CrashScreen(
@ -32,22 +32,22 @@ fun CrashScreen(
InfoScreen( InfoScreen(
icon = Icons.Outlined.BugReport, icon = Icons.Outlined.BugReport,
headingText = stringResource(MR.strings.crash_screen_title), headingText = stringResource(R.string.crash_screen_title),
subtitleText = stringResource(MR.strings.crash_screen_description, stringResource(MR.strings.app_name)), subtitleText = stringResource(R.string.crash_screen_description, stringResource(R.string.app_name)),
acceptText = stringResource(MR.strings.pref_dump_crash_logs), acceptText = stringResource(R.string.pref_dump_crash_logs),
onAcceptClick = { onAcceptClick = {
scope.launch { scope.launch {
CrashLogUtil(context).dumpLogs() CrashLogUtil(context).dumpLogs()
} }
}, },
rejectText = stringResource(MR.strings.crash_screen_restart_application), rejectText = stringResource(R.string.crash_screen_restart_application),
onRejectClick = onRestartClick, onRejectClick = onRestartClick,
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(vertical = MaterialTheme.padding.small) .padding(vertical = MaterialTheme.padding.small)
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.small)
.fillMaxSize() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant), .background(MaterialTheme.colorScheme.surfaceVariant),
) { ) {
Text( Text(
@ -60,10 +60,10 @@ fun CrashScreen(
} }
} }
@PreviewLightDark @ThemePreviews
@Composable @Composable
private fun CrashScreenPreview() { private fun CrashScreenPreview() {
TachiyomiPreviewTheme { TachiyomiTheme {
CrashScreen(exception = RuntimeException("Dummy")) {} CrashScreen(exception = RuntimeException("Dummy")) {}
} }
} }

View File

@ -8,26 +8,29 @@ import androidx.compose.material.icons.outlined.DeleteSweep
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import kotlinx.collections.immutable.persistentListOf import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
@Composable @Composable
@ -38,18 +41,19 @@ fun HistoryScreen(
onClickCover: (mangaId: Long) -> Unit, onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
SearchToolbar( SearchToolbar(
titleContent = { AppBarTitle(stringResource(MR.strings.history)) }, titleContent = { AppBarTitle(stringResource(R.string.history)) },
searchQuery = state.searchQuery, searchQuery = state.searchQuery,
onChangeSearchQuery = onSearchQueryChange, onChangeSearchQuery = onSearchQueryChange,
actions = { actions = {
AppBarActions( AppBarActions(
persistentListOf( listOf(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.pref_clear_history), title = stringResource(R.string.pref_clear_history),
icon = Icons.Outlined.DeleteSweep, icon = Icons.Outlined.DeleteSweep,
onClick = { onClick = {
onDialogChange(HistoryScreenModel.Dialog.DeleteAll) onDialogChange(HistoryScreenModel.Dialog.DeleteAll)
@ -68,12 +72,12 @@ fun HistoryScreen(
LoadingScreen(Modifier.padding(contentPadding)) LoadingScreen(Modifier.padding(contentPadding))
} else if (it.isEmpty()) { } else if (it.isEmpty()) {
val msg = if (!state.searchQuery.isNullOrEmpty()) { val msg = if (!state.searchQuery.isNullOrEmpty()) {
MR.strings.no_results_found R.string.no_results_found
} else { } else {
MR.strings.information_no_recent_manga R.string.information_no_recent_manga
} }
EmptyScreen( EmptyScreen(
stringRes = msg, textResource = msg,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
} else { } else {
@ -83,6 +87,7 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) }, onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
preferences = preferences,
) )
} }
} }
@ -96,7 +101,11 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit, onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit, onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit, onClickDelete: (HistoryWithRelations) -> Unit,
preferences: UiPreferences,
) { ) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
@ -112,9 +121,11 @@ private fun HistoryScreenContent(
) { item -> ) { item ->
when (item) { when (item) {
is HistoryUiModel.Header -> { is HistoryUiModel.Header -> {
ListGroupHeader( RelativeDateHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
text = relativeDateText(item.date), date = item.date,
relativeTime = relativeTime,
dateFormat = dateFormat,
) )
} }
is HistoryUiModel.Item -> { is HistoryUiModel.Item -> {
@ -137,13 +148,13 @@ sealed interface HistoryUiModel {
data class Item(val item: HistoryWithRelations) : HistoryUiModel data class Item(val item: HistoryWithRelations) : HistoryUiModel
} }
@PreviewLightDark @ThemePreviews
@Composable @Composable
internal fun HistoryScreenPreviews( internal fun HistoryScreenPreviews(
@PreviewParameter(HistoryScreenModelStateProvider::class) @PreviewParameter(HistoryScreenModelStateProvider::class)
historyState: HistoryScreenModel.State, historyState: HistoryScreenModel.State,
) { ) {
TachiyomiPreviewTheme { TachiyomiTheme {
HistoryScreen( HistoryScreen(
state = historyState, state = historyState,
snackbarHostState = SnackbarHostState(), snackbarHostState = SnackbarHostState(),
@ -151,6 +162,17 @@ internal fun HistoryScreenPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = { _, _ -> run {} }, onClickResume = { _, _ -> run {} },
onDialogChange = {}, onDialogChange = {},
preferences = UiPreferences(
InMemoryPreferenceStore(
sequenceOf(
InMemoryPreferenceStore.InMemoryPreference(
key = "relative_time_v2",
data = false,
defaultValue = false,
),
),
),
),
) )
} }
} }

View File

@ -0,0 +1,13 @@
package eu.kanade.presentation.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import java.time.Instant
import java.util.Date
object HistoryUiModelProviders {
class HeadNow : PreviewParameterProvider<HistoryUiModel> {
override val values: Sequence<HistoryUiModel> =
sequenceOf(HistoryUiModel.Header(Date.from(Instant.now())))
}
}

View File

@ -3,7 +3,6 @@ package eu.kanade.presentation.history.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -11,12 +10,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun HistoryDeleteDialog( fun HistoryDeleteDialog(
@ -27,16 +26,16 @@ fun HistoryDeleteDialog(
AlertDialog( AlertDialog(
title = { title = {
Text(text = stringResource(MR.strings.action_remove)) Text(text = stringResource(R.string.action_remove))
}, },
text = { text = {
Column( Column(
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
Text(text = stringResource(MR.strings.dialog_with_checkbox_remove_description)) Text(text = stringResource(R.string.dialog_with_checkbox_remove_description))
LabeledCheckbox( LabeledCheckbox(
label = stringResource(MR.strings.dialog_with_checkbox_reset), label = stringResource(R.string.dialog_with_checkbox_reset),
checked = removeEverything, checked = removeEverything,
onCheckedChange = { removeEverything = it }, onCheckedChange = { removeEverything = it },
) )
@ -48,12 +47,12 @@ fun HistoryDeleteDialog(
onDelete(removeEverything) onDelete(removeEverything)
onDismissRequest() onDismissRequest()
}) { }) {
Text(text = stringResource(MR.strings.action_remove)) Text(text = stringResource(R.string.action_remove))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
) )
@ -66,10 +65,10 @@ fun HistoryDeleteAllDialog(
) { ) {
AlertDialog( AlertDialog(
title = { title = {
Text(text = stringResource(MR.strings.action_remove_everything)) Text(text = stringResource(R.string.action_remove_everything))
}, },
text = { text = {
Text(text = stringResource(MR.strings.clear_history_confirmation)) Text(text = stringResource(R.string.clear_history_confirmation))
}, },
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
confirmButton = { confirmButton = {
@ -77,21 +76,21 @@ fun HistoryDeleteAllDialog(
onDelete() onDelete()
onDismissRequest() onDismissRequest()
}) { }) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
) )
} }
@PreviewLightDark @ThemePreviews
@Composable @Composable
private fun HistoryDeleteDialogPreview() { private fun HistoryDeleteDialogPreview() {
TachiyomiPreviewTheme { TachiyomiTheme {
HistoryDeleteDialog( HistoryDeleteDialog(
onDismissRequest = {}, onDismissRequest = {},
onDelete = {}, onDelete = {},

View File

@ -11,35 +11,34 @@ import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.formatChapterNumber import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.toTimestampString import eu.kanade.tachiyomi.util.lang.toTimestampString
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.ThemePreviews
private val HistoryItemHeight = 96.dp private val HistoryItemHeight = 96.dp
@Composable @Composable
fun HistoryItem( fun HistoryItem(
modifier: Modifier = Modifier,
history: HistoryWithRelations, history: HistoryWithRelations,
onClickCover: () -> Unit, onClickCover: () -> Unit,
onClickResume: () -> Unit, onClickResume: () -> Unit,
onClickDelete: () -> Unit, onClickDelete: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier modifier = modifier
@ -70,7 +69,7 @@ fun HistoryItem(
Text( Text(
text = if (history.chapterNumber > -1) { text = if (history.chapterNumber > -1) {
stringResource( stringResource(
MR.strings.recent_manga_time, R.string.recent_manga_time,
formatChapterNumber(history.chapterNumber), formatChapterNumber(history.chapterNumber),
readAt, readAt,
) )
@ -85,21 +84,20 @@ fun HistoryItem(
IconButton(onClick = onClickDelete) { IconButton(onClick = onClickDelete) {
Icon( Icon(
imageVector = Icons.Outlined.Delete, imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(MR.strings.action_delete), contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }
} }
} }
@PreviewLightDark @ThemePreviews
@Composable @Composable
private fun HistoryItemPreviews( private fun HistoryItemPreviews(
@PreviewParameter(HistoryWithRelationsProvider::class) @PreviewParameter(HistoryWithRelationsProvider::class)
historyWithRelations: HistoryWithRelations, historyWithRelations: HistoryWithRelations,
) { ) {
TachiyomiPreviewTheme { TachiyomiTheme {
Surface {
HistoryItem( HistoryItem(
history = historyWithRelations, history = historyWithRelations,
onClickCover = {}, onClickCover = {},
@ -108,4 +106,3 @@ private fun HistoryItemPreviews(
) )
} }
} }
}

View File

@ -9,11 +9,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.icerock.moko.resources.StringResource import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.CheckboxState import tachiyomi.core.preference.CheckboxState
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun DeleteLibraryMangaDialog( fun DeleteLibraryMangaDialog(
@ -23,10 +22,10 @@ fun DeleteLibraryMangaDialog(
) { ) {
var list by remember { var list by remember {
mutableStateOf( mutableStateOf(
buildList<CheckboxState.State<StringResource>> { buildList<CheckboxState.State<Int>> {
add(CheckboxState.State.None(MR.strings.manga_from_library)) add(CheckboxState.State.None(R.string.manga_from_library))
if (!containsLocalManga) { if (!containsLocalManga) {
add(CheckboxState.State.None(MR.strings.downloaded_chapters)) add(CheckboxState.State.None(R.string.downloaded_chapters))
} }
}, },
) )
@ -35,7 +34,7 @@ fun DeleteLibraryMangaDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
confirmButton = { confirmButton = {
@ -49,11 +48,11 @@ fun DeleteLibraryMangaDialog(
) )
}, },
) { ) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
title = { title = {
Text(text = stringResource(MR.strings.action_remove)) Text(text = stringResource(R.string.action_remove))
}, },
text = { text = {
Column { Column {
@ -65,7 +64,7 @@ fun DeleteLibraryMangaDialog(
val index = list.indexOf(state) val index = list.indexOf(state)
if (index != -1) { if (index != -1) {
val mutableList = list.toMutableList() val mutableList = list.toMutableList()
mutableList[index] = state.next() as CheckboxState.State<StringResource> mutableList[index] = state.next() as CheckboxState.State<Int>
list = mutableList.toList() list = mutableList.toList()
} }
}, },

View File

@ -13,26 +13,23 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibrarySort import tachiyomi.domain.library.model.LibrarySort
import tachiyomi.domain.library.model.sort import tachiyomi.domain.library.model.sort
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.SettingsChipRow import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
@Composable @Composable
@ -43,10 +40,10 @@ fun LibrarySettingsDialog(
) { ) {
TabbedDialog( TabbedDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
tabTitles = persistentListOf( tabTitles = listOf(
stringResource(MR.strings.action_filter), stringResource(R.string.action_filter),
stringResource(MR.strings.action_sort), stringResource(R.string.action_sort),
stringResource(MR.strings.action_display), stringResource(R.string.action_display),
), ),
) { page -> ) { page ->
Column( Column(
@ -76,10 +73,8 @@ private fun ColumnScope.FilterPage(
) { ) {
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState() val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState() val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateMangaRestrictions().collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_downloaded), label = stringResource(R.string.label_downloaded),
state = if (downloadedOnly) { state = if (downloadedOnly) {
TriState.ENABLED_IS TriState.ENABLED_IS
} else { } else {
@ -90,40 +85,28 @@ private fun ColumnScope.FilterPage(
) )
val filterUnread by screenModel.libraryPreferences.filterUnread().collectAsState() val filterUnread by screenModel.libraryPreferences.filterUnread().collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_unread), label = stringResource(R.string.action_filter_unread),
state = filterUnread, state = filterUnread,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterUnread) }, onClick = { screenModel.toggleFilter(LibraryPreferences::filterUnread) },
) )
val filterStarted by screenModel.libraryPreferences.filterStarted().collectAsState() val filterStarted by screenModel.libraryPreferences.filterStarted().collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_started), label = stringResource(R.string.label_started),
state = filterStarted, state = filterStarted,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterStarted) }, onClick = { screenModel.toggleFilter(LibraryPreferences::filterStarted) },
) )
val filterBookmarked by screenModel.libraryPreferences.filterBookmarked().collectAsState() val filterBookmarked by screenModel.libraryPreferences.filterBookmarked().collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_bookmarked), label = stringResource(R.string.action_filter_bookmarked),
state = filterBookmarked, state = filterBookmarked,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterBookmarked) }, onClick = { screenModel.toggleFilter(LibraryPreferences::filterBookmarked) },
) )
val filterCompleted by screenModel.libraryPreferences.filterCompleted().collectAsState() val filterCompleted by screenModel.libraryPreferences.filterCompleted().collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.completed), label = stringResource(R.string.completed),
state = filterCompleted, state = filterCompleted,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) }, onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
) )
// TODO: re-enable when custom intervals are ready for stable
if (
(isDevFlavor || isPreviewBuildType) &&
LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions
) {
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_interval_custom),
state = filterIntervalCustom,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
)
}
val trackers = remember { screenModel.trackers } val trackers = remember { screenModel.trackers }
when (trackers.size) { when (trackers.size) {
@ -134,13 +117,13 @@ private fun ColumnScope.FilterPage(
val service = trackers[0] val service = trackers[0]
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState() val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_tracked), label = stringResource(R.string.action_filter_tracked),
state = filterTracker, state = filterTracker,
onClick = { screenModel.toggleTracker(service.id.toInt()) }, onClick = { screenModel.toggleTracker(service.id.toInt()) },
) )
} }
else -> { else -> {
HeadingItem(MR.strings.action_filter_tracked) HeadingItem(R.string.action_filter_tracked)
trackers.map { service -> trackers.map { service ->
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState() val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem( TriStateItem(
@ -165,18 +148,18 @@ private fun ColumnScope.SortPage(
if (screenModel.trackers.isEmpty()) { if (screenModel.trackers.isEmpty()) {
emptyList() emptyList()
} else { } else {
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean) listOf(R.string.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
} }
listOf( listOf(
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical, R.string.action_sort_alpha to LibrarySort.Type.Alphabetical,
MR.strings.action_sort_total to LibrarySort.Type.TotalChapters, R.string.action_sort_total to LibrarySort.Type.TotalChapters,
MR.strings.action_sort_last_read to LibrarySort.Type.LastRead, R.string.action_sort_last_read to LibrarySort.Type.LastRead,
MR.strings.action_sort_last_manga_update to LibrarySort.Type.LastUpdate, R.string.action_sort_last_manga_update to LibrarySort.Type.LastUpdate,
MR.strings.action_sort_unread_count to LibrarySort.Type.UnreadCount, R.string.action_sort_unread_count to LibrarySort.Type.UnreadCount,
MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter, R.string.action_sort_latest_chapter to LibrarySort.Type.LatestChapter,
MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate, R.string.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded, R.string.action_sort_date_added to LibrarySort.Type.DateAdded,
).plus(trackerSortOption).map { (titleRes, mode) -> ).plus(trackerSortOption).map { (titleRes, mode) ->
SortItem( SortItem(
label = stringResource(titleRes), label = stringResource(titleRes),
@ -184,16 +167,8 @@ private fun ColumnScope.SortPage(
onClick = { onClick = {
val isTogglingDirection = sortingMode == mode val isTogglingDirection = sortingMode == mode
val direction = when { val direction = when {
isTogglingDirection -> if (sortDescending) { isTogglingDirection -> if (sortDescending) LibrarySort.Direction.Ascending else LibrarySort.Direction.Descending
LibrarySort.Direction.Ascending else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending
} else {
LibrarySort.Direction.Descending
}
else -> if (sortDescending) {
LibrarySort.Direction.Descending
} else {
LibrarySort.Direction.Ascending
}
} }
screenModel.setSort(category, mode, direction) screenModel.setSort(category, mode, direction)
}, },
@ -202,10 +177,10 @@ private fun ColumnScope.SortPage(
} }
private val displayModes = listOf( private val displayModes = listOf(
MR.strings.action_display_grid to LibraryDisplayMode.CompactGrid, R.string.action_display_grid to LibraryDisplayMode.CompactGrid,
MR.strings.action_display_comfortable_grid to LibraryDisplayMode.ComfortableGrid, R.string.action_display_comfortable_grid to LibraryDisplayMode.ComfortableGrid,
MR.strings.action_display_cover_only_grid to LibraryDisplayMode.CoverOnlyGrid, R.string.action_display_cover_only_grid to LibraryDisplayMode.CoverOnlyGrid,
MR.strings.action_display_list to LibraryDisplayMode.List, R.string.action_display_list to LibraryDisplayMode.List,
) )
@Composable @Composable
@ -213,7 +188,7 @@ private fun ColumnScope.DisplayPage(
screenModel: LibrarySettingsScreenModel, screenModel: LibrarySettingsScreenModel,
) { ) {
val displayMode by screenModel.libraryPreferences.displayMode().collectAsState() val displayMode by screenModel.libraryPreferences.displayMode().collectAsState()
SettingsChipRow(MR.strings.action_display_mode) { SettingsChipRow(R.string.action_display_mode) {
displayModes.map { (titleRes, mode) -> displayModes.map { (titleRes, mode) ->
FilterChip( FilterChip(
selected = displayMode == mode, selected = displayMode == mode,
@ -235,43 +210,43 @@ private fun ColumnScope.DisplayPage(
val columns by columnPreference.collectAsState() val columns by columnPreference.collectAsState()
SliderItem( SliderItem(
label = stringResource(MR.strings.pref_library_columns), label = stringResource(R.string.pref_library_columns),
max = 10, max = 10,
value = columns, value = columns,
valueText = if (columns > 0) { valueText = if (columns > 0) {
stringResource(MR.strings.pref_library_columns_per_row, columns) stringResource(R.string.pref_library_columns_per_row, columns)
} else { } else {
stringResource(MR.strings.label_default) stringResource(R.string.label_default)
}, },
onChange = columnPreference::set, onChange = columnPreference::set,
) )
} }
HeadingItem(MR.strings.overlay_header) HeadingItem(R.string.overlay_header)
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_download_badge), label = stringResource(R.string.action_display_download_badge),
pref = screenModel.libraryPreferences.downloadBadge(), pref = screenModel.libraryPreferences.downloadBadge(),
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_local_badge), label = stringResource(R.string.action_display_local_badge),
pref = screenModel.libraryPreferences.localBadge(), pref = screenModel.libraryPreferences.localBadge(),
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_language_badge), label = stringResource(R.string.action_display_language_badge),
pref = screenModel.libraryPreferences.languageBadge(), pref = screenModel.libraryPreferences.languageBadge(),
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_show_continue_reading_button), label = stringResource(R.string.action_display_show_continue_reading_button),
pref = screenModel.libraryPreferences.showContinueReadingButton(), pref = screenModel.libraryPreferences.showContinueReadingButton(),
) )
HeadingItem(MR.strings.tabs_header) HeadingItem(R.string.tabs_header)
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_show_tabs), label = stringResource(R.string.action_display_show_tabs),
pref = screenModel.libraryPreferences.categoryTabs(), pref = screenModel.libraryPreferences.categoryTabs(),
) )
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_show_number_of_items), label = stringResource(R.string.action_display_show_number_of_items),
pref = screenModel.libraryPreferences.categoryNumberOfItems(), pref = screenModel.libraryPreferences.categoryNumberOfItems(),
) )
} }

View File

@ -38,9 +38,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.manga.components.MangaCover
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.BadgeGroup import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
object CommonMangaItemDefaults { object CommonMangaItemDefaults {
@ -50,7 +48,7 @@ object CommonMangaItemDefaults {
const val BrowseFavoriteCoverAlpha = 0.34f const val BrowseFavoriteCoverAlpha = 0.34f
} }
private val ContinueReadingButtonSize = 28.dp private val ContinueReadingButtonSize = 32.dp
private val ContinueReadingButtonGridPadding = 6.dp private val ContinueReadingButtonGridPadding = 6.dp
private val ContinueReadingButtonListSpacing = 8.dp private val ContinueReadingButtonListSpacing = 8.dp
@ -62,15 +60,15 @@ private const val GridSelectedCoverAlpha = 0.76f
*/ */
@Composable @Composable
fun MangaCompactGridItem( fun MangaCompactGridItem(
coverData: tachiyomi.domain.manga.model.MangaCover,
onClick: () -> Unit,
onLongClick: () -> Unit,
isSelected: Boolean = false, isSelected: Boolean = false,
title: String? = null, title: String? = null,
onClickContinueReading: (() -> Unit)? = null, coverData: tachiyomi.domain.manga.model.MangaCover,
coverAlpha: Float = 1f, coverAlpha: Float = 1f,
coverBadgeStart: @Composable (RowScope.() -> Unit)? = null, coverBadgeStart: @Composable (RowScope.() -> Unit)? = null,
coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null, coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null,
onLongClick: () -> Unit,
onClick: () -> Unit,
onClickContinueReading: (() -> Unit)? = null,
) { ) {
GridItemSelectable( GridItemSelectable(
isSelected = isSelected, isSelected = isSelected,
@ -163,15 +161,15 @@ private fun BoxScope.CoverTextOverlay(
*/ */
@Composable @Composable
fun MangaComfortableGridItem( fun MangaComfortableGridItem(
coverData: tachiyomi.domain.manga.model.MangaCover,
title: String,
onClick: () -> Unit,
onLongClick: () -> Unit,
isSelected: Boolean = false, isSelected: Boolean = false,
title: String,
titleMaxLines: Int = 2, titleMaxLines: Int = 2,
coverData: tachiyomi.domain.manga.model.MangaCover,
coverAlpha: Float = 1f, coverAlpha: Float = 1f,
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null, coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null, coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
onLongClick: () -> Unit,
onClick: () -> Unit,
onClickContinueReading: (() -> Unit)? = null, onClickContinueReading: (() -> Unit)? = null,
) { ) {
GridItemSelectable( GridItemSelectable(
@ -253,10 +251,10 @@ private fun MangaGridCover(
@Composable @Composable
private fun GridItemTitle( private fun GridItemTitle(
modifier: Modifier,
title: String, title: String,
style: TextStyle, style: TextStyle,
minLines: Int, minLines: Int,
modifier: Modifier = Modifier,
maxLines: Int = 2, maxLines: Int = 2,
) { ) {
Text( Text(
@ -276,10 +274,10 @@ private fun GridItemTitle(
*/ */
@Composable @Composable
private fun GridItemSelectable( private fun GridItemSelectable(
modifier: Modifier = Modifier,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
Box( Box(
@ -316,13 +314,13 @@ private fun Modifier.selectedOutline(
*/ */
@Composable @Composable
fun MangaListItem( fun MangaListItem(
coverData: tachiyomi.domain.manga.model.MangaCover,
title: String,
onClick: () -> Unit,
onLongClick: () -> Unit,
badge: @Composable (RowScope.() -> Unit),
isSelected: Boolean = false, isSelected: Boolean = false,
title: String,
coverData: tachiyomi.domain.manga.model.MangaCover,
coverAlpha: Float = 1f, coverAlpha: Float = 1f,
badge: @Composable (RowScope.() -> Unit),
onLongClick: () -> Unit,
onClick: () -> Unit,
onClickContinueReading: (() -> Unit)? = null, onClickContinueReading: (() -> Unit)? = null,
) { ) {
Row( Row(
@ -378,7 +376,7 @@ private fun ContinueReadingButton(
) { ) {
Icon( Icon(
imageVector = Icons.Filled.PlayArrow, imageVector = Icons.Filled.PlayArrow,
contentDescription = stringResource(MR.strings.action_resume), contentDescription = "",
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
) )
} }

View File

@ -4,9 +4,9 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import tachiyomi.i18n.MR import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
internal fun GlobalSearchItem( internal fun GlobalSearchItem(
@ -19,7 +19,7 @@ internal fun GlobalSearchItem(
onClick = onClick, onClick = onClick,
) { ) {
Text( Text(
text = stringResource(MR.strings.action_global_search_query, searchQuery), text = stringResource(R.string.action_global_search_query, searchQuery),
modifier = Modifier.zIndex(99f), modifier = Modifier.zIndex(99f),
) )
} }

View File

@ -5,9 +5,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Folder import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewLightDark import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import tachiyomi.presentation.core.components.Badge import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.util.ThemePreviews
@Composable @Composable
internal fun DownloadsBadge(count: Long) { internal fun DownloadsBadge(count: Long) {
@ -47,10 +47,10 @@ internal fun LanguageBadge(
} }
} }
@PreviewLightDark @ThemePreviews
@Composable @Composable
private fun BadgePreview() { private fun BadgePreview() {
TachiyomiPreviewTheme { TachiyomiTheme {
Column { Column {
DownloadsBadge(count = 10) DownloadsBadge(count = 10)
UnreadBadge(count = 10) UnreadBadge(count = 10)

View File

@ -93,7 +93,7 @@ fun LibraryContent(
isRefreshing = false isRefreshing = false
} }
}, },
enabled = { notSelectionMode }, enabled = notSelectionMode,
) { ) {
LibraryPager( LibraryPager(
state = pagerState, state = pagerState,

View File

@ -18,10 +18,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.core.preference.PreferenceMutableState import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.HorizontalPager import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.plus
@ -124,9 +124,9 @@ private fun LibraryPagerEmptyScreen(
onGlobalSearchClicked: () -> Unit, onGlobalSearchClicked: () -> Unit,
) { ) {
val msg = when { val msg = when {
!searchQuery.isNullOrEmpty() -> MR.strings.no_results_found !searchQuery.isNullOrEmpty() -> R.string.no_results_found
hasActiveFilters -> MR.strings.error_no_match hasActiveFilters -> R.string.error_no_match
else -> MR.strings.information_no_manga_category else -> R.string.information_no_manga_category
} }
Column( Column(
@ -146,7 +146,7 @@ private fun LibraryPagerEmptyScreen(
} }
EmptyScreen( EmptyScreen(
stringRes = msg, textResource = msg,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
} }

View File

@ -4,14 +4,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.presentation.core.components.material.TabIndicator
import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.components.material.TabText
@Composable @Composable
@ -21,12 +20,11 @@ internal fun LibraryTabs(
getNumberOfMangaForCategory: (Category) -> Int?, getNumberOfMangaForCategory: (Category) -> Int?,
onTabItemClick: (Int) -> Unit, onTabItemClick: (Int) -> Unit,
) { ) {
Column( Column {
modifier = Modifier.zIndex(1f), ScrollableTabRow(
) {
PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp, edgePadding = 0.dp,
indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },
// TODO: use default when width is fixed upstream // TODO: use default when width is fixed upstream
// https://issuetracker.google.com/issues/242879624 // https://issuetracker.google.com/issues/242879624
divider = {}, divider = {},

View File

@ -14,15 +14,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import kotlinx.collections.immutable.persistentListOf import eu.kanade.tachiyomi.R
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Pill import tachiyomi.presentation.core.components.Pill
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.active import tachiyomi.presentation.core.theme.active
@Composable @Composable
@ -96,23 +95,23 @@ private fun LibraryRegularToolbar(
actions = { actions = {
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
AppBarActions( AppBarActions(
persistentListOf( listOf(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_filter), title = stringResource(R.string.action_filter),
icon = Icons.Outlined.FilterList, icon = Icons.Outlined.FilterList,
iconTint = filterTint, iconTint = filterTint,
onClick = onClickFilter, onClick = onClickFilter,
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_update_library), title = stringResource(R.string.action_update_library),
onClick = onClickGlobalUpdate, onClick = onClickGlobalUpdate,
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_update_category), title = stringResource(R.string.action_update_category),
onClick = onClickRefresh, onClick = onClickRefresh,
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_random_manga), title = stringResource(R.string.action_open_random_manga),
onClick = onClickOpenRandomManga, onClick = onClickOpenRandomManga,
), ),
), ),
@ -133,14 +132,14 @@ private fun LibrarySelectionToolbar(
titleContent = { Text(text = "$selectedCount") }, titleContent = { Text(text = "$selectedCount") },
actions = { actions = {
AppBarActions( AppBarActions(
persistentListOf( listOf(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_all), title = stringResource(R.string.action_select_all),
icon = Icons.Outlined.SelectAll, icon = Icons.Outlined.SelectAll,
onClick = onClickSelectAll, onClick = onClickSelectAll,
), ),
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_inverse), title = stringResource(R.string.action_select_inverse),
icon = Icons.Outlined.FlipToBack, icon = Icons.Outlined.FlipToBack,
onClick = onClickInvertSelection, onClick = onClickInvertSelection,
), ),

View File

@ -1,21 +1,13 @@
package eu.kanade.presentation.manga package eu.kanade.presentation.manga
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PeopleAlt
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -23,23 +15,20 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.downloadedFilter import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.domain.manga.model.forceDownloaded import eu.kanade.domain.manga.model.forceDownloaded
import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import kotlinx.collections.immutable.persistentListOf import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.RadioItem import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.active
@Composable @Composable
fun ChapterSettingsDialog( fun ChapterSettingsDialog(
@ -48,8 +37,6 @@ fun ChapterSettingsDialog(
onDownloadFilterChanged: (TriState) -> Unit, onDownloadFilterChanged: (TriState) -> Unit,
onUnreadFilterChanged: (TriState) -> Unit, onUnreadFilterChanged: (TriState) -> Unit,
onBookmarkedFilterChanged: (TriState) -> Unit, onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
onSortModeChanged: (Long) -> Unit, onSortModeChanged: (Long) -> Unit,
onDisplayModeChanged: (Long) -> Unit, onDisplayModeChanged: (Long) -> Unit,
onSetAsDefault: (applyToExistingManga: Boolean) -> Unit, onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
@ -65,21 +52,21 @@ fun ChapterSettingsDialog(
TabbedDialog( TabbedDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
tabTitles = persistentListOf( tabTitles = listOf(
stringResource(MR.strings.action_filter), stringResource(R.string.action_filter),
stringResource(MR.strings.action_sort), stringResource(R.string.action_sort),
stringResource(MR.strings.action_display), stringResource(R.string.action_display),
), ),
tabOverflowMenuContent = { closeMenu -> tabOverflowMenuContent = { closeMenu ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(MR.strings.set_chapter_settings_as_default)) }, text = { Text(stringResource(R.string.set_chapter_settings_as_default)) },
onClick = { onClick = {
showSetAsDefaultDialog = true showSetAsDefaultDialog = true
closeMenu() closeMenu()
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_reset)) }, text = { Text(stringResource(R.string.action_reset)) },
onClick = { onClick = {
onResetToDefault() onResetToDefault()
closeMenu() closeMenu()
@ -96,14 +83,11 @@ fun ChapterSettingsDialog(
0 -> { 0 -> {
FilterPage( FilterPage(
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED, downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { manga?.forceDownloaded() == true },
.takeUnless { manga?.forceDownloaded() == true },
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED, unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
onUnreadFilterChanged = onUnreadFilterChanged, onUnreadFilterChanged = onUnreadFilterChanged,
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED, bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
onBookmarkedFilterChanged = onBookmarkedFilterChanged, onBookmarkedFilterChanged = onBookmarkedFilterChanged,
scanlatorFilterActive = scanlatorFilterActive,
onScanlatorFilterClicked = onScanlatorFilterClicked,
) )
} }
1 -> { 1 -> {
@ -132,57 +116,22 @@ private fun ColumnScope.FilterPage(
onUnreadFilterChanged: (TriState) -> Unit, onUnreadFilterChanged: (TriState) -> Unit,
bookmarkedFilter: TriState, bookmarkedFilter: TriState,
onBookmarkedFilterChanged: (TriState) -> Unit, onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
) { ) {
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_downloaded), label = stringResource(R.string.label_downloaded),
state = downloadFilter, state = downloadFilter,
onClick = onDownloadFilterChanged, onClick = onDownloadFilterChanged,
) )
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_unread), label = stringResource(R.string.action_filter_unread),
state = unreadFilter, state = unreadFilter,
onClick = onUnreadFilterChanged, onClick = onUnreadFilterChanged,
) )
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_bookmarked), label = stringResource(R.string.action_filter_bookmarked),
state = bookmarkedFilter, state = bookmarkedFilter,
onClick = onBookmarkedFilterChanged, onClick = onBookmarkedFilterChanged,
) )
ScanlatorFilterItem(
active = scanlatorFilterActive,
onClick = onScanlatorFilterClicked,
)
}
@Composable
fun ScanlatorFilterItem(
active: Boolean,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.clickable(onClick = onClick)
.fillMaxWidth()
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
Icon(
imageVector = Icons.Outlined.PeopleAlt,
contentDescription = null,
tint = if (active) {
MaterialTheme.colorScheme.active
} else {
LocalContentColor.current
},
)
Text(
text = stringResource(MR.strings.scanlator),
style = MaterialTheme.typography.bodyMedium,
)
}
} }
@Composable @Composable
@ -192,10 +141,10 @@ private fun ColumnScope.SortPage(
onItemSelected: (Long) -> Unit, onItemSelected: (Long) -> Unit,
) { ) {
listOf( listOf(
MR.strings.sort_by_source to Manga.CHAPTER_SORTING_SOURCE, R.string.sort_by_source to Manga.CHAPTER_SORTING_SOURCE,
MR.strings.sort_by_number to Manga.CHAPTER_SORTING_NUMBER, R.string.sort_by_number to Manga.CHAPTER_SORTING_NUMBER,
MR.strings.sort_by_upload_date to Manga.CHAPTER_SORTING_UPLOAD_DATE, R.string.sort_by_upload_date to Manga.CHAPTER_SORTING_UPLOAD_DATE,
MR.strings.action_sort_alpha to Manga.CHAPTER_SORTING_ALPHABET, R.string.action_sort_alpha to Manga.CHAPTER_SORTING_ALPHABET,
).map { (titleRes, mode) -> ).map { (titleRes, mode) ->
SortItem( SortItem(
label = stringResource(titleRes), label = stringResource(titleRes),
@ -211,8 +160,8 @@ private fun ColumnScope.DisplayPage(
onItemSelected: (Long) -> Unit, onItemSelected: (Long) -> Unit,
) { ) {
listOf( listOf(
MR.strings.show_title to Manga.CHAPTER_DISPLAY_NAME, R.string.show_title to Manga.CHAPTER_DISPLAY_NAME,
MR.strings.show_chapter_number to Manga.CHAPTER_DISPLAY_NUMBER, R.string.show_chapter_number to Manga.CHAPTER_DISPLAY_NUMBER,
).map { (titleRes, mode) -> ).map { (titleRes, mode) ->
RadioItem( RadioItem(
label = stringResource(titleRes), label = stringResource(titleRes),
@ -231,15 +180,15 @@ private fun SetAsDefaultDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.chapter_settings)) }, title = { Text(text = stringResource(R.string.chapter_settings)) },
text = { text = {
Column( Column(
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
Text(text = stringResource(MR.strings.confirm_set_chapter_settings)) Text(text = stringResource(R.string.confirm_set_chapter_settings))
LabeledCheckbox( LabeledCheckbox(
label = stringResource(MR.strings.also_set_chapter_settings_for_library), label = stringResource(R.string.also_set_chapter_settings_for_library),
checked = optionalChecked, checked = optionalChecked,
onCheckedChange = { optionalChecked = it }, onCheckedChange = { optionalChecked = it },
) )
@ -247,7 +196,7 @@ private fun SetAsDefaultDialog(
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
confirmButton = { confirmButton = {
@ -257,7 +206,7 @@ private fun SetAsDefaultDialog(
onDismissRequest() onDismissRequest()
}, },
) { ) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
) )

View File

@ -4,14 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR import androidx.compose.ui.res.stringResource
import tachiyomi.presentation.core.components.material.padding import androidx.compose.ui.unit.dp
import tachiyomi.presentation.core.i18n.stringResource import eu.kanade.tachiyomi.R
@Composable @Composable
fun DuplicateMangaDialog( fun DuplicateMangaDialog(
@ -22,14 +21,14 @@ fun DuplicateMangaDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { title = {
Text(text = stringResource(MR.strings.are_you_sure)) Text(text = stringResource(R.string.are_you_sure))
}, },
text = { text = {
Text(text = stringResource(MR.strings.confirm_add_duplicate_manga)) Text(text = stringResource(R.string.confirm_add_duplicate_manga))
}, },
confirmButton = { confirmButton = {
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
TextButton( TextButton(
onClick = { onClick = {
@ -37,13 +36,13 @@ fun DuplicateMangaDialog(
onOpenManga() onOpenManga()
}, },
) { ) {
Text(text = stringResource(MR.strings.action_show_manga)) Text(text = stringResource(R.string.action_show_manga))
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
TextButton( TextButton(
onClick = { onClick = {
@ -51,7 +50,7 @@ fun DuplicateMangaDialog(
onConfirm() onConfirm()
}, },
) { ) {
Text(text = stringResource(MR.strings.action_add)) Text(text = stringResource(R.string.action_add))
} }
} }
}, },

View File

@ -5,9 +5,11 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
@ -26,7 +28,9 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -44,10 +48,12 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import eu.kanade.presentation.components.relativeDateText import eu.kanade.domain.manga.model.chaptersFiltered
import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterHeader import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription import eu.kanade.presentation.manga.components.ExpandableMangaDescription
@ -56,35 +62,38 @@ import eu.kanade.presentation.manga.components.MangaBottomActionMenu
import eu.kanade.presentation.manga.components.MangaChapterListItem import eu.kanade.presentation.manga.components.MangaChapterListItem
import eu.kanade.presentation.manga.components.MangaInfoBox import eu.kanade.presentation.manga.components.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaToolbar import eu.kanade.presentation.manga.components.MangaToolbar
import eu.kanade.presentation.manga.components.MissingChapterCountListItem
import eu.kanade.presentation.util.formatChapterNumber import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.manga.ChapterList import eu.kanade.tachiyomi.ui.manga.ChapterList
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.missingChaptersCount import tachiyomi.domain.chapter.service.missingChaptersCount
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.TwoPanelBox import tachiyomi.presentation.core.components.TwoPanelBox
import tachiyomi.presentation.core.components.VerticalFastScroller import tachiyomi.presentation.core.components.VerticalFastScroller
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.source.local.isLocal import tachiyomi.presentation.core.util.secondaryItemAlpha
import java.time.Instant import java.text.DateFormat
import java.util.Date
@Composable @Composable
fun MangaScreen( fun MangaScreen(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
nextUpdate: Instant?, fetchInterval: Int?,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
isTabletUi: Boolean, isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -94,7 +103,7 @@ fun MangaScreen(
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: () -> Unit, onTrackingClicked: (() -> Unit)?,
// For tags menu // For tags menu
onTagSearch: (String) -> Unit, onTagSearch: (String) -> Unit,
@ -139,7 +148,9 @@ fun MangaScreen(
MangaScreenSmallImpl( MangaScreenSmallImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
nextUpdate = nextUpdate, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
@ -174,9 +185,11 @@ fun MangaScreen(
MangaScreenLargeImpl( MangaScreenLargeImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
nextUpdate = nextUpdate, dateFormat = dateFormat,
fetchInterval = fetchInterval,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
@ -212,7 +225,9 @@ fun MangaScreen(
private fun MangaScreenSmallImpl( private fun MangaScreenSmallImpl(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
nextUpdate: Instant?, dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
@ -221,7 +236,7 @@ private fun MangaScreenSmallImpl(
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: () -> Unit, onTrackingClicked: (() -> Unit)?,
// For tags menu // For tags menu
onTagSearch: (String) -> Unit, onTagSearch: (String) -> Unit,
@ -258,12 +273,13 @@ private fun MangaScreenSmallImpl(
) { ) {
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
val (chapters, listItem, isAnySelected) = remember(state) { val chapters = remember(state) { state.processedChapters }
Triple( val listItem = remember(state) { state.chapterListItems }
first = state.processedChapters,
second = state.chapterListItems, val isAnySelected by remember {
third = state.isAnySelected, derivedStateOf {
) chapters.fastAny { it.selected }
}
} }
val internalOnBackPressed = { val internalOnBackPressed = {
@ -298,7 +314,7 @@ private fun MangaScreenSmallImpl(
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha }, titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha }, backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.filterActive, hasFilters = state.manga.chaptersFiltered(),
onBackClicked = internalOnBackPressed, onBackClicked = internalOnBackPressed,
onClickFilter = onFilterClicked, onClickFilter = onFilterClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
@ -340,9 +356,7 @@ private fun MangaScreenSmallImpl(
val isReading = remember(state.chapters) { val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read } state.chapters.fastAny { it.chapter.read }
} }
Text( Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
)
}, },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading, onClick = onContinueReading,
@ -356,8 +370,8 @@ private fun MangaScreenSmallImpl(
PullRefresh( PullRefresh(
refreshing = state.isRefreshingData, refreshing = state.isRefreshingData,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = { !isAnySelected }, enabled = !isAnySelected,
indicatorPadding = PaddingValues(top = topPadding), indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(),
) { ) {
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
VerticalFastScroller( VerticalFastScroller(
@ -400,7 +414,7 @@ private fun MangaScreenSmallImpl(
MangaActionRow( MangaActionRow(
favorite = state.manga.favorite, favorite = state.manga.favorite,
trackingCount = state.trackingCount, trackingCount = state.trackingCount,
nextUpdate = nextUpdate, fetchInterval = fetchInterval,
isUserIntervalMode = state.manga.fetchInterval < 0, isUserIntervalMode = state.manga.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked, onWebViewClicked = onWebViewClicked,
@ -443,6 +457,8 @@ private fun MangaScreenSmallImpl(
manga = state.manga, manga = state.manga,
chapters = listItem, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected }, isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
@ -460,7 +476,9 @@ private fun MangaScreenSmallImpl(
fun MangaScreenLargeImpl( fun MangaScreenLargeImpl(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
nextUpdate: Instant?, dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
@ -469,7 +487,7 @@ fun MangaScreenLargeImpl(
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: () -> Unit, onTrackingClicked: (() -> Unit)?,
// For tags menu // For tags menu
onTagSearch: (String) -> Unit, onTagSearch: (String) -> Unit,
@ -507,17 +525,27 @@ fun MangaScreenLargeImpl(
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current val density = LocalDensity.current
val (chapters, listItem, isAnySelected) = remember(state) { val chapters = remember(state) { state.processedChapters }
Triple( val listItem = remember(state) { state.chapterListItems }
first = state.processedChapters,
second = state.chapterListItems, val isAnySelected by remember {
third = state.isAnySelected, derivedStateOf {
) chapters.fastAny { it.selected }
}
} }
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
var topBarHeight by remember { mutableIntStateOf(0) } var topBarHeight by remember { mutableIntStateOf(0) }
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = !isAnySelected,
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
end = insetPadding.calculateEndPadding(layoutDirection),
),
) {
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
val internalOnBackPressed = { val internalOnBackPressed = {
@ -539,7 +567,7 @@ fun MangaScreenLargeImpl(
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f }, titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f }, backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive, hasFilters = state.manga.chaptersFiltered(),
onBackClicked = internalOnBackPressed, onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked, onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
@ -586,11 +614,7 @@ fun MangaScreenLargeImpl(
val isReading = remember(state.chapters) { val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read } state.chapters.fastAny { it.chapter.read }
} }
Text( Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
text = stringResource(
if (isReading) MR.strings.action_resume else MR.strings.action_start,
),
)
}, },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading, onClick = onContinueReading,
@ -599,16 +623,6 @@ fun MangaScreenLargeImpl(
} }
}, },
) { contentPadding -> ) { contentPadding ->
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = { !isAnySelected },
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
end = insetPadding.calculateEndPadding(layoutDirection),
),
) {
TwoPanelBox( TwoPanelBox(
modifier = Modifier.padding( modifier = Modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection), start = contentPadding.calculateStartPadding(layoutDirection),
@ -636,7 +650,7 @@ fun MangaScreenLargeImpl(
MangaActionRow( MangaActionRow(
favorite = state.manga.favorite, favorite = state.manga.favorite,
trackingCount = state.trackingCount, trackingCount = state.trackingCount,
nextUpdate = nextUpdate, fetchInterval = fetchInterval,
isUserIntervalMode = state.manga.fetchInterval < 0, isUserIntervalMode = state.manga.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked, onWebViewClicked = onWebViewClicked,
@ -686,6 +700,8 @@ fun MangaScreenLargeImpl(
manga = state.manga, manga = state.manga,
chapters = listItem, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected }, isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
@ -704,13 +720,13 @@ fun MangaScreenLargeImpl(
@Composable @Composable
private fun SharedMangaBottomActionMenu( private fun SharedMangaBottomActionMenu(
selected: List<ChapterList.Item>, selected: List<ChapterList.Item>,
modifier: Modifier = Modifier,
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
fillFraction: Float, fillFraction: Float,
modifier: Modifier = Modifier,
) { ) {
MangaBottomActionMenu( MangaBottomActionMenu(
visible = selected.isNotEmpty(), visible = selected.isNotEmpty(),
@ -738,7 +754,7 @@ private fun SharedMangaBottomActionMenu(
onDeleteClicked = { onDeleteClicked = {
onMultiDeleteClicked(selected.fastMap { it.chapter }) onMultiDeleteClicked(selected.fastMap { it.chapter })
}.takeIf { }.takeIf {
selected.fastAny { it.downloadState == Download.State.DOWNLOADED } onDownloadChapter != null && selected.fastAny { it.downloadState == Download.State.DOWNLOADED }
}, },
) )
} }
@ -747,6 +763,8 @@ private fun LazyListScope.sharedChapterItems(
manga: Manga, manga: Manga,
chapters: List<ChapterList>, chapters: List<ChapterList>,
isAnyChapterSelected: Boolean, isAnyChapterSelected: Boolean,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
@ -765,27 +783,54 @@ private fun LazyListScope.sharedChapterItems(
contentType = { MangaScreenItem.CHAPTER }, contentType = { MangaScreenItem.CHAPTER },
) { item -> ) { item ->
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current
when (item) { when (item) {
is ChapterList.MissingCount -> { is ChapterList.MissingCount -> {
MissingChapterCountListItem(count = item.count) Row(
modifier = Modifier.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
HorizontalDivider(modifier = Modifier.weight(1f))
Text(
text = pluralStringResource(
id = R.plurals.missing_chapters,
count = item.count,
item.count,
),
modifier = Modifier.secondaryItemAlpha(),
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
} }
is ChapterList.Item -> { is ChapterList.Item -> {
MangaChapterListItem( MangaChapterListItem(
title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {
stringResource( stringResource(
MR.strings.display_mode_chapter, R.string.display_mode_chapter,
formatChapterNumber(item.chapter.chapterNumber), formatChapterNumber(item.chapter.chapterNumber),
) )
} else { } else {
item.chapter.name item.chapter.name
}, },
date = relativeDateText(item.chapter.dateUpload), date = item.chapter.dateUpload
.takeIf { it > 0L }
?.let {
Date(it).toRelativeString(
context,
dateRelativeTime,
dateFormat,
)
},
readProgress = item.chapter.lastPageRead readProgress = item.chapter.lastPageRead
.takeIf { !item.chapter.read && it > 0L } .takeIf { !item.chapter.read && it > 0L }
?.let { ?.let {
stringResource( stringResource(
MR.strings.chapter_progress, R.string.chapter_progress,
it + 1, it + 1,
) )
}, },
@ -793,7 +838,7 @@ private fun LazyListScope.sharedChapterItems(
read = item.chapter.read, read = item.chapter.read,
bookmark = item.chapter.bookmark, bookmark = item.chapter.bookmark,
selected = item.selected, selected = item.selected,
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(), downloadIndicatorEnabled = !isAnyChapterSelected,
downloadStateProvider = { item.downloadState }, downloadStateProvider = { item.downloadState },
downloadProgressProvider = { item.downloadProgress }, downloadProgressProvider = { item.downloadProgress },
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,

View File

@ -19,8 +19,8 @@ import tachiyomi.presentation.core.components.material.padding
@Composable @Composable
fun BaseMangaListItem( fun BaseMangaListItem(
manga: Manga,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
manga: Manga,
onClickItem: () -> Unit = {}, onClickItem: () -> Unit = {},
onClickCover: () -> Unit = onClickItem, onClickCover: () -> Unit = onClickItem,
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) }, cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },

View File

@ -29,14 +29,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.IconButtonTokens import tachiyomi.presentation.core.components.material.IconButtonTokens
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
enum class ChapterDownloadAction { enum class ChapterDownloadAction {
@ -49,10 +48,10 @@ enum class ChapterDownloadAction {
@Composable @Composable
fun ChapterDownloadIndicator( fun ChapterDownloadIndicator(
enabled: Boolean, enabled: Boolean,
modifier: Modifier = Modifier,
downloadStateProvider: () -> Download.State, downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int, downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit, onClick: (ChapterDownloadAction) -> Unit,
modifier: Modifier = Modifier,
) { ) {
when (val downloadState = downloadStateProvider()) { when (val downloadState = downloadStateProvider()) {
Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator( Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
@ -99,7 +98,7 @@ private fun NotDownloadedIndicator(
) { ) {
Icon( Icon(
painter = painterResource(R.drawable.ic_download_chapter_24dp), painter = painterResource(R.drawable.ic_download_chapter_24dp),
contentDescription = stringResource(MR.strings.manga_download), contentDescription = stringResource(R.string.manga_download),
modifier = Modifier.size(IndicatorSize), modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@ -109,10 +108,10 @@ private fun NotDownloadedIndicator(
@Composable @Composable
private fun DownloadingIndicator( private fun DownloadingIndicator(
enabled: Boolean, enabled: Boolean,
modifier: Modifier = Modifier,
downloadState: Download.State, downloadState: Download.State,
downloadProgressProvider: () -> Int, downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit, onClick: (ChapterDownloadAction) -> Unit,
modifier: Modifier = Modifier,
) { ) {
var isMenuExpanded by remember { mutableStateOf(false) } var isMenuExpanded by remember { mutableStateOf(false) }
Box( Box(
@ -149,7 +148,7 @@ private fun DownloadingIndicator(
MaterialTheme.colorScheme.background MaterialTheme.colorScheme.background
} }
CircularProgressIndicator( CircularProgressIndicator(
progress = { animatedProgress }, progress = animatedProgress,
modifier = IndicatorModifier, modifier = IndicatorModifier,
color = strokeColor, color = strokeColor,
strokeWidth = IndicatorSize / 2, strokeWidth = IndicatorSize / 2,
@ -157,14 +156,14 @@ private fun DownloadingIndicator(
} }
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) { DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_start_downloading_now)) }, text = { Text(text = stringResource(R.string.action_start_downloading_now)) },
onClick = { onClick = {
onClick(ChapterDownloadAction.START_NOW) onClick(ChapterDownloadAction.START_NOW)
isMenuExpanded = false isMenuExpanded = false
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_cancel)) }, text = { Text(text = stringResource(R.string.action_cancel)) },
onClick = { onClick = {
onClick(ChapterDownloadAction.CANCEL) onClick(ChapterDownloadAction.CANCEL)
isMenuExpanded = false isMenuExpanded = false
@ -205,7 +204,7 @@ private fun DownloadedIndicator(
) )
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) { DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_delete)) }, text = { Text(text = stringResource(R.string.action_delete)) },
onClick = { onClick = {
onClick(ChapterDownloadAction.DELETE) onClick(ChapterDownloadAction.DELETE)
isMenuExpanded = false isMenuExpanded = false
@ -233,7 +232,7 @@ private fun ErrorIndicator(
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.ErrorOutline, imageVector = Icons.Outlined.ErrorOutline,
contentDescription = stringResource(MR.strings.chapter_error), contentDescription = stringResource(R.string.chapter_error),
modifier = Modifier.size(IndicatorSize), modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.error,
) )

View File

@ -9,13 +9,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun ChapterHeader( fun ChapterHeader(
@ -23,23 +22,22 @@ fun ChapterHeader(
chapterCount: Int?, chapterCount: Int?,
missingChapterCount: Int, missingChapterCount: Int,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable( .clickable(
enabled = enabled, enabled = enabled,
onClick = onClick, onClick = onClick,
) )
.padding(horizontal = 16.dp, vertical = 4.dp), .padding(horizontal = 16.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), verticalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
Text( Text(
text = if (chapterCount == null) { text = if (chapterCount == null) {
stringResource(MR.strings.chapters) stringResource(R.string.chapters)
} else { } else {
pluralStringResource(MR.plurals.manga_num_chapters, count = chapterCount, chapterCount) pluralStringResource(id = R.plurals.manga_num_chapters, count = chapterCount, chapterCount)
}, },
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
@ -56,7 +54,7 @@ private fun MissingChaptersWarning(count: Int) {
} }
Text( Text(
text = pluralStringResource(MR.plurals.missing_chapters, count = count, count), text = pluralStringResource(id = R.plurals.missing_chapters, count = count, count),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,

View File

@ -2,24 +2,13 @@ package eu.kanade.presentation.manga.components
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable @Composable
fun DotSeparatorText( fun DotSeparatorText() {
modifier: Modifier = Modifier, Text(text = "")
) {
Text(
text = "",
modifier = modifier,
)
} }
@Composable @Composable
fun DotSeparatorNoSpaceText( fun DotSeparatorNoSpaceText() {
modifier: Modifier = Modifier, Text(text = "")
) {
Text(
text = "",
modifier = modifier,
)
} }

View File

@ -23,12 +23,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.BookmarkAdd import androidx.compose.material.icons.outlined.BookmarkAdd
import androidx.compose.material.icons.outlined.BookmarkRemove import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.RemoveDone import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -47,6 +47,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -57,8 +58,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@Composable @Composable
@ -107,7 +106,7 @@ fun MangaBottomActionMenu(
) { ) {
if (onBookmarkClicked != null) { if (onBookmarkClicked != null) {
Button( Button(
title = stringResource(MR.strings.action_bookmark), title = stringResource(R.string.action_bookmark),
icon = Icons.Outlined.BookmarkAdd, icon = Icons.Outlined.BookmarkAdd,
toConfirm = confirm[0], toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) }, onLongClick = { onLongClickItem(0) },
@ -116,7 +115,7 @@ fun MangaBottomActionMenu(
} }
if (onRemoveBookmarkClicked != null) { if (onRemoveBookmarkClicked != null) {
Button( Button(
title = stringResource(MR.strings.action_remove_bookmark), title = stringResource(R.string.action_remove_bookmark),
icon = Icons.Outlined.BookmarkRemove, icon = Icons.Outlined.BookmarkRemove,
toConfirm = confirm[1], toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) }, onLongClick = { onLongClickItem(1) },
@ -125,7 +124,7 @@ fun MangaBottomActionMenu(
} }
if (onMarkAsReadClicked != null) { if (onMarkAsReadClicked != null) {
Button( Button(
title = stringResource(MR.strings.action_mark_as_read), title = stringResource(R.string.action_mark_as_read),
icon = Icons.Outlined.DoneAll, icon = Icons.Outlined.DoneAll,
toConfirm = confirm[2], toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) }, onLongClick = { onLongClickItem(2) },
@ -134,7 +133,7 @@ fun MangaBottomActionMenu(
} }
if (onMarkAsUnreadClicked != null) { if (onMarkAsUnreadClicked != null) {
Button( Button(
title = stringResource(MR.strings.action_mark_as_unread), title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Outlined.RemoveDone, icon = Icons.Outlined.RemoveDone,
toConfirm = confirm[3], toConfirm = confirm[3],
onLongClick = { onLongClickItem(3) }, onLongClick = { onLongClickItem(3) },
@ -143,7 +142,7 @@ fun MangaBottomActionMenu(
} }
if (onMarkPreviousAsReadClicked != null) { if (onMarkPreviousAsReadClicked != null) {
Button( Button(
title = stringResource(MR.strings.action_mark_previous_as_read), title = stringResource(R.string.action_mark_previous_as_read),
icon = ImageVector.vectorResource(R.drawable.ic_done_prev_24dp), icon = ImageVector.vectorResource(R.drawable.ic_done_prev_24dp),
toConfirm = confirm[4], toConfirm = confirm[4],
onLongClick = { onLongClickItem(4) }, onLongClick = { onLongClickItem(4) },
@ -152,7 +151,7 @@ fun MangaBottomActionMenu(
} }
if (onDownloadClicked != null) { if (onDownloadClicked != null) {
Button( Button(
title = stringResource(MR.strings.action_download), title = stringResource(R.string.action_download),
icon = Icons.Outlined.Download, icon = Icons.Outlined.Download,
toConfirm = confirm[5], toConfirm = confirm[5],
onLongClick = { onLongClickItem(5) }, onLongClick = { onLongClickItem(5) },
@ -161,7 +160,7 @@ fun MangaBottomActionMenu(
} }
if (onDeleteClicked != null) { if (onDeleteClicked != null) {
Button( Button(
title = stringResource(MR.strings.action_delete), title = stringResource(R.string.action_delete),
icon = Icons.Outlined.Delete, icon = Icons.Outlined.Delete,
toConfirm = confirm[6], toConfirm = confirm[6],
onLongClick = { onLongClickItem(6) }, onLongClick = { onLongClickItem(6) },
@ -182,10 +181,7 @@ private fun RowScope.Button(
onClick: () -> Unit, onClick: () -> Unit,
content: (@Composable () -> Unit)? = null, content: (@Composable () -> Unit)? = null,
) { ) {
val animatedWeight by animateFloatAsState( val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
targetValue = if (toConfirm) 2f else 1f,
label = "weight",
)
Column( Column(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
@ -222,12 +218,12 @@ private fun RowScope.Button(
@Composable @Composable
fun LibraryBottomActionMenu( fun LibraryBottomActionMenu(
visible: Boolean, visible: Boolean,
modifier: Modifier = Modifier,
onChangeCategoryClicked: () -> Unit, onChangeCategoryClicked: () -> Unit,
onMarkAsReadClicked: () -> Unit, onMarkAsReadClicked: () -> Unit,
onMarkAsUnreadClicked: () -> Unit, onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?, onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: () -> Unit, onDeleteClicked: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
@ -261,21 +257,21 @@ fun LibraryBottomActionMenu(
.padding(horizontal = 8.dp, vertical = 12.dp), .padding(horizontal = 8.dp, vertical = 12.dp),
) { ) {
Button( Button(
title = stringResource(MR.strings.action_move_category), title = stringResource(R.string.action_move_category),
icon = Icons.AutoMirrored.Outlined.Label, icon = Icons.Outlined.Label,
toConfirm = confirm[0], toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) }, onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked, onClick = onChangeCategoryClicked,
) )
Button( Button(
title = stringResource(MR.strings.action_mark_as_read), title = stringResource(R.string.action_mark_as_read),
icon = Icons.Outlined.DoneAll, icon = Icons.Outlined.DoneAll,
toConfirm = confirm[1], toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) }, onLongClick = { onLongClickItem(1) },
onClick = onMarkAsReadClicked, onClick = onMarkAsReadClicked,
) )
Button( Button(
title = stringResource(MR.strings.action_mark_as_unread), title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Outlined.RemoveDone, icon = Icons.Outlined.RemoveDone,
toConfirm = confirm[2], toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) }, onLongClick = { onLongClickItem(2) },
@ -284,7 +280,7 @@ fun LibraryBottomActionMenu(
if (onDownloadClicked != null) { if (onDownloadClicked != null) {
var downloadExpanded by remember { mutableStateOf(false) } var downloadExpanded by remember { mutableStateOf(false) }
Button( Button(
title = stringResource(MR.strings.action_download), title = stringResource(R.string.action_download),
icon = Icons.Outlined.Download, icon = Icons.Outlined.Download,
toConfirm = confirm[3], toConfirm = confirm[3],
onLongClick = { onLongClickItem(3) }, onLongClick = { onLongClickItem(3) },
@ -299,7 +295,7 @@ fun LibraryBottomActionMenu(
} }
} }
Button( Button(
title = stringResource(MR.strings.action_delete), title = stringResource(R.string.action_delete),
icon = Icons.Outlined.Delete, icon = Icons.Outlined.Delete,
toConfirm = confirm[4], toConfirm = confirm[4],
onLongClick = { onLongClickItem(4) }, onLongClick = { onLongClickItem(4) },

View File

@ -33,6 +33,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@ -41,22 +42,23 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import me.saket.swipe.SwipeableActionsBox import me.saket.swipe.SwipeableActionsBox
import me.saket.swipe.rememberSwipeableActionsState import me.saket.swipe.rememberSwipeableActionsState
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.ReadItemAlpha
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@Composable @Composable
fun MangaChapterListItem( fun MangaChapterListItem(
modifier: Modifier = Modifier,
title: String, title: String,
date: String?, date: String?,
readProgress: String?, readProgress: String?,
@ -73,7 +75,6 @@ fun MangaChapterListItem(
onClick: () -> Unit, onClick: () -> Unit,
onDownloadClick: ((ChapterDownloadAction) -> Unit)?, onDownloadClick: ((ChapterDownloadAction) -> Unit)?,
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val density = LocalDensity.current val density = LocalDensity.current
@ -142,7 +143,7 @@ fun MangaChapterListItem(
if (!read) { if (!read) {
Icon( Icon(
imageVector = Icons.Filled.Circle, imageVector = Icons.Filled.Circle,
contentDescription = stringResource(MR.strings.unread), contentDescription = stringResource(R.string.unread),
modifier = Modifier modifier = Modifier
.height(8.dp) .height(8.dp)
.padding(end = 4.dp), .padding(end = 4.dp),
@ -152,7 +153,7 @@ fun MangaChapterListItem(
if (bookmark) { if (bookmark) {
Icon( Icon(
imageVector = Icons.Filled.Bookmark, imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(MR.strings.action_filter_bookmarked), contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
@ -188,7 +189,7 @@ fun MangaChapterListItem(
text = readProgress, text = readProgress,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = ReadItemAlpha), modifier = Modifier.alpha(ReadItemAlpha),
) )
if (scanlator != null) DotSeparatorText() if (scanlator != null) DotSeparatorText()
} }
@ -203,17 +204,19 @@ fun MangaChapterListItem(
} }
} }
if (onDownloadClick != null) {
ChapterDownloadIndicator( ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled, enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider, downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider, downloadProgressProvider = downloadProgressProvider,
onClick = { onDownloadClick?.invoke(it) }, onClick = onDownloadClick,
) )
} }
} }
} }
} }
}
private fun getSwipeAction( private fun getSwipeAction(
action: LibraryPreferences.ChapterSwipeAction, action: LibraryPreferences.ChapterSwipeAction,

View File

@ -22,8 +22,8 @@ enum class MangaCover(val ratio: Float) {
@Composable @Composable
operator fun invoke( operator fun invoke(
data: Any?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
data: Any?,
contentDescription: String = "", contentDescription: String = "",
shape: Shape = MaterialTheme.shapes.extraSmall, shape: Shape = MaterialTheme.shapes.extraSmall,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,

View File

@ -2,7 +2,6 @@ package eu.kanade.presentation.manga.components
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.os.Build
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -32,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
@ -46,12 +46,10 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.manga.EditCoverAction import eu.kanade.presentation.manga.EditCoverAction
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.clickableNoIndication
@Composable @Composable
@ -85,25 +83,29 @@ fun MangaCoverDialog(
IconButton(onClick = onDismissRequest) { IconButton(onClick = onDismissRequest) {
Icon( Icon(
imageVector = Icons.Outlined.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(MR.strings.action_close), contentDescription = stringResource(R.string.action_close),
) )
} }
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
ActionsPill { ActionsPill {
AppBarActions( AppBarActions(
actions = persistentListOf( actions = buildList {
add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_share), title = stringResource(R.string.action_share),
icon = Icons.Outlined.Share, icon = Icons.Outlined.Share,
onClick = onShareClick, onClick = onShareClick,
), ),
)
add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_save), title = stringResource(R.string.action_save),
icon = Icons.Outlined.Save, icon = Icons.Outlined.Save,
onClick = onSaveClick, onClick = onSaveClick,
), ),
), )
},
) )
if (onEditClick != null) { if (onEditClick != null) {
Box { Box {
@ -119,7 +121,7 @@ fun MangaCoverDialog(
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Edit, imageVector = Icons.Outlined.Edit,
contentDescription = stringResource(MR.strings.action_edit_cover), contentDescription = stringResource(R.string.action_edit_cover),
) )
} }
DropdownMenu( DropdownMenu(
@ -128,14 +130,14 @@ fun MangaCoverDialog(
offset = DpOffset(8.dp, 0.dp), offset = DpOffset(8.dp, 0.dp),
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_edit)) }, text = { Text(text = stringResource(R.string.action_edit)) },
onClick = { onClick = {
onEditClick(EditCoverAction.EDIT) onEditClick(EditCoverAction.EDIT)
expanded = false expanded = false
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_delete)) }, text = { Text(text = stringResource(R.string.action_delete)) },
onClick = { onClick = {
onEditClick(EditCoverAction.DELETE) onEditClick(EditCoverAction.DELETE)
expanded = false expanded = false
@ -173,14 +175,9 @@ fun MangaCoverDialog(
// Copy bitmap in case it came from memory cache // Copy bitmap in case it came from memory cache
// Because SSIV needs to thoroughly read the image // Because SSIV needs to thoroughly read the image
val copy = (drawable as? BitmapDrawable)?.let { val copy = (drawable as? BitmapDrawable)?.let {
val config = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Bitmap.Config.HARDWARE
} else {
Bitmap.Config.ARGB_8888
}
BitmapDrawable( BitmapDrawable(
view.context.resources, view.context.resources,
it.bitmap.copy(config, false), it.bitmap.copy(Bitmap.Config.HARDWARE, false),
) )
} ?: drawable } ?: drawable
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500)) view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))

View File

@ -1,36 +1,23 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
@Composable @Composable
fun DeleteChaptersDialog( fun DeleteChaptersDialog(
@ -41,7 +28,7 @@ fun DeleteChaptersDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
confirmButton = { confirmButton = {
@ -51,14 +38,14 @@ fun DeleteChaptersDialog(
onConfirm() onConfirm()
}, },
) { ) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
title = { title = {
Text(text = stringResource(MR.strings.are_you_sure)) Text(text = stringResource(R.string.are_you_sure))
}, },
text = { text = {
Text(text = stringResource(MR.strings.confirm_delete_chapters)) Text(text = stringResource(R.string.confirm_delete_chapters))
}, },
) )
} }
@ -66,84 +53,46 @@ fun DeleteChaptersDialog(
@Composable @Composable
fun SetIntervalDialog( fun SetIntervalDialog(
interval: Int, interval: Int,
nextUpdate: Instant?,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onValueChanged: ((Int) -> Unit)? = null, onValueChanged: (Int) -> Unit,
) { ) {
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) } var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
val nextUpdateDays = remember(nextUpdate) {
return@remember if (nextUpdate != null) {
val now = Instant.now()
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
} else {
null
}
}
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) }, title = { Text(text = stringResource(R.string.manga_modify_calculated_interval_title)) },
text = { text = {
Column {
if (nextUpdateDays != null && nextUpdateDays >= 0 && interval >= 0) {
Text(
stringResource(
MR.strings.manga_interval_expected_update,
pluralStringResource(
MR.plurals.day,
count = nextUpdateDays,
nextUpdateDays,
),
pluralStringResource(
MR.plurals.day,
count = interval.absoluteValue,
interval.absoluteValue,
),
),
)
Spacer(Modifier.height(MaterialTheme.padding.small))
}
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
Text(stringResource(MR.strings.manga_interval_custom_amount))
BoxWithConstraints( BoxWithConstraints(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
val size = DpSize(width = maxWidth / 2, height = 128.dp) val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..FetchInterval.MAX_INTERVAL) val items = (0..FetchInterval.MAX_INTERVAL).map {
.map {
if (it == 0) { if (it == 0) {
stringResource(MR.strings.label_default) stringResource(R.string.label_default)
} else { } else {
it.toString() it.toString()
} }
} }
.toImmutableList()
WheelTextPicker( WheelTextPicker(
items = items,
size = size, size = size,
items = items,
startIndex = selectedInterval, startIndex = selectedInterval,
onSelectionChanged = { selectedInterval = it }, onSelectionChanged = { selectedInterval = it },
) )
} }
}
}
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
onValueChanged?.invoke(selectedInterval) onValueChanged(selectedInterval)
onDismissRequest() onDismissRequest()
}) { }) {
Text(text = stringResource(MR.strings.action_ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
) )

View File

@ -9,7 +9,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -67,6 +66,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
@ -79,21 +80,18 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import java.time.Instant import kotlin.math.absoluteValue
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable @Composable
fun MangaInfoBox( fun MangaInfoBox(
modifier: Modifier = Modifier,
isTabletUi: Boolean, isTabletUi: Boolean,
appBarPadding: Dp, appBarPadding: Dp,
title: String, title: String,
@ -105,7 +103,6 @@ fun MangaInfoBox(
status: Long, status: Long,
onCoverClick: () -> Unit, onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
modifier: Modifier = Modifier,
) { ) {
Box(modifier = modifier) { Box(modifier = modifier) {
// Backdrop // Backdrop
@ -126,7 +123,7 @@ fun MangaInfoBox(
) )
} }
.blur(4.dp) .blur(4.dp)
.alpha(0.2f), .alpha(.2f),
) )
// Manga & source info // Manga & source info
@ -164,69 +161,55 @@ fun MangaInfoBox(
@Composable @Composable
fun MangaActionRow( fun MangaActionRow(
modifier: Modifier = Modifier,
favorite: Boolean, favorite: Boolean,
trackingCount: Int, trackingCount: Int,
nextUpdate: Instant?, fetchInterval: Int?,
isUserIntervalMode: Boolean, isUserIntervalMode: Boolean,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: () -> Unit, onTrackingClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?,
onEditCategory: (() -> Unit)?, onEditCategory: (() -> Unit)?,
modifier: Modifier = Modifier,
) { ) {
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
// TODO: show something better when using custom interval
val nextUpdateDays = remember(nextUpdate) {
return@remember if (nextUpdate != null) {
val now = Instant.now()
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
} else {
null
}
}
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) { Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
MangaActionButton( MangaActionButton(
title = if (favorite) { title = if (favorite) {
stringResource(MR.strings.in_library) stringResource(R.string.in_library)
} else { } else {
stringResource(MR.strings.add_to_library) stringResource(R.string.add_to_library)
}, },
icon = if (favorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, icon = if (favorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor, color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onAddToLibraryClicked, onClick = onAddToLibraryClicked,
onLongClick = onEditCategory, onLongClick = onEditCategory,
) )
if (onEditIntervalClicked != null && fetchInterval != null) {
MangaActionButton( MangaActionButton(
title = when (nextUpdateDays) { title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
null -> stringResource(MR.strings.not_applicable)
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
else -> pluralStringResource(
MR.plurals.day,
count = nextUpdateDays,
nextUpdateDays,
)
},
icon = Icons.Default.HourglassEmpty, icon = Icons.Default.HourglassEmpty,
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor, color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = { onEditIntervalClicked?.invoke() }, onClick = onEditIntervalClicked,
) )
}
if (onTrackingClicked != null) {
MangaActionButton( MangaActionButton(
title = if (trackingCount == 0) { title = if (trackingCount == 0) {
stringResource(MR.strings.manga_tracking_tab) stringResource(R.string.manga_tracking_tab)
} else { } else {
pluralStringResource(MR.plurals.num_trackers, count = trackingCount, trackingCount) pluralStringResource(id = R.plurals.num_trackers, count = trackingCount, trackingCount)
}, },
icon = if (trackingCount == 0) Icons.Outlined.Sync else Icons.Outlined.Done, icon = if (trackingCount == 0) Icons.Outlined.Sync else Icons.Outlined.Done,
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary, color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
onClick = onTrackingClicked, onClick = onTrackingClicked,
) )
}
if (onWebViewClicked != null) { if (onWebViewClicked != null) {
MangaActionButton( MangaActionButton(
title = stringResource(MR.strings.action_web_view), title = stringResource(R.string.action_web_view),
icon = Icons.Outlined.Public, icon = Icons.Outlined.Public,
color = defaultActionButtonColor, color = defaultActionButtonColor,
onClick = onWebViewClicked, onClick = onWebViewClicked,
@ -238,19 +221,19 @@ fun MangaActionRow(
@Composable @Composable
fun ExpandableMangaDescription( fun ExpandableMangaDescription(
modifier: Modifier = Modifier,
defaultExpandState: Boolean, defaultExpandState: Boolean,
description: String?, description: String?,
tagsProvider: () -> List<String>?, tagsProvider: () -> List<String>?,
onTagSearch: (String) -> Unit, onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
val (expanded, onExpanded) = rememberSaveable { val (expanded, onExpanded) = rememberSaveable {
mutableStateOf(defaultExpandState) mutableStateOf(defaultExpandState)
} }
val desc = val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder) description.takeIf { !it.isNullOrBlank() } ?: stringResource(R.string.description_placeholder)
val trimmedDescription = remember(desc) { val trimmedDescription = remember(desc) {
desc desc
.replace(whitespaceLineRegex, "\n") .replace(whitespaceLineRegex, "\n")
@ -280,14 +263,14 @@ fun ExpandableMangaDescription(
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_search)) }, text = { Text(text = stringResource(R.string.action_search)) },
onClick = { onClick = {
onTagSearch(tagSelected) onTagSearch(tagSelected)
showMenu = false showMenu = false
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_copy_to_clipboard)) }, text = { Text(text = stringResource(R.string.action_copy_to_clipboard)) },
onClick = { onClick = {
onCopyTagToClipboard(tagSelected) onCopyTagToClipboard(tagSelected)
showMenu = false showMenu = false
@ -297,7 +280,7 @@ fun ExpandableMangaDescription(
if (expanded) { if (expanded) {
FlowRow( FlowRow(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
tags.forEach { tags.forEach {
TagsChip( TagsChip(
@ -313,7 +296,7 @@ fun ExpandableMangaDescription(
} else { } else {
LazyRow( LazyRow(
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium), contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
) { ) {
items(items = tags) { items(items = tags) {
TagsChip( TagsChip(
@ -354,7 +337,7 @@ private fun MangaAndSourceTitlesLarge(
MangaCover.Book( MangaCover.Book(
modifier = Modifier.fillMaxWidth(0.65f), modifier = Modifier.fillMaxWidth(0.65f),
data = coverDataProvider(), data = coverDataProvider(),
contentDescription = stringResource(MR.strings.manga_cover), contentDescription = stringResource(R.string.manga_cover),
onClick = onCoverClick, onClick = onCoverClick,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@ -396,7 +379,7 @@ private fun MangaAndSourceTitlesSmall(
.sizeIn(maxWidth = 100.dp) .sizeIn(maxWidth = 100.dp)
.align(Alignment.Top), .align(Alignment.Top),
data = coverDataProvider(), data = coverDataProvider(),
contentDescription = stringResource(MR.strings.manga_cover), contentDescription = stringResource(R.string.manga_cover),
onClick = onCoverClick, onClick = onCoverClick,
) )
Column( Column(
@ -416,19 +399,19 @@ private fun MangaAndSourceTitlesSmall(
} }
@Composable @Composable
private fun ColumnScope.MangaContentInfo( private fun MangaContentInfo(
title: String, title: String,
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
author: String?, author: String?,
artist: String?, artist: String?,
status: Long, status: Long,
sourceName: String, sourceName: String,
isStubSource: Boolean, isStubSource: Boolean,
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
) { ) {
val context = LocalContext.current val context = LocalContext.current
Text( Text(
text = title.ifBlank { stringResource(MR.strings.unknown_title) }, text = title.ifBlank { stringResource(R.string.unknown_title) },
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = Modifier.clickableNoIndication( modifier = Modifier.clickableNoIndication(
onLongClick = { onLongClick = {
@ -448,7 +431,7 @@ private fun ColumnScope.MangaContentInfo(
Row( Row(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(
@ -458,7 +441,7 @@ private fun ColumnScope.MangaContentInfo(
) )
Text( Text(
text = author?.takeIf { it.isNotBlank() } text = author?.takeIf { it.isNotBlank() }
?: stringResource(MR.strings.unknown_author), ?: stringResource(R.string.unknown_author),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier modifier = Modifier
.clickableNoIndication( .clickableNoIndication(
@ -479,7 +462,7 @@ private fun ColumnScope.MangaContentInfo(
if (!artist.isNullOrBlank() && author != artist) { if (!artist.isNullOrBlank() && author != artist) {
Row( Row(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(
@ -524,13 +507,13 @@ private fun ColumnScope.MangaContentInfo(
ProvideTextStyle(MaterialTheme.typography.bodyMedium) { ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
Text( Text(
text = when (status) { text = when (status) {
SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing) SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed) SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed) SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished) SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished)
SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled) SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus) SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
else -> stringResource(MR.strings.unknown) else -> stringResource(R.string.unknown)
}, },
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
@ -568,10 +551,7 @@ private fun MangaSummary(
expanded: Boolean, expanded: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val animProgress by animateFloatAsState( val animProgress by animateFloatAsState(if (expanded) 1f else 0f)
targetValue = if (expanded) 1f else 0f,
label = "summary",
)
Layout( Layout(
modifier = modifier.clipToBounds(), modifier = modifier.clipToBounds(),
contents = listOf( contents = listOf(
@ -607,9 +587,7 @@ private fun MangaSummary(
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down) val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon( Icon(
painter = rememberAnimatedVectorPainter(image, !expanded), painter = rememberAnimatedVectorPainter(image, !expanded),
contentDescription = stringResource( contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
if (expanded) MR.strings.manga_info_collapse else MR.strings.manga_info_expand,
),
tint = MaterialTheme.colorScheme.onBackground, tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())), modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
) )

View File

@ -20,6 +20,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@ -27,15 +29,15 @@ import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.DownloadDropdownMenu import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.UpIcon import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf import eu.kanade.tachiyomi.R
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.active import tachiyomi.presentation.core.theme.active
@Composable @Composable
fun MangaToolbar( fun MangaToolbar(
modifier: Modifier = Modifier,
title: String, title: String,
titleAlphaProvider: () -> Float, titleAlphaProvider: () -> Float,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
hasFilters: Boolean, hasFilters: Boolean,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onClickFilter: () -> Unit, onClickFilter: () -> Unit,
@ -44,14 +46,10 @@ fun MangaToolbar(
onClickEditCategory: (() -> Unit)?, onClickEditCategory: (() -> Unit)?,
onClickRefresh: () -> Unit, onClickRefresh: () -> Unit,
onClickMigrate: (() -> Unit)?, onClickMigrate: (() -> Unit)?,
// For action mode // For action mode
actionModeCounter: Int, actionModeCounter: Int,
onSelectAll: () -> Unit, onSelectAll: () -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
modifier: Modifier = Modifier,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
@ -63,25 +61,25 @@ fun MangaToolbar(
text = if (isActionMode) actionModeCounter.toString() else title, text = if (isActionMode) actionModeCounter.toString() else title,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()), modifier = Modifier.alpha(if (isActionMode) 1f else titleAlphaProvider()),
) )
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackClicked) { IconButton(onClick = onBackClicked) {
UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode }) UpIcon(Icons.Outlined.Close.takeIf { isActionMode })
} }
}, },
actions = { actions = {
if (isActionMode) { if (isActionMode) {
AppBarActions( AppBarActions(
persistentListOf( listOf(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_all), title = stringResource(R.string.action_select_all),
icon = Icons.Outlined.SelectAll, icon = Icons.Outlined.SelectAll,
onClick = onSelectAll, onClick = onSelectAll,
), ),
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_inverse), title = stringResource(R.string.action_select_inverse),
icon = Icons.Outlined.FlipToBack, icon = Icons.Outlined.FlipToBack,
onClick = onInvertSelection, onClick = onInvertSelection,
), ),
@ -100,12 +98,11 @@ fun MangaToolbar(
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
AppBarActions( AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder() actions = buildList {
.apply {
if (onClickDownload != null) { if (onClickDownload != null) {
add( add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.manga_download), title = stringResource(R.string.manga_download),
icon = Icons.Outlined.Download, icon = Icons.Outlined.Download,
onClick = { downloadExpanded = !downloadExpanded }, onClick = { downloadExpanded = !downloadExpanded },
), ),
@ -113,7 +110,7 @@ fun MangaToolbar(
} }
add( add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_filter), title = stringResource(R.string.action_filter),
icon = Icons.Outlined.FilterList, icon = Icons.Outlined.FilterList,
iconTint = filterTint, iconTint = filterTint,
onClick = onClickFilter, onClick = onClickFilter,
@ -121,14 +118,14 @@ fun MangaToolbar(
) )
add( add(
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_webview_refresh), title = stringResource(R.string.action_webview_refresh),
onClick = onClickRefresh, onClick = onClickRefresh,
), ),
) )
if (onClickEditCategory != null) { if (onClickEditCategory != null) {
add( add(
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_edit_categories), title = stringResource(R.string.action_edit_categories),
onClick = onClickEditCategory, onClick = onClickEditCategory,
), ),
) )
@ -136,7 +133,7 @@ fun MangaToolbar(
if (onClickMigrate != null) { if (onClickMigrate != null) {
add( add(
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_migrate), title = stringResource(R.string.action_migrate),
onClick = onClickMigrate, onClick = onClickMigrate,
), ),
) )
@ -144,13 +141,12 @@ fun MangaToolbar(
if (onClickShare != null) { if (onClickShare != null) {
add( add(
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_share), title = stringResource(R.string.action_share),
onClick = onClickShare, onClick = onClickShare,
), ),
) )
} }
} },
.build(),
) )
} }
}, },

View File

@ -1,52 +0,0 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.TachiyomiPreviewTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
fun MissingChapterCountListItem(
count: Int,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
)
.secondaryItemAlpha(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
HorizontalDivider(modifier = Modifier.weight(1f))
Text(
text = pluralStringResource(MR.plurals.missing_chapters, count = count, count),
style = MaterialTheme.typography.labelMedium,
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
}
@PreviewLightDark
@Composable
private fun Preview() {
TachiyomiPreviewTheme {
Surface {
MissingChapterCountListItem(count = 42)
}
}
}

View File

@ -1,132 +0,0 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
import androidx.compose.material.icons.rounded.DisabledByDefault
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@Composable
fun ScanlatorFilterDialog(
availableScanlators: Set<String>,
excludedScanlators: Set<String>,
onDismissRequest: () -> Unit,
onConfirm: (Set<String>) -> Unit,
) {
val sortedAvailableScanlators = remember(availableScanlators) {
availableScanlators.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it })
}
val mutableExcludedScanlators = remember(excludedScanlators) { excludedScanlators.toMutableStateList() }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.exclude_scanlators)) },
text = textFunc@{
if (sortedAvailableScanlators.isEmpty()) {
Text(text = stringResource(MR.strings.no_scanlators_found))
return@textFunc
}
Box {
val state = rememberLazyListState()
LazyColumn(state = state) {
sortedAvailableScanlators.forEach { scanlator ->
item {
val isExcluded = mutableExcludedScanlators.contains(scanlator)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
if (isExcluded) {
mutableExcludedScanlators.remove(scanlator)
} else {
mutableExcludedScanlators.add(scanlator)
}
}
.minimumInteractiveComponentSize()
.clip(MaterialTheme.shapes.small)
.fillMaxWidth()
.padding(horizontal = MaterialTheme.padding.small),
) {
Icon(
imageVector = if (isExcluded) {
Icons.Rounded.DisabledByDefault
} else {
Icons.Rounded.CheckBoxOutlineBlank
},
tint = if (isExcluded) {
MaterialTheme.colorScheme.primary
} else {
LocalContentColor.current
},
contentDescription = null,
)
Text(
text = scanlator,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(start = 24.dp),
)
}
}
}
}
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
}
},
properties = DialogProperties(
usePlatformDefaultWidth = true,
),
confirmButton = {
if (sortedAvailableScanlators.isEmpty()) {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
} else {
FlowRow {
TextButton(onClick = mutableExcludedScanlators::clear) {
Text(text = stringResource(MR.strings.action_reset))
}
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
TextButton(
onClick = {
onConfirm(mutableExcludedScanlators.toSet())
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_ok))
}
}
}
},
)
}

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.more package eu.kanade.presentation.more
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
@ -8,11 +9,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.CloudOff import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material.icons.outlined.GetApp import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.QueryStats import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Storage import androidx.compose.material.icons.outlined.Storage
@ -21,17 +22,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.more.DownloadQueueState import eu.kanade.tachiyomi.ui.more.DownloadQueueState
import tachiyomi.core.Constants import tachiyomi.core.Constants
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun MoreScreen( fun MoreScreen(
@ -58,7 +59,12 @@ fun MoreScreen(
), ),
) { ) {
if (isFDroid) { if (isFDroid) {
// Don't really care about slow updaters now WarningBanner(
textRes = R.string.fdroid_warning,
modifier = Modifier.clickable {
uriHandler.openUri("https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds")
},
)
} }
} }
}, },
@ -71,8 +77,8 @@ fun MoreScreen(
} }
item { item {
SwitchPreferenceWidget( SwitchPreferenceWidget(
title = stringResource(MR.strings.label_downloaded_only), title = stringResource(R.string.label_downloaded_only),
subtitle = stringResource(MR.strings.downloaded_only_summary), subtitle = stringResource(R.string.downloaded_only_summary),
icon = Icons.Outlined.CloudOff, icon = Icons.Outlined.CloudOff,
checked = downloadedOnly, checked = downloadedOnly,
onCheckedChanged = onDownloadedOnlyChange, onCheckedChanged = onDownloadedOnlyChange,
@ -80,8 +86,8 @@ fun MoreScreen(
} }
item { item {
SwitchPreferenceWidget( SwitchPreferenceWidget(
title = stringResource(MR.strings.pref_incognito_mode), title = stringResource(R.string.pref_incognito_mode),
subtitle = stringResource(MR.strings.pref_incognito_mode_summary), subtitle = stringResource(R.string.pref_incognito_mode_summary),
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp), icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
checked = incognitoMode, checked = incognitoMode,
onCheckedChanged = onIncognitoModeChange, onCheckedChanged = onIncognitoModeChange,
@ -93,17 +99,17 @@ fun MoreScreen(
item { item {
val downloadQueueState = downloadQueueStateProvider() val downloadQueueState = downloadQueueStateProvider()
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.label_download_queue), title = stringResource(R.string.label_download_queue),
subtitle = when (downloadQueueState) { subtitle = when (downloadQueueState) {
DownloadQueueState.Stopped -> null DownloadQueueState.Stopped -> null
is DownloadQueueState.Paused -> { is DownloadQueueState.Paused -> {
val pending = downloadQueueState.pending val pending = downloadQueueState.pending
if (pending == 0) { if (pending == 0) {
stringResource(MR.strings.paused) stringResource(R.string.paused)
} else { } else {
"${stringResource(MR.strings.paused)}${ "${stringResource(R.string.paused)}${
pluralStringResource( pluralStringResource(
MR.plurals.download_queue_summary, id = R.plurals.download_queue_summary,
count = pending, count = pending,
pending, pending,
) )
@ -112,7 +118,7 @@ fun MoreScreen(
} }
is DownloadQueueState.Downloading -> { is DownloadQueueState.Downloading -> {
val pending = downloadQueueState.pending val pending = downloadQueueState.pending
pluralStringResource(MR.plurals.download_queue_summary, count = pending, pending) pluralStringResource(id = R.plurals.download_queue_summary, count = pending, pending)
} }
}, },
icon = Icons.Outlined.GetApp, icon = Icons.Outlined.GetApp,
@ -121,21 +127,21 @@ fun MoreScreen(
} }
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.categories), title = stringResource(R.string.categories),
icon = Icons.AutoMirrored.Outlined.Label, icon = Icons.Outlined.Label,
onPreferenceClick = onClickCategories, onPreferenceClick = onClickCategories,
) )
} }
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.label_stats), title = stringResource(R.string.label_stats),
icon = Icons.Outlined.QueryStats, icon = Icons.Outlined.QueryStats,
onPreferenceClick = onClickStats, onPreferenceClick = onClickStats,
) )
} }
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.label_data_storage), title = stringResource(R.string.label_data_storage),
icon = Icons.Outlined.Storage, icon = Icons.Outlined.Storage,
onPreferenceClick = onClickDataAndStorage, onPreferenceClick = onClickDataAndStorage,
) )
@ -145,22 +151,22 @@ fun MoreScreen(
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.label_settings), title = stringResource(R.string.label_settings),
icon = Icons.Outlined.Settings, icon = Icons.Outlined.Settings,
onPreferenceClick = onClickSettings, onPreferenceClick = onClickSettings,
) )
} }
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.pref_category_about), title = stringResource(R.string.pref_category_about),
icon = Icons.Outlined.Info, icon = Icons.Outlined.Info,
onPreferenceClick = onClickAbout, onPreferenceClick = onClickAbout,
) )
} }
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.label_help), title = stringResource(R.string.label_help),
icon = Icons.AutoMirrored.Outlined.HelpOutline, icon = Icons.Outlined.HelpOutline,
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) }, onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
) )
} }

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