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}]
max_line_length = 120
indent_size=4
insert_final_newline=true
ij_kotlin_allow_trailing_comma=true

View File

@ -3,10 +3,10 @@
I acknowledge that:
- 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
- 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 will fill out the title and the information in this template

View File

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

View File

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

View File

@ -31,9 +31,9 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with an 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
- 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
- label: I will fill out all of the requested information in this form.
required: true

View File

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

View File

@ -22,12 +22,8 @@ jobs:
- name: Validate Gradle Wrapper
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
uses: actions/setup-java@v4
uses: actions/setup-java@v3
with:
java-version: 17
distribution: adopt

View File

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

View File

@ -59,7 +59,8 @@ representative at an online or offline event.
## Enforcement
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 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
No support is currently provided.
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
# Translations

View File

@ -1,6 +1,7 @@
| Build | Stable | Weekly Preview | Contribute |
|-------|--------|----------------|------------|
| [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) |
| Build | Stable | Weekly Preview | Contribute | Support Server |
|-------|----------|---------|------------|---------|
| [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
@ -28,7 +29,8 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<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>
@ -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"
* 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><summary>Contributing</summary>
@ -69,6 +71,7 @@ See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
## FAQ
[See our website.](https://tachiyomi.org/)
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
## License

View File

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

View File

@ -45,7 +45,7 @@
##---------------Begin: proguard configuration for kotlinx.serialization ----------
-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
-keepclassmembers class kotlinx.serialization.json.** {
@ -71,3 +71,7 @@
# XmlUtil
-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" />
<!-- Storage -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- For background jobs -->
<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.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<!-- To view extension packages in API 30+ -->
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
<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
android:name=".App"
@ -43,7 +39,6 @@
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:preserveLegacyExternalStorage="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
@ -51,53 +46,13 @@
<activity
android:name=".ui.main.MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.Tachiyomi.SplashScreen">
android:theme="@style/Theme.Tachiyomi.SplashScreen"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</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 -->
<meta-data
android:name="android.app.shortcuts"
@ -105,16 +60,16 @@
</activity>
<activity
android:process=":error_handler"
android:name=".crash.CrashActivity"
android:exported="false"
android:process=":error_handler" />
android:exported="false" />
<activity
android:name=".ui.deeplink.DeepLinkActivity"
android:exported="true"
android:label="@string/action_search"
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>
<action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -138,21 +93,20 @@
<activity
android:name=".ui.reader.ReaderActivity"
android:exported="false"
android:launchMode="singleTask">
android:launchMode="singleTask"
android:exported="false">
<intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter>
<meta-data
android:name="com.samsung.android.support.REMOTE_ACTION"
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions"/>
</activity>
<activity
android:name=".ui.security.UnlockActivity"
android:exported="false"
android:theme="@style/Theme.Tachiyomi" />
android:theme="@style/Theme.Tachiyomi"
android:exported="false" />
<activity
android:name=".ui.webview.WebViewActivity"
@ -161,25 +115,25 @@
<activity
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
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>
<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="anilist-auth"/>
<data android:host="bangumi-auth"/>
<data android:host="myanimelist-auth"/>
<data android:host="shikimori-auth"/>
<data android:scheme="tachiyomi"/>
</intent-filter>
</activity>
@ -187,10 +141,13 @@
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<service
android:name=".data.download.DownloadService"
android:exported="false" />
<service
android:name=".extension.util.ExtensionInstallService"
android:exported="false"
android:foregroundServiceType="shortService" />
android:exported="false" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
@ -201,11 +158,6 @@
android:value="true" />
</service>
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
@ -219,9 +171,9 @@
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data
@ -231,6 +183,11 @@
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<!-- Disable advertising ID collection for Firebase -->
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="false" />
</application>
</manifest>

View File

@ -1,18 +1,11 @@
package eu.kanade.domain
import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.download.interactor.DeleteDownload
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.extension.interactor.GetExtensionRepos
import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetEnabledSources
@ -119,8 +112,6 @@ class DomainModule : InjektModule {
addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get(), get()) }
addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) }
@ -142,8 +133,7 @@ class DomainModule : InjektModule {
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) }
@ -171,10 +161,5 @@ class DomainModule : InjektModule {
addFactory { ToggleLanguage(get()) }
addFactory { ToggleSource(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
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.PreferenceStore
import tachiyomi.i18n.MR
class BasePreferences(
val context: Context,
@ -20,12 +22,12 @@ class BasePreferences(
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) {
LEGACY(MR.strings.ext_installer_legacy, true),
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
SHIZUKU(MR.strings.ext_installer_shizuku, false),
PRIVATE(MR.strings.ext_installer_private, false),
enum class ExtensionInstaller(@StringRes val titleResId: Int) {
LEGACY(R.string.ext_installer_legacy),
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
SHIZUKU(R.string.ext_installer_shizuku),
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.toSChapter
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.download.DownloadManager
@ -23,6 +22,7 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal
import java.lang.Long.max
import java.time.ZonedDateTime
import java.util.Date
import java.util.TreeSet
class SyncChaptersWithSource(
@ -33,7 +33,6 @@ class SyncChaptersWithSource(
private val updateManga: UpdateManga,
private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators,
) {
/**
@ -56,7 +55,6 @@ class SyncChaptersWithSource(
}
val now = ZonedDateTime.now()
val nowMillis = now.toInstant().toEpochMilli()
val sourceChapters = rawSourceChapters
.distinctBy { it.url }
@ -67,27 +65,36 @@ class SyncChaptersWithSource(
.copy(mangaId = manga.id, sourceOrder = i.toLong())
}
// Chapters from db.
val dbChapters = getChaptersByMangaId.await(manga.id)
val newChapters = mutableListOf<Chapter>()
val updatedChapters = mutableListOf<Chapter>()
val removedChapters = dbChapters.filterNot { dbChapter ->
// Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>()
// Chapters whose metadata have changed.
val toChange = mutableListOf<Chapter>()
// Chapters from the db not in source.
val toDelete = dbChapters.filterNot { dbChapter ->
sourceChapters.any { sourceChapter ->
dbChapter.url == sourceChapter.url
}
}
val rightNow = Date().time
// Used to not set upload date of older chapters
// to a higher value than newer chapters
var maxSeenUploadDate = 0L
val sManga = manga.toSManga()
for (sourceChapter in sourceChapters) {
var chapter = sourceChapter
// Update metadata from source if necessary.
if (source is HttpSource) {
val sChapter = chapter.toSChapter()
source.prepareNewChapter(sChapter, manga.toSManga())
source.prepareNewChapter(sChapter, sManga)
chapter = chapter.copyFromSChapter(sChapter)
}
@ -99,19 +106,17 @@ class SyncChaptersWithSource(
if (dbChapter == null) {
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)
} else {
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
chapter
}
newChapters.add(toAddChapter)
toAdd.add(toAddChapter)
} else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
downloadManager.isChapterDownloaded(
dbChapter.name, dbChapter.scanlator, manga.title, manga.source,
)
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter)
@ -125,13 +130,13 @@ class SyncChaptersWithSource(
if (chapter.dateUpload != 0L) {
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.
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval(
manga,
@ -148,20 +153,20 @@ class SyncChaptersWithSource(
val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
removedChapters.forEach { chapter ->
toDelete.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.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 }
// 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.
var itemCount = newChapters.size
var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
var itemCount = toAdd.size
var updatedToAdd = toAdd.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
@ -180,8 +185,8 @@ class SyncChaptersWithSource(
chapter
}
if (removedChapters.isNotEmpty()) {
val toDeleteIds = removedChapters.map { it.id }
if (toDelete.isNotEmpty()) {
val toDeleteIds = toDelete.map { it.id }
chapterRepository.removeChaptersWithIds(toDeleteIds)
}
@ -189,8 +194,8 @@ class SyncChaptersWithSource(
updatedToAdd = chapterRepository.addAll(updatedToAdd)
}
if (updatedChapters.isNotEmpty()) {
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
if (toChange.isNotEmpty()) {
val chapterUpdates = toChange.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates)
}
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
@ -201,10 +206,6 @@ class SyncChaptersWithSource(
val reAddedUrls = reAdded.map { it.url }.toHashSet()
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
return updatedToAdd.filterNot {
it.url in reAddedUrls || it.scanlator in excludedScanlators
}
return updatedToAdd.filterNot { it.url in reAddedUrls }
}
}

View File

@ -22,7 +22,7 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
url = sChapter.url,
dateUpload = sChapter.date_upload,
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(downloadedFilter) {
val downloaded = downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
manga.title,
manga.source,
)
val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
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
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
@ -14,7 +14,7 @@ class SetMangaViewerFlags(
mangaRepository.update(
MangaUpdate(
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(
MangaUpdate(
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.ZonedDateTime
import java.util.Date
class UpdateManga(
private val mangaRepository: MangaRepository,
@ -46,14 +46,14 @@ class UpdateManga(
// Never refresh covers if the url is empty to avoid "losing" existing covers
remoteManga.thumbnail_url.isNullOrEmpty() -> null
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
localManga.isLocal() -> Instant.now().toEpochMilli()
localManga.isLocal() -> Date().time
localManga.hasCustomCover(coverCache) -> {
coverCache.deleteFromCache(localManga, false)
null
}
else -> {
coverCache.deleteFromCache(localManga, false)
Instant.now().toEpochMilli()
Date().time
}
}
@ -81,22 +81,22 @@ class UpdateManga(
dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
): Boolean {
return mangaRepository.update(
fetchInterval.toMangaUpdate(manga, dateTime, window),
)
return fetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
?.let { mangaRepository.update(it) }
?: false
}
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 {
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 {
val dateAdded = when (favorite) {
true -> Instant.now().toEpochMilli()
true -> Date().time
false -> 0
}
return mangaRepository.update(

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ class TrackChapter(
if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track ->
val service = trackerManager.get(track.trackerId)
val service = trackerManager.get(track.syncId)
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
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.manga_id = mangaId
it.remote_id = remoteId
it.media_id = remoteId
it.library_id = libraryId
it.title = title
it.last_chapter_read = lastChapterRead.toFloat()
@ -33,16 +33,14 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
return Track(
id = trackId,
mangaId = manga_id,
trackerId = tracker_id.toLong(),
remoteId = remote_id,
syncId = sync_id.toLong(),
remoteId = media_id,
libraryId = library_id,
title = title,
lastChapterRead = last_chapter_read.toDouble(),
totalChapters = total_chapters.toLong(),
status = status.toLong(),
// Jank workaround due to precision issues while converting
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
score = score.toString().toDouble(),
score = score.toDouble(),
remoteUrl = tracking_url,
startDate = started_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 uy.kohesive.injekt.Injekt
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) :
CoroutineWorker(context, workerParams) {
@ -42,9 +43,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
}
.forEach { track ->
logcat(LogPriority.DEBUG) {
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
}
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${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>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration())
.addTag(TAG)
.build()

View File

@ -9,24 +9,26 @@ class TrackPreferences(
private val preferenceStore: PreferenceStore,
) {
fun trackUsername(tracker: Tracker) = preferenceStore.getString(
Preference.privateKey("pref_mangasync_username_${tracker.id}"),
"",
)
fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
"",
)
fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
fun setCredentials(tracker: Tracker, username: String, password: String) {
trackUsername(tracker).set(username)
trackPassword(tracker).set(password)
fun setCredentials(sync: Tracker, username: String, password: String) {
trackUsername(sync).set(username)
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 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
import android.os.Build
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode
@ -16,10 +15,7 @@ class UiPreferences(
private val preferenceStore: PreferenceStore,
) {
fun themeMode() = preferenceStore.getEnum(
"pref_theme_mode_key",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ThemeMode.SYSTEM } else { ThemeMode.LIGHT },
)
fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM)
fun appTheme() = preferenceStore.getEnum(
"pref_app_theme",

View File

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

View File

@ -1,11 +1,10 @@
package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource
import tachiyomi.i18n.MR
import eu.kanade.tachiyomi.R
enum class TabletUiMode(val titleRes: StringResource) {
AUTOMATIC(MR.strings.automatic_background),
ALWAYS(MR.strings.lock_always),
LANDSCAPE(MR.strings.landscape),
NEVER(MR.strings.lock_never),
enum class TabletUiMode(val titleResId: Int) {
AUTOMATIC(R.string.automatic_background),
ALWAYS(R.string.lock_always),
LANDSCAPE(R.string.landscape),
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.lazy.grid.GridCells
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.Refresh
import androidx.compose.material3.SnackbarDuration
@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
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.components.AppBar
import eu.kanade.presentation.util.formattedMessage
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.Source
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.core.i18n.stringResource
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.EmptyScreenAction
import tachiyomi.presentation.core.screens.LoadingScreen
@ -63,7 +61,7 @@ fun BrowseSourceContent(
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
val result = snackbarHostState.showSnackbar(
message = getErrorMessage(errorState),
actionLabel = context.stringResource(MR.strings.action_retry),
actionLabel = context.getString(R.string.action_retry),
duration = SnackbarDuration.Indefinite,
)
when (result) {
@ -78,28 +76,28 @@ fun BrowseSourceContent(
modifier = Modifier.padding(contentPadding),
message = getErrorMessage(errorState),
actions = if (source is LocalSource) {
persistentListOf(
listOf(
EmptyScreenAction(
stringRes = MR.strings.local_source_help_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
stringResId = R.string.local_source_help_guide,
icon = Icons.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick,
),
)
} else {
persistentListOf(
listOf(
EmptyScreenAction(
stringRes = MR.strings.action_retry,
stringResId = R.string.action_retry,
icon = Icons.Outlined.Refresh,
onClick = mangaList::refresh,
),
EmptyScreenAction(
stringRes = MR.strings.action_open_in_web_view,
stringResId = R.string.action_open_in_web_view,
icon = Icons.Outlined.Public,
onClick = onWebViewClick,
),
EmptyScreenAction(
stringRes = MR.strings.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
stringResId = R.string.label_help,
icon = Icons.Outlined.HelpOutline,
onClick = onHelpClick,
),
)
@ -147,7 +145,7 @@ fun BrowseSourceContent(
}
@Composable
internal fun MissingSourceScreen(
fun MissingSourceScreen(
source: StubSource,
navigateUp: () -> Unit,
) {
@ -161,7 +159,7 @@ internal fun MissingSourceScreen(
},
) { paddingValues ->
EmptyScreen(
message = stringResource(MR.strings.source_not_installed, source.toString()),
message = stringResource(R.string.source_not_installed, source.toString()),
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.lazy.items
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.material3.AlertDialog
import androidx.compose.material3.Button
@ -37,7 +38,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.font.FontWeight
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.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
@Composable
@ -67,61 +65,55 @@ fun ExtensionDetailsScreen(
navigateUp: () -> Unit,
state: ExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit,
onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit,
onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
) {
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
regex.find(state.extension?.repoUrl.orEmpty())
?.let {
val (user, repo) = it.destructured
"https://github.com/$user/$repo"
}
?: state.extension?.repoUrl
}
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = stringResource(MR.strings.label_extension_info),
title = stringResource(R.string.label_extension_info),
navigateUp = navigateUp,
actions = {
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
if (url != null) {
actions = buildList {
if (state.extension?.isUnofficial == false) {
add(
AppBar.Action(
title = stringResource(MR.strings.action_open_repo),
icon = Icons.AutoMirrored.Outlined.Launch,
onClick = {
uriHandler.openUri(url)
},
title = stringResource(R.string.whats_new),
icon = Icons.Outlined.History,
onClick = onClickWhatsNew,
),
)
add(
AppBar.Action(
title = stringResource(R.string.action_faq_and_guides),
icon = Icons.Outlined.HelpOutline,
onClick = onClickReadme,
),
)
}
addAll(
listOf(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_enable_all),
title = stringResource(R.string.action_enable_all),
onClick = onClickEnableAll,
),
AppBar.OverflowAction(
title = stringResource(MR.strings.action_disable_all),
title = stringResource(R.string.action_disable_all),
onClick = onClickDisableAll,
),
AppBar.OverflowAction(
title = stringResource(MR.strings.pref_clear_cookies),
title = stringResource(R.string.pref_clear_cookies),
onClick = onClickClearCookies,
),
),
)
}
.build(),
},
)
},
scrollBehavior = scrollBehavior,
@ -130,7 +122,7 @@ fun ExtensionDetailsScreen(
) { paddingValues ->
if (state.extension == null) {
EmptyScreen(
MR.strings.empty_screen,
textResource = R.string.empty_screen,
modifier = Modifier.padding(paddingValues),
)
return@Scaffold
@ -151,7 +143,7 @@ fun ExtensionDetailsScreen(
private fun ExtensionDetails(
contentPadding: PaddingValues,
extension: Extension.Installed,
sources: ImmutableList<ExtensionSourceItem>,
sources: List<ExtensionSourceItem>,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
@ -162,9 +154,14 @@ private fun ExtensionDetails(
ScrollbarLazyColumn(
contentPadding = contentPadding,
) {
if (extension.isObsolete) {
when {
extension.isUnofficial ->
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(
modifier = Modifier.weight(1f),
primaryText = extension.versionName,
secondaryText = stringResource(MR.strings.ext_info_version),
secondaryText = stringResource(R.string.ext_info_version),
)
InfoDivider()
@ -269,7 +266,7 @@ private fun DetailsHeader(
InfoText(
modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
secondaryText = stringResource(MR.strings.ext_info_language),
secondaryText = stringResource(R.string.ext_info_language),
)
if (extension.isNsfw) {
@ -277,12 +274,12 @@ private fun DetailsHeader(
InfoText(
modifier = Modifier.weight(1f),
primaryText = stringResource(MR.strings.ext_nsfw_short),
primaryText = stringResource(R.string.ext_nsfw_short),
primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Medium,
),
secondaryText = stringResource(MR.strings.ext_info_age_rating),
secondaryText = stringResource(R.string.ext_info_age_rating),
onClick = onClickAgeRating,
)
}
@ -295,13 +292,13 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onClickUninstall,
) {
Text(stringResource(MR.strings.ext_uninstall))
Text(stringResource(R.string.ext_uninstall))
}
if (onClickAppInfo != null) {
@ -310,7 +307,7 @@ private fun DetailsHeader(
onClick = onClickAppInfo,
) {
Text(
text = stringResource(MR.strings.ext_app_info),
text = stringResource(R.string.ext_app_info),
color = MaterialTheme.colorScheme.onPrimary,
)
}
@ -323,10 +320,10 @@ private fun DetailsHeader(
@Composable
private fun InfoText(
modifier: Modifier,
primaryText: String,
secondaryText: String,
modifier: Modifier = Modifier,
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
secondaryText: String,
onClick: (() -> Unit)? = null,
) {
val interactionSource = remember { MutableInteractionSource() }
@ -366,10 +363,10 @@ private fun InfoDivider() {
@Composable
private fun SourceSwitchPreference(
modifier: Modifier = Modifier,
source: ExtensionSourceItem,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
@ -388,7 +385,7 @@ private fun SourceSwitchPreference(
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.label_settings),
contentDescription = stringResource(R.string.label_settings),
tint = MaterialTheme.colorScheme.onSurface,
)
}
@ -411,11 +408,11 @@ private fun NsfwWarningDialog(
) {
AlertDialog(
text = {
Text(text = stringResource(MR.strings.ext_nsfw_warning))
Text(text = stringResource(R.string.ext_nsfw_warning))
},
confirmButton = {
TextButton(onClick = onClickConfirm) {
Text(text = stringResource(MR.strings.action_ok))
Text(text = stringResource(R.string.action_ok))
}
},
onDismissRequest = onClickConfirm,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,15 +13,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
import eu.kanade.presentation.library.components.MangaComfortableGridItem
import eu.kanade.tachiyomi.R
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.manga.model.asMangaCover
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun GlobalSearchCardRow(
@ -37,7 +37,7 @@ fun GlobalSearchCardRow(
LazyRow(
contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
) {
items(titles) {
val title by getManga(it)
@ -78,7 +78,7 @@ private fun MangaItem(
@Composable
private fun EmptyResultItem() {
Text(
text = stringResource(MR.strings.no_results_found),
text = stringResource(R.string.no_results_found),
modifier = Modifier
.padding(
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.size
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.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
@ -21,11 +21,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
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.i18n.stringResource
@Composable
fun GlobalSearchResultItem(
@ -39,7 +39,7 @@ fun GlobalSearchResultItem(
modifier = Modifier
.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.extraSmall,
end = MaterialTheme.padding.tiny,
)
.fillMaxWidth()
.clickable(onClick = onClick),
@ -54,7 +54,7 @@ fun GlobalSearchResultItem(
Text(text = subtitle)
}
IconButton(onClick = onClick) {
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
}
}
content()
@ -92,7 +92,7 @@ fun GlobalSearchErrorResultItem(message: String?) {
Icon(imageVector = Icons.Outlined.Error, contentDescription = null)
Spacer(Modifier.height(4.dp))
Text(
text = message ?: stringResource(MR.strings.unknown_error),
text = message ?: stringResource(R.string.unknown_error),
textAlign = TextAlign.Center,
)
}

View File

@ -26,11 +26,11 @@ import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun GlobalSearchToolbar(
@ -58,7 +58,7 @@ fun GlobalSearchToolbar(
)
if (progress in 1..<total) {
LinearProgressIndicator(
progress = { progress / total.toFloat() },
progress = progress / total.toFloat(),
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(),
@ -85,7 +85,7 @@ fun GlobalSearchToolbar(
)
},
label = {
Text(text = stringResource(MR.strings.pinned_sources))
Text(text = stringResource(id = R.string.pinned_sources))
},
)
FilterChip(
@ -100,7 +100,7 @@ fun GlobalSearchToolbar(
)
},
label = {
Text(text = stringResource(MR.strings.all))
Text(text = stringResource(id = R.string.all))
},
)
@ -118,7 +118,7 @@ fun GlobalSearchToolbar(
)
},
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 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.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
val Category.visualName: String
@Composable
get() = when {
isSystemCategory -> stringResource(MR.strings.label_default)
isSystemCategory -> stringResource(R.string.label_default)
else -> name
}
fun Category.visualName(context: Context): String =
when {
isSystemCategory -> context.stringResource(MR.strings.label_default)
isSystemCategory -> context.getString(R.string.label_default)
else -> name
}

View File

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

View File

@ -25,28 +25,26 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.presentation.category.visualName
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.delay
import tachiyomi.core.preference.CheckboxState
import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.time.Duration.Companion.seconds
@Composable
fun CategoryCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
categories: ImmutableList<String>,
categories: List<Category>,
) {
var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.contains(name) }
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
AlertDialog(
onDismissRequest = onDismissRequest,
@ -58,32 +56,25 @@ fun CategoryCreateDialog(
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_add))
Text(text = stringResource(R.string.action_add))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_add_category))
Text(text = stringResource(R.string.action_add_category))
},
text = {
OutlinedTextField(
modifier = Modifier
.focusRequester(focusRequester),
modifier = Modifier.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
label = {
Text(text = stringResource(MR.strings.name))
},
label = { Text(text = stringResource(R.string.name)) },
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
MR.strings.error_category_exists
} else {
MR.strings.information_required_plain
}
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
Text(text = stringResource(msgRes))
},
isError = name.isNotEmpty() && nameAlreadyExists,
@ -103,14 +94,14 @@ fun CategoryCreateDialog(
fun CategoryRenameDialog(
onDismissRequest: () -> Unit,
onRename: (String) -> Unit,
categories: ImmutableList<String>,
category: String,
categories: List<Category>,
category: Category,
) {
var name by remember { mutableStateOf(category) }
var name by remember { mutableStateOf(category.name) }
var valueHasChanged by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.contains(name) }
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
AlertDialog(
onDismissRequest = onDismissRequest,
@ -122,16 +113,16 @@ fun CategoryRenameDialog(
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_rename_category))
Text(text = stringResource(R.string.action_rename_category))
},
text = {
OutlinedTextField(
@ -141,13 +132,9 @@ fun CategoryRenameDialog(
valueHasChanged = name != it
name = it
},
label = { Text(text = stringResource(MR.strings.name)) },
label = { Text(text = stringResource(R.string.name)) },
supportingText = {
val msgRes = if (valueHasChanged && nameAlreadyExists) {
MR.strings.error_category_exists
} else {
MR.strings.information_required_plain
}
val msgRes = if (valueHasChanged && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
Text(text = stringResource(msgRes))
},
isError = valueHasChanged && nameAlreadyExists,
@ -167,7 +154,7 @@ fun CategoryRenameDialog(
fun CategoryDeleteDialog(
onDismissRequest: () -> Unit,
onDelete: () -> Unit,
category: String,
category: Category,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
@ -176,19 +163,19 @@ fun CategoryDeleteDialog(
onDelete()
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.delete_category))
Text(text = stringResource(R.string.delete_category))
},
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()
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_sort_category))
Text(text = stringResource(R.string.action_sort_category))
},
text = {
Text(text = stringResource(MR.strings.sort_category_confirmation))
Text(text = stringResource(R.string.sort_category_confirmation))
},
)
}
@Composable
fun ChangeCategoryDialog(
initialSelection: ImmutableList<CheckboxState<Category>>,
initialSelection: List<CheckboxState<Category>>,
onDismissRequest: () -> Unit,
onEditCategories: () -> Unit,
onConfirm: (List<Long>, List<Long>) -> Unit,
@ -239,14 +226,14 @@ fun ChangeCategoryDialog(
onEditCategories()
},
) {
Text(text = stringResource(MR.strings.action_edit_categories))
Text(text = stringResource(R.string.action_edit_categories))
}
},
title = {
Text(text = stringResource(MR.strings.action_move_category))
Text(text = stringResource(R.string.action_move_category))
},
text = {
Text(text = stringResource(MR.strings.information_empty_category_dialog))
Text(text = stringResource(R.string.information_empty_category_dialog))
},
)
return
@ -260,31 +247,27 @@ fun ChangeCategoryDialog(
onDismissRequest()
onEditCategories()
}) {
Text(text = stringResource(MR.strings.action_edit))
Text(text = stringResource(R.string.action_edit))
}
Spacer(modifier = Modifier.weight(1f))
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(
onClick = {
onDismissRequest()
onConfirm(
selection
.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }
.map { it.value.id },
selection
.filter { it is CheckboxState.State.None || it is CheckboxState.TriState.None }
.map { it.value.id },
selection.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }.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 = {
Text(text = stringResource(MR.strings.action_move_category))
Text(text = stringResource(R.string.action_move_category))
},
text = {
Column(
@ -296,7 +279,7 @@ fun ChangeCategoryDialog(
if (index != -1) {
val mutableList = selection.toMutableList()
mutableList[index] = it.next()
selection = mutableList.toList().toImmutableList()
selection = mutableList.toList()
}
}
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp
@ -17,13 +16,11 @@ import tachiyomi.presentation.core.util.isScrollingUp
fun CategoryFloatingActionButton(
lazyListState: LazyListState,
onCreate: () -> Unit,
modifier: Modifier = Modifier,
) {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
text = { Text(text = stringResource(R.string.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
onClick = onCreate,
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.padding
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.ArrowDropUp
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -19,13 +19,14 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun CategoryListItem(
modifier: Modifier = Modifier,
category: Category,
canMoveUp: Boolean,
canMoveDown: Boolean,
@ -33,7 +34,6 @@ fun CategoryListItem(
onMoveDown: (Category) -> Unit,
onRename: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
ElevatedCard(
modifier = modifier,
@ -49,7 +49,7 @@ fun CategoryListItem(
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
Text(
text = category.name,
modifier = Modifier
@ -61,23 +61,20 @@ fun CategoryListItem(
onClick = { onMoveUp(category) },
enabled = canMoveUp,
) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
}
IconButton(
onClick = { onMoveDown(category) },
enabled = canMoveDown,
) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) {
Icon(
imageVector = Icons.Outlined.Edit,
contentDescription = stringResource(MR.strings.action_rename_category),
)
Icon(imageVector = Icons.Outlined.Edit, contentDescription = stringResource(R.string.action_rename_category))
}
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
fun AdaptiveSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
tonalElevation: Dp = 1.dp,
enableSwipeDismiss: Boolean = true,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit,
) {
val isTabletUi = isTabletUi()

View File

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

View File

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

View File

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

View File

@ -1,45 +1,44 @@
package eu.kanade.presentation.components
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.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.EmptyScreenAction
import tachiyomi.presentation.core.util.ThemePreviews
@PreviewLightDark
@ThemePreviews
@Composable
private fun NoActionPreview() {
TachiyomiPreviewTheme {
TachiyomiTheme {
Surface {
EmptyScreen(
stringRes = MR.strings.empty_screen,
textResource = R.string.empty_screen,
)
}
}
}
@PreviewLightDark
@ThemePreviews
@Composable
private fun WithActionPreview() {
TachiyomiPreviewTheme {
TachiyomiTheme {
Surface {
EmptyScreen(
stringRes = MR.strings.empty_screen,
actions = persistentListOf(
textResource = R.string.empty_screen,
actions = listOf(
EmptyScreenAction(
stringRes = MR.strings.action_retry,
stringResId = R.string.action_retry,
icon = Icons.Outlined.Refresh,
onClick = {},
),
EmptyScreenAction(
stringRes = MR.strings.getting_started_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
stringResId = R.string.getting_started_guide,
icon = Icons.Outlined.HelpOutline,
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.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -24,14 +24,14 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import kotlinx.collections.immutable.ImmutableList
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.TabIndicator
import tachiyomi.presentation.core.components.material.TabText
import tachiyomi.presentation.core.i18n.stringResource
object TabbedDialogPaddings {
val Horizontal = 24.dp
@ -40,9 +40,9 @@ object TabbedDialogPaddings {
@Composable
fun TabbedDialog(
onDismissRequest: () -> Unit,
tabTitles: ImmutableList<String>,
modifier: Modifier = Modifier,
onDismissRequest: () -> Unit,
tabTitles: List<String>,
tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null,
pagerState: PagerState = rememberPagerState { tabTitles.size },
content: @Composable (Int) -> Unit,
@ -55,9 +55,10 @@ fun TabbedDialog(
Column {
Row {
PrimaryTabRow(
TabRow(
modifier = Modifier.weight(1f),
selectedTabIndex = pagerState.currentPage,
indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },
divider = {},
) {
tabTitles.fastForEachIndexed { index, tab ->
@ -94,7 +95,7 @@ private fun MoreMenu(
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(MR.strings.label_more),
contentDescription = stringResource(R.string.label_more),
)
}
DropdownMenu(

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.components
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@ -19,20 +20,17 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.zIndex
import dev.icerock.moko.resources.StringResource
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.HorizontalPager
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.i18n.stringResource
@Composable
fun TabbedScreen(
titleRes: StringResource,
tabs: ImmutableList<TabContent>,
@StringRes titleRes: Int,
tabs: List<TabContent>,
startIndex: Int? = null,
searchQuery: String? = null,
onChangeSearchQuery: (String?) -> Unit = {},
@ -69,9 +67,9 @@ fun TabbedScreen(
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
),
) {
PrimaryTabRow(
TabRow(
selectedTabIndex = state.currentPage,
modifier = Modifier.zIndex(1f),
indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) },
) {
tabs.forEachIndexed { index, tab ->
Tab(
@ -98,9 +96,9 @@ fun TabbedScreen(
}
data class TabContent(
val titleRes: StringResource,
@StringRes val titleRes: Int,
val badgeNumber: Int? = null,
val searchEnabled: Boolean = false,
val actions: ImmutableList<AppBar.AppBarAction> = persistentListOf(),
val actions: List<AppBar.Action> = emptyList(),
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.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
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.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.CrashLogUtil
import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.InfoScreen
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
fun CrashScreen(
@ -32,22 +32,22 @@ fun CrashScreen(
InfoScreen(
icon = Icons.Outlined.BugReport,
headingText = stringResource(MR.strings.crash_screen_title),
subtitleText = stringResource(MR.strings.crash_screen_description, stringResource(MR.strings.app_name)),
acceptText = stringResource(MR.strings.pref_dump_crash_logs),
headingText = stringResource(R.string.crash_screen_title),
subtitleText = stringResource(R.string.crash_screen_description, stringResource(R.string.app_name)),
acceptText = stringResource(R.string.pref_dump_crash_logs),
onAcceptClick = {
scope.launch {
CrashLogUtil(context).dumpLogs()
}
},
rejectText = stringResource(MR.strings.crash_screen_restart_application),
rejectText = stringResource(R.string.crash_screen_restart_application),
onRejectClick = onRestartClick,
) {
Box(
modifier = Modifier
.padding(vertical = MaterialTheme.padding.small)
.clip(MaterialTheme.shapes.small)
.fillMaxSize()
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant),
) {
Text(
@ -60,10 +60,10 @@ fun CrashScreen(
}
}
@PreviewLightDark
@ThemePreviews
@Composable
private fun CrashScreenPreview() {
TachiyomiPreviewTheme {
TachiyomiTheme {
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.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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 eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.components.relativeDateText
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 kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
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
@Composable
@ -38,18 +41,19 @@ fun HistoryScreen(
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
) {
Scaffold(
topBar = { scrollBehavior ->
SearchToolbar(
titleContent = { AppBarTitle(stringResource(MR.strings.history)) },
titleContent = { AppBarTitle(stringResource(R.string.history)) },
searchQuery = state.searchQuery,
onChangeSearchQuery = onSearchQueryChange,
actions = {
AppBarActions(
persistentListOf(
listOf(
AppBar.Action(
title = stringResource(MR.strings.pref_clear_history),
title = stringResource(R.string.pref_clear_history),
icon = Icons.Outlined.DeleteSweep,
onClick = {
onDialogChange(HistoryScreenModel.Dialog.DeleteAll)
@ -68,12 +72,12 @@ fun HistoryScreen(
LoadingScreen(Modifier.padding(contentPadding))
} else if (it.isEmpty()) {
val msg = if (!state.searchQuery.isNullOrEmpty()) {
MR.strings.no_results_found
R.string.no_results_found
} else {
MR.strings.information_no_recent_manga
R.string.information_no_recent_manga
}
EmptyScreen(
stringRes = msg,
textResource = msg,
modifier = Modifier.padding(contentPadding),
)
} else {
@ -83,6 +87,7 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
preferences = preferences,
)
}
}
@ -96,7 +101,11 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit,
preferences: UiPreferences,
) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
@ -112,9 +121,11 @@ private fun HistoryScreenContent(
) { item ->
when (item) {
is HistoryUiModel.Header -> {
ListGroupHeader(
RelativeDateHeader(
modifier = Modifier.animateItemPlacement(),
text = relativeDateText(item.date),
date = item.date,
relativeTime = relativeTime,
dateFormat = dateFormat,
)
}
is HistoryUiModel.Item -> {
@ -137,13 +148,13 @@ sealed interface HistoryUiModel {
data class Item(val item: HistoryWithRelations) : HistoryUiModel
}
@PreviewLightDark
@ThemePreviews
@Composable
internal fun HistoryScreenPreviews(
@PreviewParameter(HistoryScreenModelStateProvider::class)
historyState: HistoryScreenModel.State,
) {
TachiyomiPreviewTheme {
TachiyomiTheme {
HistoryScreen(
state = historyState,
snackbarHostState = SnackbarHostState(),
@ -151,6 +162,17 @@ internal fun HistoryScreenPreviews(
onClickCover = {},
onClickResume = { _, _ -> run {} },
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.Column
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -11,12 +10,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import tachiyomi.i18n.MR
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
fun HistoryDeleteDialog(
@ -27,16 +26,16 @@ fun HistoryDeleteDialog(
AlertDialog(
title = {
Text(text = stringResource(MR.strings.action_remove))
Text(text = stringResource(R.string.action_remove))
},
text = {
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(
label = stringResource(MR.strings.dialog_with_checkbox_reset),
label = stringResource(R.string.dialog_with_checkbox_reset),
checked = removeEverything,
onCheckedChange = { removeEverything = it },
)
@ -48,12 +47,12 @@ fun HistoryDeleteDialog(
onDelete(removeEverything)
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_remove))
Text(text = stringResource(R.string.action_remove))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
)
@ -66,10 +65,10 @@ fun HistoryDeleteAllDialog(
) {
AlertDialog(
title = {
Text(text = stringResource(MR.strings.action_remove_everything))
Text(text = stringResource(R.string.action_remove_everything))
},
text = {
Text(text = stringResource(MR.strings.clear_history_confirmation))
Text(text = stringResource(R.string.clear_history_confirmation))
},
onDismissRequest = onDismissRequest,
confirmButton = {
@ -77,21 +76,21 @@ fun HistoryDeleteAllDialog(
onDelete()
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
)
}
@PreviewLightDark
@ThemePreviews
@Composable
private fun HistoryDeleteDialogPreview() {
TachiyomiPreviewTheme {
TachiyomiTheme {
HistoryDeleteDialog(
onDismissRequest = {},
onDelete = {},

View File

@ -11,35 +11,34 @@ import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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.unit.dp
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.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.toTimestampString
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.ThemePreviews
private val HistoryItemHeight = 96.dp
@Composable
fun HistoryItem(
modifier: Modifier = Modifier,
history: HistoryWithRelations,
onClickCover: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
@ -70,7 +69,7 @@ fun HistoryItem(
Text(
text = if (history.chapterNumber > -1) {
stringResource(
MR.strings.recent_manga_time,
R.string.recent_manga_time,
formatChapterNumber(history.chapterNumber),
readAt,
)
@ -85,21 +84,20 @@ fun HistoryItem(
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(MR.strings.action_delete),
contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@PreviewLightDark
@ThemePreviews
@Composable
private fun HistoryItemPreviews(
@PreviewParameter(HistoryWithRelationsProvider::class)
historyWithRelations: HistoryWithRelations,
) {
TachiyomiPreviewTheme {
Surface {
TachiyomiTheme {
HistoryItem(
history = historyWithRelations,
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.remember
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.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun DeleteLibraryMangaDialog(
@ -23,10 +22,10 @@ fun DeleteLibraryMangaDialog(
) {
var list by remember {
mutableStateOf(
buildList<CheckboxState.State<StringResource>> {
add(CheckboxState.State.None(MR.strings.manga_from_library))
buildList<CheckboxState.State<Int>> {
add(CheckboxState.State.None(R.string.manga_from_library))
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,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
@ -49,11 +48,11 @@ fun DeleteLibraryMangaDialog(
)
},
) {
Text(text = stringResource(MR.strings.action_ok))
Text(text = stringResource(R.string.action_ok))
}
},
title = {
Text(text = stringResource(MR.strings.action_remove))
Text(text = stringResource(R.string.action_remove))
},
text = {
Column {
@ -65,7 +64,7 @@ fun DeleteLibraryMangaDialog(
val index = list.indexOf(state)
if (index != -1) {
val mutableList = list.toMutableList()
mutableList[index] = state.next() as CheckboxState.State<StringResource>
mutableList[index] = state.next() as CheckboxState.State<Int>
list = mutableList.toList()
}
},

View File

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

View File

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

View File

@ -4,9 +4,9 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.zIndex
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import eu.kanade.tachiyomi.R
@Composable
internal fun GlobalSearchItem(
@ -19,7 +19,7 @@ internal fun GlobalSearchItem(
onClick = onClick,
) {
Text(
text = stringResource(MR.strings.action_global_search_query, searchQuery),
text = stringResource(R.string.action_global_search_query, searchQuery),
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.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
internal fun DownloadsBadge(count: Long) {
@ -47,10 +47,10 @@ internal fun LanguageBadge(
}
}
@PreviewLightDark
@ThemePreviews
@Composable
private fun BadgePreview() {
TachiyomiPreviewTheme {
TachiyomiTheme {
Column {
DownloadsBadge(count = 10)
UnreadBadge(count = 10)

View File

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

View File

@ -18,10 +18,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibraryItem
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.util.plus
@ -124,9 +124,9 @@ private fun LibraryPagerEmptyScreen(
onGlobalSearchClicked: () -> Unit,
) {
val msg = when {
!searchQuery.isNullOrEmpty() -> MR.strings.no_results_found
hasActiveFilters -> MR.strings.error_no_match
else -> MR.strings.information_no_manga_category
!searchQuery.isNullOrEmpty() -> R.string.no_results_found
hasActiveFilters -> R.string.error_no_match
else -> R.string.information_no_manga_category
}
Column(
@ -146,7 +146,7 @@ private fun LibraryPagerEmptyScreen(
}
EmptyScreen(
stringRes = msg,
textResource = msg,
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.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.presentation.category.visualName
import tachiyomi.domain.category.model.Category
import tachiyomi.presentation.core.components.material.TabIndicator
import tachiyomi.presentation.core.components.material.TabText
@Composable
@ -21,12 +20,11 @@ internal fun LibraryTabs(
getNumberOfMangaForCategory: (Category) -> Int?,
onTabItemClick: (Int) -> Unit,
) {
Column(
modifier = Modifier.zIndex(1f),
) {
PrimaryScrollableTabRow(
Column {
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp,
indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },
// TODO: use default when width is fixed upstream
// https://issuetracker.google.com/issues/242879624
divider = {},

View File

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

View File

@ -1,21 +1,13 @@
package eu.kanade.presentation.manga
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.rememberScrollState
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.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.TextButton
import androidx.compose.runtime.Composable
@ -23,23 +15,20 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.domain.manga.model.forceDownloaded
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import kotlinx.collections.immutable.persistentListOf
import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.TriState
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.active
@Composable
fun ChapterSettingsDialog(
@ -48,8 +37,6 @@ fun ChapterSettingsDialog(
onDownloadFilterChanged: (TriState) -> Unit,
onUnreadFilterChanged: (TriState) -> Unit,
onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
onSortModeChanged: (Long) -> Unit,
onDisplayModeChanged: (Long) -> Unit,
onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
@ -65,21 +52,21 @@ fun ChapterSettingsDialog(
TabbedDialog(
onDismissRequest = onDismissRequest,
tabTitles = persistentListOf(
stringResource(MR.strings.action_filter),
stringResource(MR.strings.action_sort),
stringResource(MR.strings.action_display),
tabTitles = listOf(
stringResource(R.string.action_filter),
stringResource(R.string.action_sort),
stringResource(R.string.action_display),
),
tabOverflowMenuContent = { closeMenu ->
DropdownMenuItem(
text = { Text(stringResource(MR.strings.set_chapter_settings_as_default)) },
text = { Text(stringResource(R.string.set_chapter_settings_as_default)) },
onClick = {
showSetAsDefaultDialog = true
closeMenu()
},
)
DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_reset)) },
text = { Text(stringResource(R.string.action_reset)) },
onClick = {
onResetToDefault()
closeMenu()
@ -96,14 +83,11 @@ fun ChapterSettingsDialog(
0 -> {
FilterPage(
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged
.takeUnless { manga?.forceDownloaded() == true },
onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { manga?.forceDownloaded() == true },
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
onUnreadFilterChanged = onUnreadFilterChanged,
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
onBookmarkedFilterChanged = onBookmarkedFilterChanged,
scanlatorFilterActive = scanlatorFilterActive,
onScanlatorFilterClicked = onScanlatorFilterClicked,
)
}
1 -> {
@ -132,57 +116,22 @@ private fun ColumnScope.FilterPage(
onUnreadFilterChanged: (TriState) -> Unit,
bookmarkedFilter: TriState,
onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
) {
TriStateItem(
label = stringResource(MR.strings.label_downloaded),
label = stringResource(R.string.label_downloaded),
state = downloadFilter,
onClick = onDownloadFilterChanged,
)
TriStateItem(
label = stringResource(MR.strings.action_filter_unread),
label = stringResource(R.string.action_filter_unread),
state = unreadFilter,
onClick = onUnreadFilterChanged,
)
TriStateItem(
label = stringResource(MR.strings.action_filter_bookmarked),
label = stringResource(R.string.action_filter_bookmarked),
state = bookmarkedFilter,
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
@ -192,10 +141,10 @@ private fun ColumnScope.SortPage(
onItemSelected: (Long) -> Unit,
) {
listOf(
MR.strings.sort_by_source to Manga.CHAPTER_SORTING_SOURCE,
MR.strings.sort_by_number to Manga.CHAPTER_SORTING_NUMBER,
MR.strings.sort_by_upload_date to Manga.CHAPTER_SORTING_UPLOAD_DATE,
MR.strings.action_sort_alpha to Manga.CHAPTER_SORTING_ALPHABET,
R.string.sort_by_source to Manga.CHAPTER_SORTING_SOURCE,
R.string.sort_by_number to Manga.CHAPTER_SORTING_NUMBER,
R.string.sort_by_upload_date to Manga.CHAPTER_SORTING_UPLOAD_DATE,
R.string.action_sort_alpha to Manga.CHAPTER_SORTING_ALPHABET,
).map { (titleRes, mode) ->
SortItem(
label = stringResource(titleRes),
@ -211,8 +160,8 @@ private fun ColumnScope.DisplayPage(
onItemSelected: (Long) -> Unit,
) {
listOf(
MR.strings.show_title to Manga.CHAPTER_DISPLAY_NAME,
MR.strings.show_chapter_number to Manga.CHAPTER_DISPLAY_NUMBER,
R.string.show_title to Manga.CHAPTER_DISPLAY_NAME,
R.string.show_chapter_number to Manga.CHAPTER_DISPLAY_NUMBER,
).map { (titleRes, mode) ->
RadioItem(
label = stringResource(titleRes),
@ -231,15 +180,15 @@ private fun SetAsDefaultDialog(
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.chapter_settings)) },
title = { Text(text = stringResource(R.string.chapter_settings)) },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(text = stringResource(MR.strings.confirm_set_chapter_settings))
Text(text = stringResource(R.string.confirm_set_chapter_settings))
LabeledCheckbox(
label = stringResource(MR.strings.also_set_chapter_settings_for_library),
label = stringResource(R.string.also_set_chapter_settings_for_library),
checked = optionalChecked,
onCheckedChange = { optionalChecked = it },
)
@ -247,7 +196,7 @@ private fun SetAsDefaultDialog(
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
@ -257,7 +206,7 @@ private fun SetAsDefaultDialog(
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.Spacer
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
@Composable
fun DuplicateMangaDialog(
@ -22,14 +21,14 @@ fun DuplicateMangaDialog(
AlertDialog(
onDismissRequest = onDismissRequest,
title = {
Text(text = stringResource(MR.strings.are_you_sure))
Text(text = stringResource(R.string.are_you_sure))
},
text = {
Text(text = stringResource(MR.strings.confirm_add_duplicate_manga))
Text(text = stringResource(R.string.confirm_add_duplicate_manga))
},
confirmButton = {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
TextButton(
onClick = {
@ -37,13 +36,13 @@ fun DuplicateMangaDialog(
onOpenManga()
},
) {
Text(text = stringResource(MR.strings.action_show_manga))
Text(text = stringResource(R.string.action_show_manga))
}
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
TextButton(
onClick = {
@ -51,7 +50,7 @@ fun DuplicateMangaDialog(
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.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
@ -26,7 +28,9 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
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.LocalHapticFeedback
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.fastAny
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.ChapterHeader
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.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaToolbar
import eu.kanade.presentation.manga.components.MissingChapterCountListItem
import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.manga.ChapterList
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.missingChaptersCount
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.TwoPanelBox
import tachiyomi.presentation.core.components.VerticalFastScroller
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.PullRefresh
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.isScrollingUp
import tachiyomi.source.local.isLocal
import java.time.Instant
import tachiyomi.presentation.core.util.secondaryItemAlpha
import java.text.DateFormat
import java.util.Date
@Composable
fun MangaScreen(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
nextUpdate: Instant?,
fetchInterval: Int?,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -94,7 +103,7 @@ fun MangaScreen(
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: () -> Unit,
onTrackingClicked: (() -> Unit)?,
// For tags menu
onTagSearch: (String) -> Unit,
@ -139,7 +148,9 @@ fun MangaScreen(
MangaScreenSmallImpl(
state = state,
snackbarHostState = snackbarHostState,
nextUpdate = nextUpdate,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onBackClicked = onBackClicked,
@ -174,9 +185,11 @@ fun MangaScreen(
MangaScreenLargeImpl(
state = state,
snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
nextUpdate = nextUpdate,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
@ -212,7 +225,9 @@ fun MangaScreen(
private fun MangaScreenSmallImpl(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
nextUpdate: Instant?,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
@ -221,7 +236,7 @@ private fun MangaScreenSmallImpl(
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: () -> Unit,
onTrackingClicked: (() -> Unit)?,
// For tags menu
onTagSearch: (String) -> Unit,
@ -258,12 +273,13 @@ private fun MangaScreenSmallImpl(
) {
val chapterListState = rememberLazyListState()
val (chapters, listItem, isAnySelected) = remember(state) {
Triple(
first = state.processedChapters,
second = state.chapterListItems,
third = state.isAnySelected,
)
val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember {
derivedStateOf {
chapters.fastAny { it.selected }
}
}
val internalOnBackPressed = {
@ -298,7 +314,7 @@ private fun MangaScreenSmallImpl(
title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.filterActive,
hasFilters = state.manga.chaptersFiltered(),
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterClicked,
onClickShare = onShareClicked,
@ -340,9 +356,7 @@ private fun MangaScreenSmallImpl(
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
)
Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
@ -356,8 +370,8 @@ private fun MangaScreenSmallImpl(
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = { !isAnySelected },
indicatorPadding = PaddingValues(top = topPadding),
enabled = !isAnySelected,
indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(),
) {
val layoutDirection = LocalLayoutDirection.current
VerticalFastScroller(
@ -400,7 +414,7 @@ private fun MangaScreenSmallImpl(
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
nextUpdate = nextUpdate,
fetchInterval = fetchInterval,
isUserIntervalMode = state.manga.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
@ -443,6 +457,8 @@ private fun MangaScreenSmallImpl(
manga = state.manga,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked,
@ -460,7 +476,9 @@ private fun MangaScreenSmallImpl(
fun MangaScreenLargeImpl(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
nextUpdate: Instant?,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
@ -469,7 +487,7 @@ fun MangaScreenLargeImpl(
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: () -> Unit,
onTrackingClicked: (() -> Unit)?,
// For tags menu
onTagSearch: (String) -> Unit,
@ -507,17 +525,27 @@ fun MangaScreenLargeImpl(
val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current
val (chapters, listItem, isAnySelected) = remember(state) {
Triple(
first = state.processedChapters,
second = state.chapterListItems,
third = state.isAnySelected,
)
val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember {
derivedStateOf {
chapters.fastAny { it.selected }
}
}
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
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 internalOnBackPressed = {
@ -539,7 +567,7 @@ fun MangaScreenLargeImpl(
title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive,
hasFilters = state.manga.chaptersFiltered(),
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked,
@ -586,11 +614,7 @@ fun MangaScreenLargeImpl(
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(
if (isReading) MR.strings.action_resume else MR.strings.action_start,
),
)
Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
@ -599,16 +623,6 @@ fun MangaScreenLargeImpl(
}
},
) { 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(
modifier = Modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
@ -636,7 +650,7 @@ fun MangaScreenLargeImpl(
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
nextUpdate = nextUpdate,
fetchInterval = fetchInterval,
isUserIntervalMode = state.manga.fetchInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
@ -686,6 +700,8 @@ fun MangaScreenLargeImpl(
manga = state.manga,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked,
@ -704,13 +720,13 @@ fun MangaScreenLargeImpl(
@Composable
private fun SharedMangaBottomActionMenu(
selected: List<ChapterList.Item>,
modifier: Modifier = Modifier,
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onMultiDeleteClicked: (List<Chapter>) -> Unit,
fillFraction: Float,
modifier: Modifier = Modifier,
) {
MangaBottomActionMenu(
visible = selected.isNotEmpty(),
@ -738,7 +754,7 @@ private fun SharedMangaBottomActionMenu(
onDeleteClicked = {
onMultiDeleteClicked(selected.fastMap { it.chapter })
}.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,
chapters: List<ChapterList>,
isAnyChapterSelected: Boolean,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit,
@ -765,27 +783,54 @@ private fun LazyListScope.sharedChapterItems(
contentType = { MangaScreenItem.CHAPTER },
) { item ->
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
when (item) {
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 -> {
MangaChapterListItem(
title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {
stringResource(
MR.strings.display_mode_chapter,
R.string.display_mode_chapter,
formatChapterNumber(item.chapter.chapterNumber),
)
} else {
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
.takeIf { !item.chapter.read && it > 0L }
?.let {
stringResource(
MR.strings.chapter_progress,
R.string.chapter_progress,
it + 1,
)
},
@ -793,7 +838,7 @@ private fun LazyListScope.sharedChapterItems(
read = item.chapter.read,
bookmark = item.chapter.bookmark,
selected = item.selected,
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
downloadIndicatorEnabled = !isAnyChapterSelected,
downloadStateProvider = { item.downloadState },
downloadProgressProvider = { item.downloadProgress },
chapterSwipeStartAction = chapterSwipeStartAction,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,36 +1,23 @@
package eu.kanade.presentation.manga.components
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.height
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.collections.immutable.toImmutableList
import eu.kanade.tachiyomi.R
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.i18n.MR
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
fun DeleteChaptersDialog(
@ -41,7 +28,7 @@ fun DeleteChaptersDialog(
onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
@ -51,14 +38,14 @@ fun DeleteChaptersDialog(
onConfirm()
},
) {
Text(text = stringResource(MR.strings.action_ok))
Text(text = stringResource(R.string.action_ok))
}
},
title = {
Text(text = stringResource(MR.strings.are_you_sure))
Text(text = stringResource(R.string.are_you_sure))
},
text = {
Text(text = stringResource(MR.strings.confirm_delete_chapters))
Text(text = stringResource(R.string.confirm_delete_chapters))
},
)
}
@ -66,84 +53,46 @@ fun DeleteChaptersDialog(
@Composable
fun SetIntervalDialog(
interval: Int,
nextUpdate: Instant?,
onDismissRequest: () -> Unit,
onValueChanged: ((Int) -> Unit)? = null,
onValueChanged: (Int) -> Unit,
) {
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(
onDismissRequest = onDismissRequest,
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
title = { Text(text = stringResource(R.string.manga_modify_calculated_interval_title)) },
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(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..FetchInterval.MAX_INTERVAL)
.map {
val items = (0..FetchInterval.MAX_INTERVAL).map {
if (it == 0) {
stringResource(MR.strings.label_default)
stringResource(R.string.label_default)
} else {
it.toString()
}
}
.toImmutableList()
WheelTextPicker(
items = items,
size = size,
items = items,
startIndex = selectedInterval,
onSelectionChanged = { selectedInterval = it },
)
}
}
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
TextButton(onClick = {
onValueChanged?.invoke(selectedInterval)
onValueChanged(selectedInterval)
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
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.Layout
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.TextOverflow
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.util.system.copyToClipboard
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton
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.secondaryItemAlpha
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable
fun MangaInfoBox(
modifier: Modifier = Modifier,
isTabletUi: Boolean,
appBarPadding: Dp,
title: String,
@ -105,7 +103,6 @@ fun MangaInfoBox(
status: Long,
onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
// Backdrop
@ -126,7 +123,7 @@ fun MangaInfoBox(
)
}
.blur(4.dp)
.alpha(0.2f),
.alpha(.2f),
)
// Manga & source info
@ -164,69 +161,55 @@ fun MangaInfoBox(
@Composable
fun MangaActionRow(
modifier: Modifier = Modifier,
favorite: Boolean,
trackingCount: Int,
nextUpdate: Instant?,
fetchInterval: Int?,
isUserIntervalMode: Boolean,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: () -> Unit,
onTrackingClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onEditCategory: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
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)) {
MangaActionButton(
title = if (favorite) {
stringResource(MR.strings.in_library)
stringResource(R.string.in_library)
} else {
stringResource(MR.strings.add_to_library)
stringResource(R.string.add_to_library)
},
icon = if (favorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onAddToLibraryClicked,
onLongClick = onEditCategory,
)
if (onEditIntervalClicked != null && fetchInterval != null) {
MangaActionButton(
title = when (nextUpdateDays) {
null -> stringResource(MR.strings.not_applicable)
0 -> stringResource(MR.strings.manga_interval_expected_update_soon)
else -> pluralStringResource(
MR.plurals.day,
count = nextUpdateDays,
nextUpdateDays,
)
},
title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
icon = Icons.Default.HourglassEmpty,
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = { onEditIntervalClicked?.invoke() },
onClick = onEditIntervalClicked,
)
}
if (onTrackingClicked != null) {
MangaActionButton(
title = if (trackingCount == 0) {
stringResource(MR.strings.manga_tracking_tab)
stringResource(R.string.manga_tracking_tab)
} 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,
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
onClick = onTrackingClicked,
)
}
if (onWebViewClicked != null) {
MangaActionButton(
title = stringResource(MR.strings.action_web_view),
title = stringResource(R.string.action_web_view),
icon = Icons.Outlined.Public,
color = defaultActionButtonColor,
onClick = onWebViewClicked,
@ -238,19 +221,19 @@ fun MangaActionRow(
@Composable
fun ExpandableMangaDescription(
modifier: Modifier = Modifier,
defaultExpandState: Boolean,
description: String?,
tagsProvider: () -> List<String>?,
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
val (expanded, onExpanded) = rememberSaveable {
mutableStateOf(defaultExpandState)
}
val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
description.takeIf { !it.isNullOrBlank() } ?: stringResource(R.string.description_placeholder)
val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
@ -280,14 +263,14 @@ fun ExpandableMangaDescription(
onDismissRequest = { showMenu = false },
) {
DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_search)) },
text = { Text(text = stringResource(R.string.action_search)) },
onClick = {
onTagSearch(tagSelected)
showMenu = false
},
)
DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_copy_to_clipboard)) },
text = { Text(text = stringResource(R.string.action_copy_to_clipboard)) },
onClick = {
onCopyTagToClipboard(tagSelected)
showMenu = false
@ -297,7 +280,7 @@ fun ExpandableMangaDescription(
if (expanded) {
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
tags.forEach {
TagsChip(
@ -313,7 +296,7 @@ fun ExpandableMangaDescription(
} else {
LazyRow(
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
) {
items(items = tags) {
TagsChip(
@ -354,7 +337,7 @@ private fun MangaAndSourceTitlesLarge(
MangaCover.Book(
modifier = Modifier.fillMaxWidth(0.65f),
data = coverDataProvider(),
contentDescription = stringResource(MR.strings.manga_cover),
contentDescription = stringResource(R.string.manga_cover),
onClick = onCoverClick,
)
Spacer(modifier = Modifier.height(16.dp))
@ -396,7 +379,7 @@ private fun MangaAndSourceTitlesSmall(
.sizeIn(maxWidth = 100.dp)
.align(Alignment.Top),
data = coverDataProvider(),
contentDescription = stringResource(MR.strings.manga_cover),
contentDescription = stringResource(R.string.manga_cover),
onClick = onCoverClick,
)
Column(
@ -416,19 +399,19 @@ private fun MangaAndSourceTitlesSmall(
}
@Composable
private fun ColumnScope.MangaContentInfo(
private fun MangaContentInfo(
title: String,
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
artist: String?,
status: Long,
sourceName: String,
isStubSource: Boolean,
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
) {
val context = LocalContext.current
Text(
text = title.ifBlank { stringResource(MR.strings.unknown_title) },
text = title.ifBlank { stringResource(R.string.unknown_title) },
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.clickableNoIndication(
onLongClick = {
@ -448,7 +431,7 @@ private fun ColumnScope.MangaContentInfo(
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@ -458,7 +441,7 @@ private fun ColumnScope.MangaContentInfo(
)
Text(
text = author?.takeIf { it.isNotBlank() }
?: stringResource(MR.strings.unknown_author),
?: stringResource(R.string.unknown_author),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.clickableNoIndication(
@ -479,7 +462,7 @@ private fun ColumnScope.MangaContentInfo(
if (!artist.isNullOrBlank() && author != artist) {
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@ -524,13 +507,13 @@ private fun ColumnScope.MangaContentInfo(
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
Text(
text = when (status) {
SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing)
SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed)
SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed)
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished)
SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled)
SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus)
else -> stringResource(MR.strings.unknown)
SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished)
SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
else -> stringResource(R.string.unknown)
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
@ -568,10 +551,7 @@ private fun MangaSummary(
expanded: Boolean,
modifier: Modifier = Modifier,
) {
val animProgress by animateFloatAsState(
targetValue = if (expanded) 1f else 0f,
label = "summary",
)
val animProgress by animateFloatAsState(if (expanded) 1f else 0f)
Layout(
modifier = modifier.clipToBounds(),
contents = listOf(
@ -607,9 +587,7 @@ private fun MangaSummary(
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon(
painter = rememberAnimatedVectorPainter(image, !expanded),
contentDescription = stringResource(
if (expanded) MR.strings.manga_info_collapse else MR.strings.manga_info_expand,
),
contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
tint = MaterialTheme.colorScheme.onBackground,
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.setValue
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.unit.dp
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.UpIcon
import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.theme.active
@Composable
fun MangaToolbar(
modifier: Modifier = Modifier,
title: String,
titleAlphaProvider: () -> Float,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
hasFilters: Boolean,
onBackClicked: () -> Unit,
onClickFilter: () -> Unit,
@ -44,14 +46,10 @@ fun MangaToolbar(
onClickEditCategory: (() -> Unit)?,
onClickRefresh: () -> Unit,
onClickMigrate: (() -> Unit)?,
// For action mode
actionModeCounter: Int,
onSelectAll: () -> Unit,
onInvertSelection: () -> Unit,
modifier: Modifier = Modifier,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
) {
Column(
modifier = modifier,
@ -63,25 +61,25 @@ fun MangaToolbar(
text = if (isActionMode) actionModeCounter.toString() else title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()),
modifier = Modifier.alpha(if (isActionMode) 1f else titleAlphaProvider()),
)
},
navigationIcon = {
IconButton(onClick = onBackClicked) {
UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode })
UpIcon(Icons.Outlined.Close.takeIf { isActionMode })
}
},
actions = {
if (isActionMode) {
AppBarActions(
persistentListOf(
listOf(
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
title = stringResource(R.string.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = onSelectAll,
),
AppBar.Action(
title = stringResource(MR.strings.action_select_inverse),
title = stringResource(R.string.action_select_inverse),
icon = Icons.Outlined.FlipToBack,
onClick = onInvertSelection,
),
@ -100,12 +98,11 @@ fun MangaToolbar(
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
actions = buildList {
if (onClickDownload != null) {
add(
AppBar.Action(
title = stringResource(MR.strings.manga_download),
title = stringResource(R.string.manga_download),
icon = Icons.Outlined.Download,
onClick = { downloadExpanded = !downloadExpanded },
),
@ -113,7 +110,7 @@ fun MangaToolbar(
}
add(
AppBar.Action(
title = stringResource(MR.strings.action_filter),
title = stringResource(R.string.action_filter),
icon = Icons.Outlined.FilterList,
iconTint = filterTint,
onClick = onClickFilter,
@ -121,14 +118,14 @@ fun MangaToolbar(
)
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_webview_refresh),
title = stringResource(R.string.action_webview_refresh),
onClick = onClickRefresh,
),
)
if (onClickEditCategory != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_edit_categories),
title = stringResource(R.string.action_edit_categories),
onClick = onClickEditCategory,
),
)
@ -136,7 +133,7 @@ fun MangaToolbar(
if (onClickMigrate != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_migrate),
title = stringResource(R.string.action_migrate),
onClick = onClickMigrate,
),
)
@ -144,13 +141,12 @@ fun MangaToolbar(
if (onClickShare != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
title = stringResource(R.string.action_share),
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
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
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.windowInsetsPadding
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.GetApp
import androidx.compose.material.icons.outlined.HelpOutline
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.Settings
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.graphics.vector.ImageVector
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 eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.more.DownloadQueueState
import tachiyomi.core.Constants
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun MoreScreen(
@ -58,7 +59,12 @@ fun MoreScreen(
),
) {
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 {
SwitchPreferenceWidget(
title = stringResource(MR.strings.label_downloaded_only),
subtitle = stringResource(MR.strings.downloaded_only_summary),
title = stringResource(R.string.label_downloaded_only),
subtitle = stringResource(R.string.downloaded_only_summary),
icon = Icons.Outlined.CloudOff,
checked = downloadedOnly,
onCheckedChanged = onDownloadedOnlyChange,
@ -80,8 +86,8 @@ fun MoreScreen(
}
item {
SwitchPreferenceWidget(
title = stringResource(MR.strings.pref_incognito_mode),
subtitle = stringResource(MR.strings.pref_incognito_mode_summary),
title = stringResource(R.string.pref_incognito_mode),
subtitle = stringResource(R.string.pref_incognito_mode_summary),
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
checked = incognitoMode,
onCheckedChanged = onIncognitoModeChange,
@ -93,17 +99,17 @@ fun MoreScreen(
item {
val downloadQueueState = downloadQueueStateProvider()
TextPreferenceWidget(
title = stringResource(MR.strings.label_download_queue),
title = stringResource(R.string.label_download_queue),
subtitle = when (downloadQueueState) {
DownloadQueueState.Stopped -> null
is DownloadQueueState.Paused -> {
val pending = downloadQueueState.pending
if (pending == 0) {
stringResource(MR.strings.paused)
stringResource(R.string.paused)
} else {
"${stringResource(MR.strings.paused)}${
"${stringResource(R.string.paused)}${
pluralStringResource(
MR.plurals.download_queue_summary,
id = R.plurals.download_queue_summary,
count = pending,
pending,
)
@ -112,7 +118,7 @@ fun MoreScreen(
}
is DownloadQueueState.Downloading -> {
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,
@ -121,21 +127,21 @@ fun MoreScreen(
}
item {
TextPreferenceWidget(
title = stringResource(MR.strings.categories),
icon = Icons.AutoMirrored.Outlined.Label,
title = stringResource(R.string.categories),
icon = Icons.Outlined.Label,
onPreferenceClick = onClickCategories,
)
}
item {
TextPreferenceWidget(
title = stringResource(MR.strings.label_stats),
title = stringResource(R.string.label_stats),
icon = Icons.Outlined.QueryStats,
onPreferenceClick = onClickStats,
)
}
item {
TextPreferenceWidget(
title = stringResource(MR.strings.label_data_storage),
title = stringResource(R.string.label_data_storage),
icon = Icons.Outlined.Storage,
onPreferenceClick = onClickDataAndStorage,
)
@ -145,22 +151,22 @@ fun MoreScreen(
item {
TextPreferenceWidget(
title = stringResource(MR.strings.label_settings),
title = stringResource(R.string.label_settings),
icon = Icons.Outlined.Settings,
onPreferenceClick = onClickSettings,
)
}
item {
TextPreferenceWidget(
title = stringResource(MR.strings.pref_category_about),
title = stringResource(R.string.pref_category_about),
icon = Icons.Outlined.Info,
onPreferenceClick = onClickAbout,
)
}
item {
TextPreferenceWidget(
title = stringResource(MR.strings.label_help),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
title = stringResource(R.string.label_help),
icon = Icons.Outlined.HelpOutline,
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
)
}

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