Compare commits

..

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

1038 changed files with 15734 additions and 41020 deletions

View File

@ -1,32 +1,12 @@
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{xml,sq,sqm}]
indent_size = 4
# noinspection EditorConfigKeyCorrectness
[*.{kt,kts}]
indent_size = 4
max_line_length = 120
indent_size = 4
insert_final_newline = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled
ktlint_standard_discouraged-comment-location = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-signature = disabled
# SY
ktlint_standard_filename = disabled
ktlint_standard_argument-list-wrapping = disabled
ktlint_standard_function-naming = disabled
@ -34,7 +14,3 @@ ktlint_standard_property-naming = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_comment-wrapping = disabled
ktlint_standard_max-line-length = disabled
ktlint_standard_type-argument-comment = disabled
ktlint_standard_value-argument-comment = disabled
ktlint_standard_value-parameter-comment = disabled

View File

@ -1,8 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: ❌ Help with Extensions
url: https://mihon.app/docs/faq/browse/extensions
about: For extension-related questions/issues
- name: 🖥️ Mihon website
url: https://mihon.app/
about: Guides, troubleshooting, and answers to common questions

View File

@ -43,9 +43,9 @@ body:
attributes:
label: Crash logs
description: |
If you're experiencing crashes, if possible, go to the app's **More → Settings → Advanced** page, press **Dump crash logs** and share the crash logs here.
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
placeholder: |
You can upload the crash log file as an attachment, or paste the crash logs in plain text if needed.
You can paste the crash logs in plain text or upload it as an attachment.
- type: input
id: tachiyomisy-version
@ -53,7 +53,7 @@ body:
label: TachiyomiSY version
description: You can find your TachiyomiSY version in **More → About**.
placeholder: |
Example: "1.12.0"
Example: "1.10.4"
validations:
required: true
@ -63,7 +63,7 @@ body:
label: Android version
description: You can find this somewhere in your Android settings.
placeholder: |
Example: "Android 14"
Example: "Android 11"
validations:
required: true
@ -73,7 +73,7 @@ body:
label: Device
description: List your device and model.
placeholder: |
Example: "Google Pixel 8"
Example: "Google Pixel 5"
validations:
required: true
@ -94,11 +94,11 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https:/mihon.app/docs/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[1.12.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
- label: I have updated the app to version **[1.10.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true
- label: I have filled out all of the requested information in this form, including specific version numbers.
- label: I have updated all installed extensions.
required: true
- label: I understand that **Mihon does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -31,7 +31,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to version **[1.12.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
- label: I have updated the app to version **[1.10.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -1,7 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"labels": ["Dependencies"],
"includePaths": [".github/workflows/*", "gradle/sy.versions.toml"],
"semanticCommits": "disabled"
}

View File

@ -6,8 +6,20 @@ concurrency:
cancel-in-progress: true
jobs:
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
build:
name: Build app
needs: check_wrapper
runs-on: ubuntu-latest
steps:
@ -15,19 +27,18 @@ jobs:
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
- name: Set up gradle
uses: gradle/actions/setup-gradle@v4
distribution: adopt
- name: Build app
run: ./gradlew spotlessCheck assembleDevDebug
uses: gradle/gradle-command-action@v2
with:
arguments: assembleDevDebug
- name: Upload APK
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: TachiyomiSY-${{ github.sha }}.apk
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk

View File

@ -17,6 +17,9 @@ jobs:
- name: Clone repo
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v2
- name: Setup Android SDK
run: |
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
@ -25,35 +28,22 @@ jobs:
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
distribution: adopt
- name: Set up gradle
uses: gradle/actions/setup-gradle@v4
# SY -->
uses: gradle/actions/setup-gradle@v3
# SY <--
- name: Write google-services.json
uses: DamianReeves/write-file-action@v1.3
uses: DamianReeves/write-file-action@v1.2
with:
path: app/google-services.json
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
write-mode: overwrite
# SY -->
- name: Write client_secrets.json
uses: DamianReeves/write-file-action@v1.3
with:
path: app/src/main/assets/client_secrets.json
contents: ${{ secrets.CLIENT_SECRETS_TEXT }}
write-mode: overwrite
# SY <--
- name: Check code format
run: ./gradlew spotlessCheck
- name: Build app
run: ./gradlew assembleStandardRelease
- name: Run unit tests
run: ./gradlew testReleaseUnitTest testStandardReleaseUnitTest
- name: Build app and run unit tests
run: ./gradlew assembleStandardRelease testStandardReleaseUnitTest --stacktrace
- name: Sign APK
uses: r0adkll/sign-android-release@v1
@ -63,8 +53,6 @@ jobs:
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: '35.0.1'
- name: Clean up build artifacts
run: |
@ -74,24 +62,24 @@ jobs:
sha=`sha256sum TachiyomiSY.apk | awk '{ print $1 }'`
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
mv app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk TachiyomiSY-arm64-v8a.apk
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk TachiyomiSY-arm64-v8a.apk
sha=`sha256sum TachiyomiSY-arm64-v8a.apk | awk '{ print $1 }'`
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
mv app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk TachiyomiSY-armeabi-v7a.apk
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk TachiyomiSY-armeabi-v7a.apk
sha=`sha256sum TachiyomiSY-armeabi-v7a.apk | awk '{ print $1 }'`
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
mv app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk TachiyomiSY-x86.apk
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk TachiyomiSY-x86.apk
sha=`sha256sum TachiyomiSY-x86.apk | awk '{ print $1 }'`
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
mv app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk TachiyomiSY-x86_64.apk
cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk TachiyomiSY-x86_64.apk
sha=`sha256sum TachiyomiSY-x86_64.apk | awk '{ print $1 }'`
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
- name: Create release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.run_number }}
name: TachiyomiSY

View File

@ -1,32 +1,30 @@
name: Remote Dispatch Action Initiator
on:
push:
branches:
- 'preview'
branches:
- 'master'
jobs:
trigger_preview_build:
name: Trigger preview build
if: ${{ github.ref == 'refs/heads/master' }}
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Set up gradle
uses: gradle/actions/setup-gradle@v4
- name: Create Tag
run: |
git tag "preview-${{ github.run_number }}"
git push origin "preview-${{ github.run_number }}"
- name: TAG - Bump version and push tag
uses: anothrNick/github-tag-action@1.67.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WITH_V: true
RELEASE_BRANCHES: master
DEFAULT_BUMP: patch
- name: PING - Dispatch initiating repository event
run: |
@ -34,6 +32,3 @@ jobs:
-H 'Accept: application/vnd.github.everest-preview+json' \
-u ${{ secrets.ACCESS_TOKEN }} \
--data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}'
- name: Run unit tests
run: ./gradlew testDebugUnitTest testDevDebugUnitTest

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Moderate issues
uses: tachiyomiorg/issue-moderator-action@v2.6.1
uses: tachiyomiorg/issue-moderator-action@v2.6.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
duplicate-label: Duplicate
@ -40,12 +40,6 @@ jobs:
"ignoreCase": true,
"labels": ["Cloudflare protected"],
"message": "Refer to the **Solving Cloudflare issues** section at https://mihon.app/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
},
{
"type": "both",
"regex": "^.*(myanimelist|mal).*$",
"ignoreCase": true,
"message": "For issues with linking MyAnimeList, please follow these steps:\n1. Update Mihon to version 0.16.4 or newer\n2. Change your default User-Agent (`More → Settings → Advanced → Default user agent string`)\n3. Close and restart Mihon\n4. Attempt to link MyAnimeList again\n\nIf you had MyAnimeList linked before, try to unlink it first before trying these steps."
}
]
auto-close-ignore-label: do-not-autoclose

19
.github/workflows/lock.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Lock threads
on:
# Daily
schedule:
- cron: '0 0 * * *'
# Manual trigger
workflow_dispatch:
inputs:
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: '2'
pr-inactive-days: '2'

View File

@ -1,26 +0,0 @@
name: Label PRs
on:
pull_request:
types: [opened]
jobs:
label_pr:
runs-on: ubuntu-latest
steps:
- name: Check PR and Add Label
uses: actions/github-script@v7
with:
script: |
const prAuthor = context.payload.pull_request.user.login;
if (prAuthor === 'weblate') {
const labels = ['Translations'];
await github.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: labels
});
}

36
.gitignore vendored
View File

@ -1,24 +1,24 @@
# Build files
.gradle
.kotlin
build
# IDE files
*.iml
/local.properties
/.idea/workspace.xml
.DS_Store
.idea/*
!.idea/icon.png
/captures
*iml
*.iml
/mainframer
/.mainframer
# Configuration files
local.properties
# macOS specific files
.DS_Store
# SY ignores
google-services.json
/app/src/main/assets/client_secrets.json
# Built files
*/build
/build
*.apk
app/**/output.json
# Custom ignores
/keys
# Unnecessary file
*.swp
TODO.md
CHANGELOG.md
/captures
build.sh

View File

@ -1,4 +1,4 @@
Looking to report an issue/bug or make a feature request? Please refer to the [README file](/README.md#issues-feature-requests-and-contributing).
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing).
---
@ -9,7 +9,7 @@ Thanks for your interest in contributing to Tachiyomi!
Pull requests are welcome!
If you're interested in taking on [an open issue](https://github.com/jobobby04/TachiyomiSY/issues), please comment on it so others are aware.
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
You do not need to ask for permission nor an assignment.
## Prerequisites
@ -24,41 +24,31 @@ Before you start, please note that the ability to use following technologies is
- [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled to test changes.
## Linting
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
## Getting help
- Join [the Discord server](https://discord.gg/mihon) for online help and to ask questions while developing.
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
# Translations
Translations are done externally via Weblate. See [our website](https://mihon.app/docs/contribute#translation) for more details.
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/docs/contribute#translation) for more details.
# Forks
Forks are allowed so long as they abide by [the project's LICENSE](/LICENSE).
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE).
When creating a fork, remember to:
- To avoid confusion with the main app:
- Change the app name
- Change the app icon
- Change or disable the [app update checker](/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
- To avoid installation conflicts:
- Change the `applicationId` in [`build.gradle.kts`](/app/build.gradle.kts)
### Supporting Cloud Sync - Google Drive Implementation
1. Go to [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project
3. Go to API & Services -> Library -> Google Drive API and click enable
4. Go to API & Services -> Oauth consent screen
5. Create it, fill in the app name, user support email, and developer contact information
6. In the next screen, click add or remove scopes, and add the `.../auth/drive.appdata` and `.../auth/drive.file` scopes
7. Don't add any test users and go back to the dashboard
8. Click publish
9. Go to API & Services -> Credentials
10. Click Create credentials -> Oauth client ID
11. Select Android, give it a name, and set `eu.kanade.google.oauth` as the package name
12. To get the SHA-1 key, run `keytool -printcert -jarfile app-standard-universal-release.apk` on your apk, and copy the listed SHA-1
13. Expand advanced settings, and enable Custom URL scheme
14. After that just download the json, name it to `client_secrets.json` and put it in `app/src/main/assets/`
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
- To avoid having your data polluting the main app's analytics and crash report services:
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own

View File

@ -1,20 +1,20 @@
| Preview Builds | Release Builds | Mihon Support Server |
| Preview Builds | Release Builds | Tachiyomi Support Server |
|-------|----------|----------|
| [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/1195734228319617024.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/mihon) |
# ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY
Mihon is a free and open source manga reader for Android 6.0 and above. This version of Mihon, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
Tachiyomi is a free and open source manga reader for Android 6.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
![screenshots of app](./.github/readme-images/screens.png)
## Features
Features of Mihon(original) include:
Features of Tachiyomi(original) include:
* Online reading from a variety of sources
* Local reading of downloaded content
* A configurable reader with multiple viewers, reading directions and other settings.
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
* Categories to organize your library
* Light and dark themes
* Schedule updating your library for new chapters
@ -42,6 +42,7 @@ Features of TachiyomiSY include:
* Page preload customization
* Customize image cache size
* Batch import of custom sources and featured extensions
* Automatic CAPTCHA solving
* Advanced source settings page, searching, enable/disable all
* Click tag for local search, long click tag for global search
* Merge multiple of the same manga from different sources
@ -67,22 +68,13 @@ Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/re
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
## Translation
Feel free to translate the project on [Weblate](https://hosted.weblate.org/projects/mihon/tachiyomisy/)
<details><summary>Translation Progress</summary>
<a href="https://hosted.weblate.org/engage/mihon/">
<img src="https://hosted.weblate.org/widgets/mihon/-/tachiyomisy/multi-auto.svg" alt="Translation status" />
</a>
</details>
## Issues, Feature Requests and Contributing
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not.
<details><summary>Issues</summary>
1. **Before reporting a new issue, take a look at the [FAQ](https://mihon.app/docs/faq/general), the [changelog](https://github.com/jobobby04/tachiyomisy/releases) and the already opened [issues](https://github.com/jobobby04/tachiyomisy/issues).**
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://github.com/jobobby04/tachiyomisy/releases) and the already opened [issues](https://github.com/jobobby04/tachiyomisy/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/1195734228319617024.svg)](https://discord.gg/mihon)
</details>
@ -97,7 +89,9 @@ Please make sure to read the full guidelines. Your issue may be closed without w
* If it could be device-dependent, try reproducing on another device (if possible)
* Don't group unrelated requests into one issue
Use the [issue forms](https://github.com/jobobby04/TachiyomiSY/issues/new/choose) to submit a bug.
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
</details>
@ -122,4 +116,4 @@ See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
## FAQ
[See our website.](https://mihon.app/)
You can also reach out to us on [Discord](https://discord.gg/mihon).
You can also reach out to us on [Discord](https://discord.gg/mihon).

4
app/.gitignore vendored Executable file
View File

@ -0,0 +1,4 @@
/build
*iml
*.iml
google-services.json

View File

@ -1,29 +1,24 @@
@file:Suppress("ChromeOsAbiSupport")
import mihon.buildlogic.getBuildTime
import mihon.buildlogic.getCommitCount
import mihon.buildlogic.getGitSha
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("mihon.android.application")
id("mihon.android.application.compose")
id("com.android.application")
id("com.mikepenz.aboutlibraries.plugin")
kotlin("android")
kotlin("plugin.parcelize")
kotlin("plugin.serialization")
// id("com.github.zellius.shortcut-helper")
alias(libs.plugins.aboutLibraries)
id("com.github.ben-manes.versions")
}
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
pluginManager.apply {
apply(libs.plugins.google.services.get().pluginId)
apply(libs.plugins.firebase.crashlytics.get().pluginId)
}
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
// Firebase Crashlytics
apply(plugin = "com.google.firebase.crashlytics")
}
// shortcutHelper.setFilePath("./shortcuts.xml")
val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android {
namespace = "eu.kanade.tachiyomi"
@ -31,16 +26,16 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy"
versionCode = 75
versionName = "1.12.0"
versionCode = 64
versionName = "1.10.4"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
ndk {
abiFilters += supportedAbis
abiFilters += SUPPORTED_ABIS
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@ -49,7 +44,7 @@ android {
abi {
isEnable = true
reset()
include(*supportedAbis.toTypedArray())
include(*SUPPORTED_ABIS.toTypedArray())
isUniversalApk = true
}
}
@ -71,8 +66,6 @@ android {
isMinifyEnabled = true
isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"")
}
create("benchmark") {
initWith(getByName("release"))
@ -101,6 +94,8 @@ android {
dimension = "default"
}
create("dev") {
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
dimension = "default"
}
}
@ -108,16 +103,13 @@ android {
packaging {
resources.excludes.addAll(
listOf(
"kotlin-tooling-metadata.json",
"META-INF/DEPENDENCIES",
"LICENSE.txt",
"META-INF/LICENSE",
"META-INF/**/LICENSE.txt",
"META-INF/*.properties",
"META-INF/**/*.properties",
"META-INF/LICENSE.txt",
"META-INF/README.md",
"META-INF/NOTICE",
"META-INF/*.version",
"META-INF/*.kotlin_module",
),
)
}
@ -128,6 +120,7 @@ android {
buildFeatures {
viewBinding = true
compose = true
buildConfig = true
// Disable some unused things
@ -140,23 +133,9 @@ android {
abortOnError = false
checkReleaseBuilds = false
}
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll(
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
composeOptions {
kotlinCompilerExtensionVersion = compose.versions.compiler.get()
}
}
@ -175,24 +154,26 @@ dependencies {
implementation(projects.presentationWidget)
// Compose
implementation(platform(compose.bom))
implementation(compose.activity)
implementation(compose.foundation)
implementation(compose.material3.core)
implementation(compose.material.core)
implementation(compose.material.icons)
implementation(compose.animation)
implementation(compose.animation.graphics)
debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util)
implementation(androidx.interpolator)
implementation(compose.accompanist.webview)
implementation(compose.accompanist.systemuicontroller)
implementation(androidx.paging.runtime)
implementation(androidx.paging.compose)
implementation(libs.bundles.sqlite)
// SY -->
implementation(sylibs.sqlcipher)
implementation(libs.sqlcipher)
// SY <--
implementation(kotlinx.reflect)
@ -234,12 +215,16 @@ dependencies {
// Disk
implementation(libs.disklrucache)
implementation(libs.unifile)
implementation(libs.junrar)
// SY -->
implementation(libs.zip4j)
// SY <--
// Preferences
implementation(libs.preferencektx)
// Dependency injection
implementation(libs.injekt)
implementation(libs.injekt.core)
// Image loading
implementation(platform(libs.coil.bom))
@ -257,23 +242,18 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation(libs.insetter)
implementation(libs.richeditor.compose)
implementation(libs.bundles.richtext)
implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager)
implementation(libs.compose.materialmotion)
implementation(libs.swipe)
implementation(libs.compose.webview)
implementation(libs.compose.grid)
implementation(libs.reorderable)
implementation(libs.bundles.markdown)
// Logging
implementation(libs.logcat)
// Crash reports/analytics
// "standardImplementation"(platform(libs.firebase.bom))
// "standardImplementation"(libs.firebase.analytics)
// "standardImplementation"(libs.firebase.crashlytics)
// implementation(libs.bundles.acra)
// "standardImplementation"(libs.firebase.analytics)
// Shizuku
implementation(libs.bundles.shizuku)
@ -285,16 +265,13 @@ dependencies {
// debugImplementation(libs.leakcanary.android)
implementation(libs.leakcanary.plumber)
testImplementation(kotlinx.coroutines.test)
// SY -->
// Text distance (EH)
implementation(sylibs.simularity)
// Firebase (EH)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics)
implementation(libs.firebase.crashlytics)
implementation(sylibs.firebase.analytics)
implementation(sylibs.firebase.crashlytics.ktx)
// Better logging (EH)
implementation(sylibs.xlog)
@ -302,20 +279,17 @@ dependencies {
// RatingBar (SY)
implementation(sylibs.ratingbar)
implementation(sylibs.composeRatingbar)
// Google drive
implementation(sylibs.google.api.services.drive)
implementation(sylibs.google.api.client.oauth)
// Koin
implementation(sylibs.koin.core)
implementation(sylibs.koin.android)
// ZXing Android Embedded
implementation(sylibs.zxing.android.embedded)
}
androidComponents {
beforeVariants { variantBuilder ->
// Disables standardBenchmark
if (variantBuilder.buildType == "benchmark") {
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
listOf("default" to "dev"),
)
}
}
onVariants(selector().withFlavor("default" to "standard")) {
// Only excluding in standard flavor because this breaks
// Layout Inspector's Compose tree
@ -323,6 +297,42 @@ androidComponents {
}
}
tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-Xcontext-receivers",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
)
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
)
}
}
}
buildscript {
dependencies {
classpath(kotlinx.gradle)

View File

@ -2,7 +2,6 @@
-keep,allowoptimization class eu.kanade.**
-keep,allowoptimization class tachiyomi.**
-keep,allowoptimization class mihon.**
# Keep common dependencies used in extensions
-keep,allowoptimization class androidx.preference.** { public protected *; }
@ -47,10 +46,6 @@
-dontnote rx.internal.util.PlatformDependent
##---------------End: proguard configuration for RxJava 1.x ----------
##---------------Begin: proguard configuration for okhttp ----------
-keepclasseswithmembers class okhttp3.MultipartBody$Builder { *; }
##---------------End: proguard configuration for okhttp ----------
##---------------Begin: proguard configuration for kotlinx.serialization ----------
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.** # core serialization annotations
@ -131,12 +126,6 @@
-keep class com.google.firebase.installations.** { *; }
-keep interface com.google.firebase.installations.** { *; }
# Google Drive
-keep class com.google.api.services.** { *; }
# Google OAuth
-keep class com.google.api.client.** { *; }
# SY -->
# SqlCipher
-keepclassmembers class net.zetetic.database.sqlcipher.SQLiteCustomFunction { *; }
@ -271,9 +260,6 @@
-keep,allowoptimization class * extends uy.kohesive.injekt.api.TypeReference
-keep,allowoptimization public class io.requery.android.database.sqlite.SQLiteConnection { *; }
# Keep apache http client
-keep class org.apache.http.** { *; }
# Suggested rules
-dontwarn com.oracle.svm.core.annotate.AutomaticFeature
-dontwarn com.oracle.svm.core.annotate.Delete
@ -286,16 +272,4 @@
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn java.lang.Module
-dontwarn org.graalvm.nativeimage.hosted.RuntimeResourceAccess
-dontwarn org.jspecify.annotations.NullMarked
-dontwarn javax.naming.InvalidNameException
-dontwarn javax.naming.NamingException
-dontwarn javax.naming.directory.Attribute
-dontwarn javax.naming.directory.Attributes
-dontwarn javax.naming.ldap.LdapName
-dontwarn javax.naming.ldap.Rdn
-dontwarn org.ietf.jgss.GSSContext
-dontwarn org.ietf.jgss.GSSCredential
-dontwarn org.ietf.jgss.GSSException
-dontwarn org.ietf.jgss.GSSManager
-dontwarn org.ietf.jgss.GSSName
-dontwarn org.ietf.jgss.Oid
-dontwarn org.jspecify.annotations.NullMarked

View File

@ -3,4 +3,4 @@
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon>
</adaptive-icon>

View File

@ -3,4 +3,4 @@
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon>
</adaptive-icon>

View File

@ -1,11 +0,0 @@
package mihon.core.firebase
import android.content.Context
object FirebaseConfig {
fun init(context: Context) = Unit
fun setAnalyticsEnabled(enabled: Boolean) = Unit
fun setCrashlyticsEnabled(enabled: Boolean) = Unit
}

View File

@ -34,23 +34,11 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Remove unnecessary permissions from Firebase dependency -->
<uses-permission
android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE"
tools:node="remove" />
<!-- Remove permission from Firebase dependency -->
<uses-permission
android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
<uses-permission
android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION"
tools:node="remove" />
<uses-permission
android:name="android.permission.ACCESS_ADSERVICES_AD_ID"
tools:node="remove" />
<application
android:name=".App"
android:allowBackup="false"
@ -200,20 +188,6 @@
<data android:host="shikimori-auth" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.GoogleDriveLoginActivity"
android:label="GoogleDrive"
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="eu.kanade.google.oauth" />
</intent-filter>
</activity>
<activity
android:name="exh.ui.login.EhLoginActivity"
@ -268,14 +242,6 @@
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<!-- Disable for manual opt-in -->
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<!-- Disable advertising ID collection for Firebase -->
<meta-data
android:name="google_analytics_adid_collection_enabled"
@ -359,7 +325,7 @@
<data android:scheme="https" />
<data android:scheme="http" />
<data android:host="pururin.me" />
<data android:host="pururin.io" />
<data android:pathPattern="/gallery/..*" />
</intent-filter>
@ -413,13 +379,6 @@
android:scheme="tachiyomisy" />
</intent-filter>
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
tools:remove="screenOrientation" />
</application>
<uses-sdk tools:overrideLibrary="rikka.shizuku.api"
tools:ignore="ManifestOrder" />
</manifest>

View File

@ -1,12 +1,11 @@
package eu.kanade.core.util
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators(
generator: (before: T?, after: T?) -> R?,
generator: (T?, T?) -> R?,
): List<R> {
if (isEmpty()) return emptyList()
val newList = mutableListOf<R>()
@ -20,24 +19,6 @@ fun <T : R, R : Any> List<T>.insertSeparators(
return newList
}
/**
* Similar to [eu.kanade.core.util.insertSeparators] but iterates from last to first element
*/
fun <T : R, R : Any> List<T>.insertSeparatorsReversed(
generator: (before: T?, after: T?) -> R?,
): List<R> {
if (isEmpty()) return emptyList()
val newList = mutableListOf<R>()
for (i in size downTo 0) {
val after = getOrNull(i)
after?.let(newList::add)
val before = getOrNull(i - 1)
val separator = generator.invoke(before, after)
separator?.let(newList::add)
}
return newList.asReversed()
}
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) {
add(value)
@ -46,6 +27,21 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
}
}
/**
* Returns a list containing only elements matching the given [predicate].
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) }
val destination = ArrayList<T>()
fastForEach { if (predicate(it)) destination.add(it) }
return destination
}
/**
* Returns a list containing all elements not matching the given [predicate].
*
@ -56,7 +52,27 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) }
return fastFilter { !predicate(it) }
val destination = ArrayList<T>()
fastForEach { if (!predicate(it)) destination.add(it) }
return destination
}
/**
* Returns a list containing only the non-null results of applying the
* given [transform] function to each element in the original collection.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
contract { callsInPlace(transform) }
val destination = ArrayList<R>()
fastForEach { element ->
transform(element)?.let(destination::add)
}
return destination
}
/**
@ -97,3 +113,26 @@ inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
fastForEach { if (predicate(it)) --count }
return count
}
/**
* Returns a list containing only elements from the given collection
* having distinct keys returned by the given [selector] function.
*
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
* The elements in the resulting list are in the same order as they were in the source collection.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
contract { callsInPlace(selector) }
val set = HashSet<K>()
val list = ArrayList<T>()
fastForEach {
val key = selector(it)
if (set.add(key)) list.add(it)
}
return list
}

View File

@ -4,7 +4,10 @@ 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
@ -13,11 +16,9 @@ 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
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleIncognito
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
@ -25,20 +26,6 @@ import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.tachiyomi.di.InjektModule
import eu.kanade.tachiyomi.di.addFactory
import eu.kanade.tachiyomi.di.addSingletonFactory
import mihon.data.repository.ExtensionRepoRepositoryImpl
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import mihon.domain.extensionrepo.service.ExtensionRepoService
import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl
import tachiyomi.data.history.HistoryRepositoryImpl
@ -82,7 +69,6 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.service.ReleaseService
@ -97,7 +83,11 @@ import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.repository.TrackRepository
import tachiyomi.domain.updates.interactor.GetUpdates
import tachiyomi.domain.updates.repository.UpdatesRepository
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class DomainModule : InjektModule {
@ -111,7 +101,7 @@ class DomainModule : InjektModule {
addFactory { RenameCategory(get()) }
addFactory { ReorderCategory(get()) }
addFactory { UpdateCategory(get()) }
addFactory { DeleteCategory(get(), get(), get()) }
addFactory { DeleteCategory(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) }
@ -121,7 +111,6 @@ class DomainModule : InjektModule {
addFactory { GetMangaByUrlAndSourceId(get()) }
addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get(), get()) }
addFactory { GetUpcomingManga(get()) }
addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
addFactory { FetchInterval(get()) }
@ -129,7 +118,6 @@ class DomainModule : InjektModule {
addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get(), get()) }
addFactory { UpdateMangaNotes(get()) }
addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) }
@ -154,9 +142,8 @@ class DomainModule : InjektModule {
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) }
addFactory { FilterChaptersForDownload(get(), get(), get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) }
@ -184,17 +171,10 @@ class DomainModule : InjektModule {
addFactory { ToggleLanguage(get()) }
addFactory { ToggleSource(get()) }
addFactory { ToggleSourcePin(get()) }
addFactory { TrustExtension(get(), get()) }
addFactory { TrustExtension(get()) }
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
addFactory { ExtensionRepoService(get(), get()) }
addFactory { GetExtensionRepo(get()) }
addFactory { GetExtensionRepoCount(get()) }
addFactory { CreateExtensionRepo(get(), get()) }
addFactory { CreateExtensionRepo(get()) }
addFactory { DeleteExtensionRepo(get()) }
addFactory { ReplaceExtensionRepo(get()) }
addFactory { UpdateExtensionRepo(get(), get()) }
addFactory { ToggleIncognito(get()) }
addFactory { GetIncognitoState(get(), get(), get()) }
addFactory { GetExtensionRepos(get()) }
}
}

View File

@ -14,9 +14,6 @@ import eu.kanade.domain.source.interactor.GetSourceCategories
import eu.kanade.domain.source.interactor.RenameSourceCategory
import eu.kanade.domain.source.interactor.SetSourceCategories
import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver
import eu.kanade.tachiyomi.di.InjektModule
import eu.kanade.tachiyomi.di.addFactory
import eu.kanade.tachiyomi.di.addSingletonFactory
import eu.kanade.tachiyomi.source.online.MetadataSource
import exh.search.SearchEngine
import tachiyomi.data.manga.CustomMangaRepositoryImpl
@ -28,6 +25,7 @@ import tachiyomi.data.source.SavedSearchRepositoryImpl
import tachiyomi.domain.chapter.interactor.DeleteChapters
import tachiyomi.domain.chapter.interactor.GetChapterByUrl
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
import tachiyomi.domain.history.interactor.GetHistoryByMangaId
import tachiyomi.domain.manga.interactor.DeleteByMergeId
import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries
import tachiyomi.domain.manga.interactor.DeleteMangaById
@ -44,7 +42,7 @@ import tachiyomi.domain.manga.interactor.GetMergedManga
import tachiyomi.domain.manga.interactor.GetMergedMangaById
import tachiyomi.domain.manga.interactor.GetMergedMangaForDownloading
import tachiyomi.domain.manga.interactor.GetMergedReferencesById
import tachiyomi.domain.manga.interactor.GetReadMangaNotInLibraryView
import tachiyomi.domain.manga.interactor.GetReadMangaNotInLibrary
import tachiyomi.domain.manga.interactor.GetSearchMetadata
import tachiyomi.domain.manga.interactor.GetSearchTags
import tachiyomi.domain.manga.interactor.GetSearchTitles
@ -73,7 +71,11 @@ import tachiyomi.domain.source.interactor.InsertSavedSearch
import tachiyomi.domain.source.repository.FeedSavedSearchRepository
import tachiyomi.domain.source.repository.SavedSearchRepository
import tachiyomi.domain.track.interactor.IsTrackUnfollowed
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
class SYDomainModule : InjektModule {
@ -87,6 +89,7 @@ class SYDomainModule : InjektModule {
addFactory { DeleteChapters(get()) }
addFactory { DeleteMangaById(get()) }
addFactory { FilterSerializer() }
addFactory { GetHistoryByMangaId(get()) }
addFactory { GetChapterByUrl(get()) }
addFactory { GetSourceCategories(get()) }
addFactory { CreateSourceCategory(get()) }
@ -99,7 +102,7 @@ class SYDomainModule : InjektModule {
addFactory { GetPagePreviews(get(), get()) }
addFactory { SearchEngine() }
addFactory { IsTrackUnfollowed() }
addFactory { GetReadMangaNotInLibraryView(get()) }
addFactory { GetReadMangaNotInLibrary(get()) }
// Required for [MetadataSource]
addFactory<MetadataSource.GetMangaId> { GetManga(get()) }

View File

@ -2,7 +2,8 @@ package eu.kanade.domain.base
import android.content.Context
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.i18n.MR
@ -21,6 +22,8 @@ class BasePreferences(
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
@ -29,10 +32,4 @@ class BasePreferences(
SHIZUKU(MR.strings.ext_installer_shizuku, false),
PRIVATE(MR.strings.ext_installer_private, false),
}
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false)
}

View File

@ -20,7 +20,6 @@ import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.chapter.service.ChapterRecognition
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal
import java.lang.Long.max
@ -36,7 +35,6 @@ class SyncChaptersWithSource(
private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators,
private val libraryPreferences: LibraryPreferences,
) {
/**
@ -152,18 +150,12 @@ class SyncChaptersWithSource(
return emptyList()
}
val changedOrDuplicateReadUrls = mutableSetOf<String>()
val reAdded = mutableListOf<Chapter>()
val deletedChapterNumbers = TreeSet<Double>()
val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
val readChapterNumbers = dbChapters
.asSequence()
.filter { it.read && it.isRecognizedNumber }
.map { it.chapterNumber }
.toSet()
removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
@ -173,20 +165,12 @@ class SyncChaptersWithSource(
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to it.dateFetch }
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get()
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW)
// 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--)
if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) {
changedOrDuplicateReadUrls.add(chapter.url)
chapter = chapter.copy(read = true)
}
if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
chapter = chapter.copy(
@ -199,19 +183,19 @@ class SyncChaptersWithSource(
chapter = chapter.copy(dateFetch = it)
}
changedOrDuplicateReadUrls.add(chapter.url)
reAdded.add(chapter)
chapter
}
// --> EXH (carry over reading progress)
if (manga.isEhBasedManga()) {
val finalAdded = updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls }
val finalAdded = updatedToAdd.subtract(reAdded)
if (finalAdded.isNotEmpty()) {
val max = dbChapters.maxOfOrNull { it.lastPageRead }
if (max != null && max > 0) {
updatedToAdd = updatedToAdd.map {
if (it.url !in changedOrDuplicateReadUrls) {
if (it !in reAdded) {
it.copy(lastPageRead = max)
} else {
it
@ -241,8 +225,12 @@ class SyncChaptersWithSource(
// Note that last_update actually represents last time the chapter list changed at all
updateManga.awaitUpdateLastUpdate(manga.id)
val reAddedUrls = reAdded.map { it.url }.toHashSet()
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
return updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
return updatedToAdd.filterNot {
it.url in reAddedUrls || it.scanlator in excludedScanlators
}
}
}

View File

@ -39,5 +39,4 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
it.date_upload = dateUpload
it.chapter_number = chapterNumber.toFloat()
it.source_order = sourceOrder.toInt()
it.last_modified = lastModifiedAt
}

View File

@ -0,0 +1,25 @@
package eu.kanade.domain.extension.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.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

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

View File

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

View File

@ -20,7 +20,7 @@ class GetExtensionsByType(
extensionManager.installedExtensionsFlow,
extensionManager.untrustedExtensionsFlow,
extensionManager.availableExtensionsFlow,
) { enabledLanguages, _installed, _untrusted, _available ->
) { _activeLanguages, _installed, _untrusted, _available ->
val (updates, installed) = _installed
.filter { (showNsfwSources || !it.isNsfw) }
.sortedWith(
@ -41,9 +41,9 @@ class GetExtensionsByType(
}
.flatMap { ext ->
if (ext.sources.isEmpty()) {
return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
return@flatMap if (ext.lang in _activeLanguages) listOf(ext) else emptyList()
}
ext.sources.filter { it.lang in enabledLanguages }
ext.sources.filter { it.lang in _activeLanguages }
.map {
ext.copy(
name = it.name,

View File

@ -3,18 +3,15 @@ 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 mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.core.common.preference.getAndSet
class TrustExtension(
private val extensionRepoRepository: ExtensionRepoRepository,
private val preferences: SourcePreferences,
) {
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
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) {
@ -22,7 +19,9 @@ class TrustExtension(
// Remove previously trusted versions
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
removed.also { it += "$pkgName:$versionCode:$signatureHash" }
removed.also {
it += "$pkgName:$versionCode:$signatureHash"
}
}
}

View File

@ -23,7 +23,7 @@ class GetPagePreviews(
return try {
val pagePreviews = try {
pagePreviewCache.getPageListFromCache(manga, chapterIds, page)
} catch (_: Exception) {
} catch (e: Exception) {
source.getPagePreviewList(manga.toSManga(), chapters.map { it.toSChapter() }, page).also {
pagePreviewCache.putPageListToCache(manga, chapterIds, it)
}

View File

@ -4,7 +4,6 @@ import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
@ -33,8 +32,9 @@ class UpdateManga(
remoteManga: SManga,
manualFetch: Boolean,
coverCache: CoverCache = Injekt.get(),
libraryPreferences: LibraryPreferences = Injekt.get(),
// SY -->
downloadManager: DownloadManager = Injekt.get(),
// SY <--
): Boolean {
val remoteTitle = try {
remoteManga.title
@ -42,13 +42,14 @@ class UpdateManga(
""
}
// if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db
val title =
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
remoteTitle
} else {
null
}
// SY -->
val title = if (remoteTitle.isNotBlank() && localManga.ogTitle != remoteTitle) {
downloadManager.renameMangaDir(localManga.ogTitle, remoteTitle, localManga.source)
remoteTitle
} else {
null
}
// SY <--
val coverLastModified =
when {
@ -68,7 +69,7 @@ class UpdateManga(
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
val success = mangaRepository.update(
return mangaRepository.update(
MangaUpdate(
id = localManga.id,
title = title,
@ -83,10 +84,6 @@ class UpdateManga(
initialized = true,
),
)
if (success && title != null) {
downloadManager.renameManga(localManga, title)
}
return success
}
suspend fun awaitUpdateFetchInterval(

View File

@ -23,7 +23,7 @@ val Manga.readerOrientation: Long
val Manga.downloadedFilter: TriState
get() {
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
if (forceDownloaded()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) {
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
@ -35,17 +35,18 @@ fun Manga.chaptersFiltered(): Boolean {
downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED
}
fun Manga.forceDownloaded(): Boolean {
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
}
fun Manga.toSManga(): SManga = SManga.create().also {
it.url = url
// SY -->
it.title = ogTitle
it.artist = ogArtist
it.author = ogAuthor
it.description = ogDescription
it.genre = ogGenre.orEmpty().joinToString()
it.status = ogStatus.toInt()
// SY <--
it.title = title
it.artist = artist
it.author = author
it.description = description
it.genre = genre.orEmpty().joinToString()
it.status = status.toInt()
it.thumbnail_url = thumbnailUrl
it.initialized = initialized
}
@ -78,6 +79,24 @@ fun Manga.copyFrom(other: SManga): Manga {
)
}
fun SManga.toDomainManga(sourceId: Long): Manga {
return Manga.create().copy(
url = url,
// SY -->
ogTitle = title,
ogArtist = artist,
ogAuthor = author,
ogThumbnailUrl = thumbnail_url,
ogDescription = description,
ogGenre = getGenres(),
ogStatus = status.toLong(),
// SY <--
updateStrategy = update_strategy,
initialized = initialized,
source = sourceId,
)
}
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
return coverCache.getCustomCoverFile(id).exists()
}
@ -85,13 +104,7 @@ fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
/**
* Creates a ComicInfo instance based on the manga and chapter metadata.
*/
fun getComicInfo(
manga: Manga,
chapter: Chapter,
urls: List<String>,
categories: List<String>?,
sourceName: String,
) = ComicInfo(
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories: List<String>?) = ComicInfo(
title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title),
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
@ -101,7 +114,7 @@ fun getComicInfo(
ComicInfo.Number(it.toString())
}
},
web = ComicInfo.Web(urls.joinToString(" ")),
web = ComicInfo.Web(chapterUrl),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) },
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
@ -111,7 +124,6 @@ fun getComicInfo(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
source = ComicInfo.SourceMihon(sourceName),
// SY -->
padding = CbzCrypto.createComicInfoPadding()?.let { ComicInfo.PaddingTachiyomiSY(it) },
// SY <--

View File

@ -32,11 +32,10 @@ class GetEnabledSources(
) { a, b, c -> Triple(a, b, c) },
// SY <--
repository.getSources(),
) {
pinnedSourceIds,
(enabledLanguages, disabledSources, lastUsedSource),
(excludedFromDataSaver, sourcesInCategories, sourceCategoriesFilter),
sources,
) { pinnedSourceIds,
(enabledLanguages, disabledSources, lastUsedSource),
(excludedFromDataSaver, sourcesInCategories, sourceCategoriesFilter),
sources,
->
val sourcesAndCategories = sourcesInCategories.map {

View File

@ -1,35 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
class GetIncognitoState(
private val basePreferences: BasePreferences,
private val sourcePreferences: SourcePreferences,
private val extensionManager: ExtensionManager,
) {
fun await(sourceId: Long?): Boolean {
if (basePreferences.incognitoMode().get()) return true
if (sourceId == null) return false
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
return extensionPackage in sourcePreferences.incognitoExtensions().get()
}
fun subscribe(sourceId: Long?): Flow<Boolean> {
if (sourceId == null) return basePreferences.incognitoMode().changes()
return combine(
basePreferences.incognitoMode().changes(),
sourcePreferences.incognitoExtensions().changes(),
extensionManager.getExtensionPackageAsFlow(sourceId),
) { incognito, incognitoExtensions, extensionPackage ->
incognito || (extensionPackage in incognitoExtensions)
}
.distinctUntilChanged()
}
}

View File

@ -1,14 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
class ToggleIncognito(
private val preferences: SourcePreferences,
) {
fun await(extensions: String, enable: Boolean) {
preferences.incognitoExtensions().getAndSet {
if (enable) it.plus(extensions) else it.minus(extensions)
}
}
}

View File

@ -22,8 +22,6 @@ class SourcePreferences(
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedSource() = preferenceStore.getLong(
@ -51,11 +49,6 @@ class SourcePreferences(
emptySet(),
)
fun globalSearchFilterState() = preferenceStore.getBoolean(
Preference.appStateKey("has_filters_toggle_state"),
false,
)
// SY -->
fun enableSourceBlacklist() = preferenceStore.getBoolean("eh_enable_source_blacklist", true)
@ -88,32 +81,5 @@ class SourcePreferences(
BANDWIDTH_HERO,
WSRV_NL,
}
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
fun defaultMangaOrder() = preferenceStore.getString("default_manga_order", "")
fun migrationSources() = preferenceStore.getString("migrate_sources", "")
fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false)
fun useSourceWithMost() = preferenceStore.getBoolean("use_source_with_most", false)
fun skipPreMigration() = preferenceStore.getBoolean(Preference.appStateKey("skip_pre_migration"), false)
fun hideNotFoundMigration() = preferenceStore.getBoolean("hide_not_found_migration", false)
fun showOnlyUpdatesMigration() = preferenceStore.getBoolean("show_only_updates_migration", false)
fun allowLocalSourceHiddenFolders() = preferenceStore.getBoolean("allow_local_source_hidden_folders", false)
fun preferredMangaDexId() = preferenceStore.getString("preferred_mangaDex_id", "0")
fun mangadexSyncToLibraryIndexes() = preferenceStore.getStringSet(
"pref_mangadex_sync_to_library_indexes",
emptySet(),
)
fun recommendationSearchFlags() = preferenceStore.getInt("rec_search_flags", Int.MAX_VALUE)
// SY <--
}

View File

@ -1,105 +0,0 @@
package eu.kanade.domain.sync
import eu.kanade.domain.sync.models.SyncSettings
import eu.kanade.tachiyomi.data.sync.models.SyncTriggerOptions
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import java.util.UUID
class SyncPreferences(
private val preferenceStore: PreferenceStore,
) {
fun clientHost() = preferenceStore.getString("sync_client_host", "https://sync.tachiyomi.org")
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
fun syncInterval() = preferenceStore.getInt("sync_interval", 0)
fun syncService() = preferenceStore.getInt("sync_service", 0)
fun googleDriveAccessToken() = preferenceStore.getString(
Preference.appStateKey("google_drive_access_token"),
"",
)
fun googleDriveRefreshToken() = preferenceStore.getString(
Preference.appStateKey("google_drive_refresh_token"),
"",
)
fun uniqueDeviceID(): String {
val uniqueIDPreference = preferenceStore.getString(Preference.appStateKey("unique_device_id"), "")
// Retrieve the current value of the preference
var uniqueID = uniqueIDPreference.get()
if (uniqueID.isBlank()) {
uniqueID = UUID.randomUUID().toString()
uniqueIDPreference.set(uniqueID)
}
return uniqueID
}
fun isSyncEnabled(): Boolean {
return syncService().get() != 0
}
fun getSyncSettings(): SyncSettings {
return SyncSettings(
libraryEntries = preferenceStore.getBoolean("library_entries", true).get(),
categories = preferenceStore.getBoolean("categories", true).get(),
chapters = preferenceStore.getBoolean("chapters", true).get(),
tracking = preferenceStore.getBoolean("tracking", true).get(),
history = preferenceStore.getBoolean("history", true).get(),
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(),
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
// SY -->
customInfo = preferenceStore.getBoolean("customInfo", true).get(),
readEntries = preferenceStore.getBoolean("readEntries", true).get(),
savedSearches = preferenceStore.getBoolean("savedSearches", true).get(),
// SY <--
)
}
fun setSyncSettings(syncSettings: SyncSettings) {
preferenceStore.getBoolean("library_entries", true).set(syncSettings.libraryEntries)
preferenceStore.getBoolean("categories", true).set(syncSettings.categories)
preferenceStore.getBoolean("chapters", true).set(syncSettings.chapters)
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
preferenceStore.getBoolean("history", true).set(syncSettings.history)
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings)
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
// SY -->
preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo)
preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries)
preferenceStore.getBoolean("savedSearches", true).set(syncSettings.savedSearches)
// SY <--
}
fun getSyncTriggerOptions(): SyncTriggerOptions {
return SyncTriggerOptions(
syncOnChapterRead = preferenceStore.getBoolean("sync_on_chapter_read", false).get(),
syncOnChapterOpen = preferenceStore.getBoolean("sync_on_chapter_open", false).get(),
syncOnAppStart = preferenceStore.getBoolean("sync_on_app_start", false).get(),
syncOnAppResume = preferenceStore.getBoolean("sync_on_app_resume", false).get(),
)
}
fun setSyncTriggerOptions(syncTriggerOptions: SyncTriggerOptions) {
preferenceStore.getBoolean("sync_on_chapter_read", false)
.set(syncTriggerOptions.syncOnChapterRead)
preferenceStore.getBoolean("sync_on_chapter_open", false)
.set(syncTriggerOptions.syncOnChapterOpen)
preferenceStore.getBoolean("sync_on_app_start", false)
.set(syncTriggerOptions.syncOnAppStart)
preferenceStore.getBoolean("sync_on_app_resume", false)
.set(syncTriggerOptions.syncOnAppResume)
}
}

View File

@ -1,19 +0,0 @@
package eu.kanade.domain.sync.models
data class SyncSettings(
val libraryEntries: Boolean = true,
val categories: Boolean = true,
val chapters: Boolean = true,
val tracking: Boolean = true,
val history: Boolean = true,
val appSettings: Boolean = true,
val extensionRepoSettings: Boolean = true,
val sourceSettings: Boolean = true,
val privateSettings: Boolean = false,
// SY -->
val customInfo: Boolean = true,
val readEntries: Boolean = true,
val savedSearches: Boolean = true,
// SY <--
)

View File

@ -5,7 +5,6 @@ import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import logcat.LogPriority
@ -15,16 +14,17 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZoneOffset
class AddTracks(
private val getTracks: GetTracks,
private val insertTrack: InsertTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val trackerManager: TrackerManager,
) {
// TODO: update all trackers based on common data
@ -79,7 +79,7 @@ class AddTracks(
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
withIOContext {
trackerManager.loggedInTrackers()
getTracks.await(manga.id)
.filterIsInstance<EnhancedTracker>()
.filter { it.accept(source) }
.forEach { service ->
@ -87,11 +87,11 @@ class AddTracks(
service.match(manga)?.let { track ->
track.manga_id = manga.id
(service as Tracker).bind(track)
insertTrack.await(track.toDomainTrack(idRequired = false)!!)
insertTrack.await(track.toDomainTrack()!!)
syncChapterProgressWithTrack.await(
manga.id,
track.toDomainTrack(idRequired = false)!!,
track.toDomainTrack()!!,
service,
)
}

View File

@ -10,7 +10,6 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track
import kotlin.math.max
class SyncChapterProgressWithTrack(
private val updateChapter: UpdateChapter,
@ -37,8 +36,7 @@ class SyncChapterProgressWithTrack(
// only take into account continuous reading
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
val lastRead = max(remoteTrack.lastChapterRead, localLastRead.toDouble())
val updatedTrack = remoteTrack.copy(lastChapterRead = lastRead)
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
try {
tracker.update(updatedTrack.toDbTrack())

View File

@ -23,7 +23,7 @@ class TrackChapter(
private val delayedTrackingStore: DelayedTrackingStore,
) {
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) {
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) {
withNonCancellableContext {
val tracks = getTracks.await(mangaId)
if (tracks.isEmpty()) return@withNonCancellableContext
@ -34,7 +34,7 @@ class TrackChapter(
service == null ||
!service.isLoggedIn ||
chapterNumber <= track.lastChapterRead /* SY --> */ ||
(service is MdList && track.status == FollowStatus.UNFOLLOWED.long)/* SY <-- */
(service is MdList && track.status == FollowStatus.UNFOLLOWED.int.toLong())/* SY <-- */
) {
return@mapNotNull null
}
@ -50,9 +50,7 @@ class TrackChapter(
delayedTrackingStore.remove(track.id)
} catch (e: Exception) {
delayedTrackingStore.add(track.id, chapterNumber)
if (setupJobOnFailure) {
DelayedTrackingUpdateJob.setupTask(context)
}
DelayedTrackingUpdateJob.setupTask(context)
throw e
}
}

View File

@ -1,10 +0,0 @@
package eu.kanade.domain.track.model
import dev.icerock.moko.resources.StringResource
import tachiyomi.i18n.MR
enum class AutoTrackState(val titleRes: StringResource) {
ALWAYS(MR.strings.lock_always),
ASK(MR.strings.default_category_summary),
NEVER(MR.strings.lock_never),
}

View File

@ -10,7 +10,6 @@ fun Track.copyPersonalFrom(other: Track): Track {
status = other.status,
startDate = other.startDate,
finishDate = other.finishDate,
private = other.private,
)
}
@ -27,7 +26,6 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
it.tracking_url = remoteUrl
it.started_reading_date = startDate
it.finished_reading_date = finishDate
it.private = private
}
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
@ -46,6 +44,5 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
remoteUrl = tracking_url,
startDate = started_reading_date,
finishDate = finished_reading_date,
private = private,
)
}

View File

@ -45,7 +45,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
logcat(LogPriority.DEBUG) {
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
}
trackChapter.await(context, track.mangaId, track.lastChapterRead, setupJobOnFailure = false)
trackChapter.await(context, track.mangaId, track.lastChapterRead)
}
}

View File

@ -1,11 +1,9 @@
package eu.kanade.domain.track.service
import eu.kanade.domain.track.model.AutoTrackState
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
class TrackPreferences(
private val preferenceStore: PreferenceStore,
@ -37,16 +35,4 @@ class TrackPreferences(
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum(
"pref_auto_update_manga_on_mark_read",
AutoTrackState.ALWAYS,
)
// SY -->
fun resolveUsingSourceMetadata() = preferenceStore.getBoolean(
"pref_resolve_using_source_metadata_key",
true,
)
// SY <--
}

View File

@ -8,8 +8,8 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
class UiPreferences(
@ -18,20 +18,12 @@ class UiPreferences(
fun themeMode() = preferenceStore.getEnum(
"pref_theme_mode_key",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ThemeMode.SYSTEM
} else {
ThemeMode.LIGHT
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ThemeMode.SYSTEM } else { ThemeMode.LIGHT },
)
fun appTheme() = preferenceStore.getEnum(
"pref_app_theme",
if (DeviceUtil.isDynamicColorAvailable) {
AppTheme.MONET
} else {
AppTheme.DEFAULT
},
if (DeviceUtil.isDynamicColorAvailable) { AppTheme.MONET } else { AppTheme.DEFAULT },
)
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
@ -54,8 +46,6 @@ class UiPreferences(
fun mergeInOverflow() = preferenceStore.getBoolean("merge_in_overflow", true)
fun previewsRowCount() = preferenceStore.getInt("pref_previews_row_count", 4)
fun useNewSourceNavigation() = preferenceStore.getBoolean("use_new_source_navigation", true)
fun bottomBarLabels() = preferenceStore.getBoolean("pref_show_bottom_bar_labels", true)
@ -67,9 +57,9 @@ class UiPreferences(
// SY <--
companion object {
fun dateFormat(format: String): DateTimeFormatter = when (format) {
"" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
else -> DateTimeFormatter.ofPattern(format, Locale.getDefault())
fun dateFormat(format: String): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
}
}
}

View File

@ -1,6 +1,8 @@
package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import tachiyomi.i18n.MR
enum class AppTheme(val titleRes: StringResource?) {
@ -9,14 +11,15 @@ enum class AppTheme(val titleRes: StringResource?) {
GREEN_APPLE(MR.strings.theme_greenapple),
LAVENDER(MR.strings.theme_lavender),
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
NORD(MR.strings.theme_nord),
// 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),
MONOCHROME(MR.strings.theme_monochrome),
// Deprecated
DARK_BLUE(null),

View File

@ -82,18 +82,10 @@ fun BrowseSourceContent(
}
}
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
LoadingScreen(Modifier.padding(contentPadding))
return
}
if (mangaList.itemCount == 0) {
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
EmptyScreen(
modifier = Modifier.padding(contentPadding),
message = when (errorState) {
is LoadState.Error -> getErrorMessage(errorState)
else -> stringResource(MR.strings.no_results_found)
},
message = getErrorMessage(errorState),
actions = if (source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) {
persistentListOf(
EmptyScreenAction(
@ -112,7 +104,7 @@ fun BrowseSourceContent(
// SY -->
if (onWebViewClick != null) {
EmptyScreenAction(
stringRes = MR.strings.action_open_in_web_view,
MR.strings.action_open_in_web_view,
icon = Icons.Outlined.Public,
onClick = onWebViewClick,
)
@ -121,7 +113,7 @@ fun BrowseSourceContent(
},
if (onHelpClick != null) {
EmptyScreenAction(
stringRes = MR.strings.label_help,
MR.strings.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick,
)
@ -136,6 +128,13 @@ fun BrowseSourceContent(
return
}
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
LoadingScreen(
modifier = Modifier.padding(contentPadding),
)
return
}
// SY -->
if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
BrowseSourceEHentaiList(

View File

@ -11,7 +11,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun BrowseTabWrapper(tab: TabContent, onBackPressed: (() -> Unit)? = null) {
fun BrowseTabWrapper(tab: TabContent) {
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = { scrollBehavior ->
@ -20,7 +20,6 @@ fun BrowseTabWrapper(tab: TabContent, onBackPressed: (() -> Unit)? = null) {
actions = {
AppBarActions(tab.actions)
},
navigateUp = onBackPressed,
scrollBehavior = scrollBehavior,
)
},

View File

@ -5,6 +5,7 @@ import android.net.Uri
import android.provider.Settings
import android.util.DisplayMetrics
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -35,10 +36,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -50,7 +49,6 @@ 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
@ -76,7 +74,6 @@ fun ExtensionDetailsScreen(
onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) {
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
@ -145,11 +142,9 @@ fun ExtensionDetailsScreen(
contentPadding = paddingValues,
extension = state.extension,
sources = state.sources,
incognitoMode = state.isIncognito,
onClickSourcePreferences = onClickSourcePreferences,
onClickUninstall = onClickUninstall,
onClickSource = onClickSource,
onClickIncognito = onClickIncognito,
)
}
}
@ -159,11 +154,9 @@ private fun ExtensionDetails(
contentPadding: PaddingValues,
extension: Extension.Installed,
sources: ImmutableList<ExtensionSourceItem>,
incognitoMode: Boolean,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) {
val context = LocalContext.current
var showNsfwWarning by remember { mutableStateOf(false) }
@ -187,7 +180,6 @@ private fun ExtensionDetails(
item {
DetailsHeader(
extension = extension,
extIncognitoMode = incognitoMode,
onClickUninstall = onClickUninstall,
onClickAppInfo = {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
@ -199,7 +191,6 @@ private fun ExtensionDetails(
onClickAgeRating = {
showNsfwWarning = true
},
onExtIncognitoChange = onClickIncognito,
)
}
@ -208,7 +199,7 @@ private fun ExtensionDetails(
key = { it.source.id },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource,
@ -227,11 +218,9 @@ private fun ExtensionDetails(
@Composable
private fun DetailsHeader(
extension: Extension,
extIncognitoMode: Boolean,
onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit,
onClickAppInfo: (() -> Unit)?,
onExtIncognitoChange: (Boolean) -> Unit,
) {
val context = LocalContext.current
@ -239,8 +228,9 @@ private fun DetailsHeader(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = MaterialTheme.padding.medium)
.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
bottom = MaterialTheme.padding.small,
)
@ -251,7 +241,7 @@ private fun DetailsHeader(
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
NSFW: ${extension.isNsfw}
""".trimIndent(),
""".trimIndent()
)
if (extension is Extension.Installed) {
@ -261,8 +251,8 @@ private fun DetailsHeader(
Update available: ${extension.hasUpdate}
Obsolete: ${extension.isObsolete}
Shared: ${extension.isShared}
Repository: ${extension.repoUrl}
""".trimIndent(),
Repository: ${extension.repoUrl}
""".trimIndent()
)
}
}
@ -332,9 +322,12 @@ private fun DetailsHeader(
}
Row(
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.medium)
.padding(top = MaterialTheme.padding.small),
modifier = Modifier.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
OutlinedButton(
@ -357,24 +350,6 @@ private fun DetailsHeader(
}
}
TextPreferenceWidget(
modifier = Modifier.padding(horizontal = MaterialTheme.padding.small),
title = stringResource(MR.strings.pref_incognito_mode),
subtitle = stringResource(MR.strings.pref_incognito_mode_extension_summary),
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
widget = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Switch(
checked = extIncognitoMode,
onCheckedChange = onExtIncognitoChange,
modifier = Modifier.padding(start = TrailingWidgetBuffer),
)
}
},
)
HorizontalDivider()
}
}
@ -387,8 +362,10 @@ private fun InfoText(
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
onClick: (() -> Unit)? = null,
) {
val interactionSource = remember { MutableInteractionSource() }
val clickableModifier = if (onClick != null) {
Modifier.clickable(interactionSource = null, indication = null, onClick = onClick)
Modifier.clickable(interactionSource, indication = null) { onClick() }
} else {
Modifier
}

View File

@ -58,7 +58,7 @@ private fun ExtensionFilterContent(
) {
items(state.languages) { language ->
SwitchPreferenceWidget(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
title = LocaleHelper.getSourceDisplayName(language, context),
checked = language in state.enabledLanguages,
onCheckedChanged = { onClickLang(language) },

View File

@ -48,7 +48,6 @@ 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.animateItemFastScroll
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
@ -92,7 +91,7 @@ fun ExtensionScreen(
PullRefresh(
refreshing = state.isRefreshing,
onRefresh = onRefresh,
enabled = !state.isLoading,
enabled = { !state.isLoading },
) {
when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
@ -189,14 +188,14 @@ private fun ExtensionContent(
}
ExtensionHeader(
textRes = header.textRes,
modifier = Modifier.animateItemFastScroll(),
modifier = Modifier.animateItemPlacement(),
action = action,
)
}
is ExtensionUiModel.Header.Text -> {
ExtensionHeader(
text = header.text,
modifier = Modifier.animateItemFastScroll(),
modifier = Modifier.animateItemPlacement(),
)
}
}
@ -214,15 +213,13 @@ private fun ExtensionContent(
},
) { item ->
ExtensionItem(
modifier = Modifier.animateItemFastScroll(),
modifier = Modifier.animateItemPlacement(),
item = item,
onClickItem = {
when (it) {
is Extension.Available -> onInstallExtension(it)
is Extension.Installed -> onOpenExtension(it)
is Extension.Untrusted -> {
trustState = it
}
is Extension.Untrusted -> { trustState = it }
}
},
onLongClickItem = onLongClickItem,
@ -244,9 +241,7 @@ private fun ExtensionContent(
onOpenExtension(it)
}
}
is Extension.Untrusted -> {
trustState = it
}
is Extension.Untrusted -> { trustState = it }
}
},
)

View File

@ -70,7 +70,7 @@ fun FeedScreen(
onClickDelete: (FeedSavedSearch) -> Unit,
onClickManga: (Manga) -> Unit,
onRefresh: () -> Unit,
getMangaState: @Composable (Manga) -> State<Manga>,
getMangaState: @Composable (Manga, CatalogueSource?) -> State<Manga>,
) {
when {
state.isLoading -> LoadingScreen()
@ -92,7 +92,7 @@ fun FeedScreen(
refreshing = true
onRefresh()
},
enabled = !state.isLoadingItems,
enabled = { !state.isLoadingItems },
) {
ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
@ -103,6 +103,7 @@ fun FeedScreen(
key = { it.feed.id },
) { item ->
GlobalSearchResultItem(
modifier = Modifier.animateItemPlacement(),
title = item.title,
subtitle = item.subtitle,
onLongClick = {
@ -115,11 +116,10 @@ fun FeedScreen(
onClickSource(item.source)
}
},
modifier = Modifier.animateItem(),
) {
FeedItem(
item = item,
getMangaState = { getMangaState(it) },
getMangaState = { getMangaState(it, item.source) },
onClickManga = onClickManga,
)
}

View File

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
@ -81,7 +80,6 @@ internal fun GlobalSearchContent(
} ?: source.name,
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
onClick = { onClickSource(source) },
modifier = Modifier.animateItem(),
) {
when (result) {
SearchItemResult.Loading -> {

View File

@ -144,7 +144,7 @@ private fun MigrateSourceList(
key = { (source, _) -> "migrate-${source.id}" },
) { (source, count) ->
MigrateSourceItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
source = source,
count = count,
onClickItem = { onClickItem(source) },

View File

@ -28,7 +28,6 @@ import eu.kanade.presentation.browse.components.MigrationItem
import eu.kanade.presentation.browse.components.MigrationItemResult
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -96,7 +95,7 @@ fun MigrationListScreen(
Row(
Modifier
.fillMaxWidth()
.animateItemFastScroll()
.animateItemPlacement()
.padding(horizontal = 16.dp)
.height(IntrinsicSize.Min),
horizontalArrangement = Arrangement.SpaceBetween,

View File

@ -15,7 +15,6 @@ import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.util.animateItemFastScroll
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.FeedSavedSearch
@ -154,7 +153,7 @@ fun SourceFeedList(
key = { it.id },
) { item ->
GlobalSearchResultItem(
modifier = Modifier.animateItemFastScroll(),
modifier = Modifier.animateItemPlacement(),
title = item.title,
subtitle = null,
onLongClick = if (item is SourceFeedUI.SourceSavedSearch) {

View File

@ -11,7 +11,6 @@ import androidx.compose.ui.platform.LocalContext
import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.model.Source
@ -80,7 +79,7 @@ private fun SourcesFilterContent(
contentType = "source-filter-header",
) {
SourcesFilterHeader(
modifier = Modifier.animateItemFastScroll(),
modifier = Modifier.animateItemPlacement(),
language = language,
enabled = enabled,
onClickItem = onClickLanguage,
@ -96,7 +95,7 @@ private fun SourcesFilterContent(
sources.none { it.id.toString() in state.disabledSources }
}
SourcesFilterToggle(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
isEnabled = toggleEnabled,
onClickItem = {
onClickSources(!toggleEnabled, sources)
@ -110,7 +109,7 @@ private fun SourcesFilterContent(
contentType = { "source-filter-item" },
) { source ->
SourcesFilterItem(
modifier = Modifier.animateItemFastScroll(),
modifier = Modifier.animateItemPlacement(),
source = source,
enabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource,

View File

@ -35,7 +35,7 @@ import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
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
@ -81,7 +81,7 @@ fun SourcesScreen(
when (model) {
is SourceUiModel.Header -> {
SourceHeader(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
language = model.language,
// SY -->
isCategory = model.isCategory,
@ -89,7 +89,7 @@ fun SourcesScreen(
)
}
is SourceUiModel.Item -> SourceItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
source = model.source,
// SY -->
showLatest = state.showLatest,
@ -179,7 +179,7 @@ private fun SourcePinButton(
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onBackground.copy(
alpha = SECONDARY_ALPHA,
alpha = SecondaryItemAlpha,
)
}
val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin

View File

@ -25,7 +25,7 @@ import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import coil3.compose.AsyncImage
import coil.compose.AsyncImage
import eu.kanade.domain.source.model.icon
import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R
@ -127,7 +127,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
withIOContext {
value = try {
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
val appResources = context.packageManager.getResourcesForApplication(appInfo)
Result.Success(
appResources.getDrawableForDensity(appInfo.icon, density, null)!!

View File

@ -19,7 +19,6 @@ import eu.kanade.presentation.library.components.MangaComfortableGridItem
import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.RaisedSearchMetadata
import exh.metadata.metadata.RankedSearchMetadata
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
@ -120,14 +119,6 @@ private fun BrowseSourceComfortableGridItem(
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
} else if (metadata is RankedSearchMetadata) {
metadata.rank?.let {
Badge(
text = "+$it",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
},
// SY <--

View File

@ -19,7 +19,6 @@ import eu.kanade.presentation.library.components.MangaCompactGridItem
import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.RaisedSearchMetadata
import exh.metadata.metadata.RankedSearchMetadata
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
@ -120,14 +119,6 @@ private fun BrowseSourceCompactGridItem(
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
} else if (metadata is RankedSearchMetadata) {
metadata.rank?.let {
Badge(
text = "+$it",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
},
// SY <--

View File

@ -50,8 +50,7 @@ import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import java.time.Instant
import java.time.ZoneId
import java.util.Date
@Composable
fun BrowseSourceEHentaiList(
@ -129,11 +128,9 @@ fun BrowseSourceEHentaiListItem(
}
val datePosted by produceState("", metadata) {
value = withIOContext {
runCatching {
metadata.datePosted?.let {
MetadataUtil.EX_DATE_FORMAT.format(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()))
}
}.getOrNull().orEmpty()
runCatching { metadata.datePosted?.let { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) } }
.getOrNull()
.orEmpty()
}
}
val genre by produceState<Pair<GenreColor, StringResource>?>(null, metadata) {

View File

@ -16,7 +16,6 @@ import eu.kanade.presentation.library.components.MangaListItem
import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.RaisedSearchMetadata
import exh.metadata.metadata.RankedSearchMetadata
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
@ -111,14 +110,6 @@ private fun BrowseSourceListItem(
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
} else if (metadata is RankedSearchMetadata) {
metadata.rank?.let {
Badge(
text = "+$it",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
// SY <--
},

View File

@ -30,6 +30,9 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun GlobalSearchResultItem(
// SY -->
modifier: Modifier = Modifier,
// SY <--
title: String,
// SY -->
subtitle: String?,
@ -38,10 +41,9 @@ fun GlobalSearchResultItem(
// SY -->
onLongClick: (() -> Unit)? = null,
// SY <--
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Column(modifier = modifier) {
Column(modifier) {
Row(
modifier = Modifier
.padding(

View File

@ -2,25 +2,23 @@ package eu.kanade.presentation.category
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
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.ui.category.CategoryScreenState
import kotlinx.collections.immutable.ImmutableList
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
@ -34,9 +32,11 @@ import tachiyomi.presentation.core.util.plus
fun CategoryScreen(
state: CategoryScreenState.Success,
onClickCreate: () -> Unit,
onClickSortAlphabetically: () -> Unit,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onChangeOrder: (Category, Int) -> Unit,
onClickMoveUp: (Category) -> Unit,
onClickMoveDown: (Category) -> Unit,
navigateUp: () -> Unit,
) {
val lazyListState = rememberLazyListState()
@ -45,6 +45,17 @@ fun CategoryScreen(
AppBar(
title = stringResource(MR.strings.action_edit_categories),
navigateUp = navigateUp,
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_sort),
icon = Icons.Outlined.SortByAlpha,
onClick = onClickSortAlphabetically,
),
),
)
},
scrollBehavior = scrollBehavior,
)
},
@ -66,10 +77,12 @@ fun CategoryScreen(
CategoryContent(
categories = state.categories,
lazyListState = lazyListState,
paddingValues = paddingValues,
paddingValues = paddingValues + topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
onClickRename = onClickRename,
onClickDelete = onClickDelete,
onChangeOrder = onChangeOrder,
onMoveUp = onClickMoveUp,
onMoveDown = onClickMoveDown,
)
}
}
@ -81,44 +94,28 @@ private fun CategoryContent(
paddingValues: PaddingValues,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onChangeOrder: (Category, Int) -> Unit,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
) {
val categoriesState = remember { categories.toMutableStateList() }
val reorderableState = rememberReorderableLazyListState(lazyListState, paddingValues) { from, to ->
val item = categoriesState.removeAt(from.index)
categoriesState.add(to.index, item)
onChangeOrder(item, to.index)
}
LaunchedEffect(categories) {
if (!reorderableState.isAnyItemDragging) {
categoriesState.clear()
categoriesState.addAll(categories)
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = paddingValues +
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
items(
items = categoriesState,
key = { category -> category.key },
) { category ->
ReorderableItem(reorderableState, category.key) {
CategoryListItem(
modifier = Modifier.animateItem(),
category = category,
onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },
)
}
itemsIndexed(
items = categories,
key = { _, category -> "category-${category.id}" },
) { index, category ->
CategoryListItem(
modifier = Modifier.animateItemPlacement(),
category = category,
canMoveUp = index != 0,
canMoveDown = index != categories.lastIndex,
onMoveUp = onMoveUp,
onMoveDown = onMoveDown,
onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },
)
}
}
}
private val Category.key inline get() = "category-$id"

View File

@ -219,6 +219,35 @@ fun CategoryDeleteDialog(
)
}
@Composable
fun CategorySortAlphabeticallyDialog(
onDismissRequest: () -> Unit,
onSort: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = {
onSort()
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_sort_category))
},
text = {
Text(text = stringResource(MR.strings.sort_category_confirmation))
},
)
}
@Composable
fun ChangeCategoryDialog(
initialSelection: ImmutableList<CheckboxState<Category>>,

View File

@ -10,7 +10,8 @@ import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.shouldExpandFAB
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp
@Composable
fun CategoryFloatingActionButton(
@ -22,7 +23,7 @@ fun CategoryFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
onClick = onCreate,
expanded = lazyListState.shouldExpandFAB(),
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
modifier = modifier,
)
}

View File

@ -2,11 +2,14 @@ package eu.kanade.presentation.category.components
import androidx.compose.foundation.clickable
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.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.DragHandle
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
@ -16,42 +19,57 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import sh.calvin.reorderable.ReorderableCollectionItemScope
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 ReorderableCollectionItemScope.CategoryListItem(
fun CategoryListItem(
category: Category,
canMoveUp: Boolean,
canMoveDown: Boolean,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
onRename: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
ElevatedCard(modifier = modifier) {
ElevatedCard(
modifier = modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onRename)
.padding(vertical = MaterialTheme.padding.small)
.clickable { onRename() }
.padding(
start = MaterialTheme.padding.small,
start = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.DragHandle,
contentDescription = null,
modifier = Modifier
.padding(MaterialTheme.padding.medium)
.draggableHandle(),
)
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Text(
text = category.name,
modifier = Modifier.weight(1f),
modifier = Modifier
.padding(start = MaterialTheme.padding.medium),
)
}
Row {
IconButton(
onClick = { onMoveUp(category) },
enabled = canMoveUp,
) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
}
IconButton(
onClick = { onMoveDown(category) },
enabled = canMoveDown,
) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) {
Icon(
imageVector = Icons.Outlined.Edit,
@ -59,10 +77,7 @@ fun ReorderableCollectionItemScope.CategoryListItem(
)
}
IconButton(onClick = onDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(MR.strings.action_delete),
)
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete))
}
}
}

View File

@ -26,7 +26,7 @@ fun BiometricTimesContent(
) {
items(timeRanges, key = { it.formattedString }) { timeRange ->
BiometricTimesListItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
timeRange = timeRange,
onDelete = { onClickDelete(timeRange) },
)

View File

@ -27,7 +27,7 @@ fun SortTagContent(
) {
itemsIndexed(tags, key = { _, tag -> tag }) { index, tag ->
SortTagListItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
tag = tag,
canMoveUp = index != 0,
canMoveDown = index != tags.lastIndex,

View File

@ -26,7 +26,7 @@ fun SourceCategoryContent(
) {
items(categories, key = { it }) { category ->
SourceCategoryListItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
category = category,
onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },

View File

@ -1,11 +1,14 @@
package eu.kanade.presentation.components
import androidx.compose.animation.SizeTransform
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
@ -20,6 +23,7 @@ import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
@Composable
fun NavigatorAdaptiveSheet(
screen: Screen,
tonalElevation: Dp = 1.dp,
enableSwipeDismiss: (Navigator) -> Boolean = { true },
onDismissRequest: () -> Unit,
) {
@ -27,14 +31,21 @@ fun NavigatorAdaptiveSheet(
screen = screen,
content = { sheetNavigator ->
AdaptiveSheet(
onDismissRequest = onDismissRequest,
tonalElevation = tonalElevation,
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
onDismissRequest = onDismissRequest,
) {
ScreenTransition(
navigator = sheetNavigator,
enterTransition = { fadeIn(animationSpec = tween(220, delayMillis = 90)) },
exitTransition = { fadeOut(animationSpec = tween(90)) },
sizeTransform = { SizeTransform() },
transition = {
fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith
fadeOut(animationSpec = tween(90))
},
)
BackHandler(
enabled = sheetNavigator.size > 1,
onBack = sheetNavigator::pop,
)
}
@ -62,6 +73,7 @@ fun NavigatorAdaptiveSheet(
fun AdaptiveSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
tonalElevation: Dp = 1.dp,
enableSwipeDismiss: Boolean = true,
content: @Composable () -> Unit,
) {
@ -72,10 +84,11 @@ fun AdaptiveSheet(
properties = dialogProperties,
) {
AdaptiveSheetImpl(
modifier = modifier,
isTabletUi = isTabletUi,
tonalElevation = tonalElevation,
enableSwipeDismiss = enableSwipeDismiss,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
content()
}

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicTextField
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.Close
@ -20,7 +21,6 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TopAppBar
@ -36,7 +36,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
@ -180,7 +179,7 @@ fun AppBarTitle(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.basicMarquee(
repeatDelayMillis = 2_000,
delayMillis = 2_000,
),
)
}
@ -202,7 +201,6 @@ fun AppBarActions(
}
},
state = rememberTooltipState(),
focusable = false,
) {
IconButton(
onClick = it.onClick,
@ -227,7 +225,6 @@ fun AppBarActions(
}
},
state = rememberTooltipState(),
focusable = false,
) {
IconButton(
onClick = { showMenu = !showMenu },
@ -292,7 +289,6 @@ fun SearchToolbar(
onSearch(searchQuery)
focusManager.clearFocus()
keyboardController?.hide()
focusManager.moveFocus(FocusDirection.Next)
}
BasicTextField(
@ -316,7 +312,7 @@ fun SearchToolbar(
visualTransformation = visualTransformation,
interactionSource = interactionSource,
decorationBox = { innerTextField ->
TextFieldDefaults.DecorationBox(
TextFieldDefaults.TextFieldDecorationBox(
value = searchQuery,
innerTextField = innerTextField,
enabled = true,
@ -335,7 +331,6 @@ fun SearchToolbar(
),
)
},
container = {},
)
},
)
@ -356,7 +351,6 @@ fun SearchToolbar(
}
},
state = rememberTooltipState(),
focusable = false,
) {
IconButton(
onClick = onClick,
@ -376,7 +370,6 @@ fun SearchToolbar(
}
},
state = rememberTooltipState(),
focusable = false,
) {
IconButton(
onClick = {

View File

@ -9,26 +9,20 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.util.Date
@Composable
fun relativeDateText(
dateEpochMillis: Long,
): String {
return relativeDateText(
localDate = LocalDate.ofInstant(
Instant.ofEpochMilli(dateEpochMillis),
ZoneId.systemDefault(),
)
.takeIf { dateEpochMillis > 0L },
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L },
)
}
@Composable
fun relativeDateText(
localDate: LocalDate?,
date: Date?,
): String {
val context = LocalContext.current
@ -36,10 +30,11 @@ fun relativeDateText(
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
return localDate?.toRelativeString(
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
return date
?.toRelativeString(
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
?: stringResource(MR.strings.not_applicable)
}

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
@ -30,6 +29,7 @@ import androidx.compose.ui.util.fastForEachIndexed
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.TabText
import tachiyomi.presentation.core.i18n.stringResource
@ -58,7 +58,6 @@ fun TabbedDialog(
PrimaryTabRow(
modifier = Modifier.weight(1f),
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
divider = {},
) {
tabTitles.fastForEachIndexed { index, tab ->
@ -79,8 +78,9 @@ fun TabbedDialog(
modifier = Modifier.animateContentSize(),
state = pagerState,
verticalAlignment = Alignment.Top,
pageContent = { page -> content(page) },
)
) { page ->
content(page)
}
}
}
}

View File

@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
@ -15,6 +13,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
@ -25,6 +24,7 @@ import dev.icerock.moko.resources.StringResource
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.TabText
import tachiyomi.presentation.core.i18n.stringResource
@ -33,13 +33,20 @@ import tachiyomi.presentation.core.i18n.stringResource
fun TabbedScreen(
titleRes: StringResource,
tabs: ImmutableList<TabContent>,
state: PagerState = rememberPagerState { tabs.size },
startIndex: Int? = null,
searchQuery: String? = null,
onChangeSearchQuery: (String?) -> Unit = {},
) {
val scope = rememberCoroutineScope()
val state = rememberPagerState { tabs.size }
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(startIndex) {
if (startIndex != null) {
state.scrollToPage(startIndex)
}
}
Scaffold(
topBar = {
val tab = tabs[state.currentPage]

View File

@ -37,7 +37,7 @@ fun CrashScreen(
acceptText = stringResource(MR.strings.pref_dump_crash_logs),
onAcceptClick = {
scope.launch {
CrashLogUtil(context).dumpLogs(exception)
CrashLogUtil(context).dumpLogs()
}
},
rejectText = stringResource(MR.strings.crash_screen_restart_application),

View File

@ -18,7 +18,6 @@ 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.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -30,7 +29,7 @@ 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 java.time.LocalDate
import java.util.Date
@Composable
fun HistoryScreen(
@ -39,7 +38,6 @@ fun HistoryScreen(
onSearchQueryChange: (String?) -> Unit,
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onClickFavorite: (mangaId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
) {
Scaffold(
@ -86,7 +84,6 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
)
}
}
@ -100,7 +97,6 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit,
onClickFavorite: (HistoryWithRelations) -> Unit,
) {
FastScrollLazyColumn(
contentPadding = contentPadding,
@ -118,19 +114,18 @@ private fun HistoryScreenContent(
when (item) {
is HistoryUiModel.Header -> {
ListGroupHeader(
modifier = Modifier.animateItemFastScroll(),
modifier = Modifier.animateItemPlacement(),
text = relativeDateText(item.date),
)
}
is HistoryUiModel.Item -> {
val value = item.item
HistoryItem(
modifier = Modifier.animateItemFastScroll(),
modifier = Modifier.animateItemPlacement(),
history = value,
onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) },
onClickDelete = { onClickDelete(value) },
onClickFavorite = { onClickFavorite(value) },
)
}
}
@ -139,7 +134,7 @@ private fun HistoryScreenContent(
}
sealed interface HistoryUiModel {
data class Header(val date: LocalDate) : HistoryUiModel
data class Header(val date: Date) : HistoryUiModel
data class Item(val item: HistoryWithRelations) : HistoryUiModel
}
@ -157,7 +152,6 @@ internal fun HistoryScreenPreviews(
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
onClickFavorite = {},
)
}
}

View File

@ -7,7 +7,6 @@ import kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.domain.manga.model.MangaCover
import java.time.Instant
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.random.Random
@ -74,10 +73,10 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
private object HistoryUiModelExamples {
val headerToday = header()
val headerTomorrow =
HistoryUiModel.Header(LocalDate.now().plusDays(1))
HistoryUiModel.Header(Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))
fun header(instantBuilder: (Instant) -> Instant = { it }) =
HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now())))
HistoryUiModel.Header(Date.from(instantBuilder(Instant.now())))
fun items() = sequence {
var count = 1

View File

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -40,7 +39,6 @@ fun HistoryItem(
onClickCover: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
onClickFavorite: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
@ -84,16 +82,6 @@ fun HistoryItem(
)
}
if (!history.coverData.isMangaFavorite) {
IconButton(onClick = onClickFavorite) {
Icon(
imageVector = Icons.Outlined.FavoriteBorder,
contentDescription = stringResource(MR.strings.add_to_library),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
@ -117,7 +105,6 @@ private fun HistoryItemPreviews(
onClickCover = {},
onClickResume = {},
onClickDelete = {},
onClickFavorite = {},
)
}
}

View File

@ -6,10 +6,7 @@ import androidx.compose.foundation.layout.ColumnScope
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.filled.Refresh
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -38,7 +35,6 @@ import tachiyomi.domain.library.model.sort
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.BaseSortItem
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.IconItem
@ -159,7 +155,7 @@ private fun ColumnScope.FilterPage(
)
// SY <--
val trackers by screenModel.trackersFlow.collectAsState()
val trackers = remember { screenModel.trackers }
when (trackers.size) {
0 -> {
// No trackers
@ -192,7 +188,6 @@ private fun ColumnScope.SortPage(
category: Category?,
screenModel: LibrarySettingsScreenModel,
) {
val trackers by screenModel.trackersFlow.collectAsState()
// SY -->
val globalSortMode by screenModel.libraryPreferences.sortingMode().collectAsState()
val sortingMode = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
@ -201,58 +196,40 @@ private fun ColumnScope.SortPage(
globalSortMode.type
}
val sortDescending = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
!category.sort.isAscending
category.sort.isAscending
} else {
!globalSortMode.isAscending
}
globalSortMode.isAscending
}.not()
val hasSortTags by remember {
screenModel.libraryPreferences.sortTagsForLibrary().changes()
.map { it.isNotEmpty() }
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
// SY <--
val options = remember(trackers.isEmpty()/* SY --> */, hasSortTags/* SY <-- */) {
val trackerMeanPair = if (trackers.isNotEmpty()) {
MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean
val trackerSortOption =
if (screenModel.trackers.isEmpty()) {
emptyList()
} else {
null
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
}
listOfNotNull(
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,
// SY -->
val tagSortPair = if (hasSortTags) {
if (hasSortTags) {
SYMR.strings.tag_sorting to LibrarySort.Type.TagList
} else {
null
}
},
// SY <--
listOfNotNull(
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,
trackerMeanPair,
// SY -->
tagSortPair,
// SY <--
MR.strings.action_sort_random to LibrarySort.Type.Random,
)
}
options.map { (titleRes, mode) ->
if (mode == LibrarySort.Type.Random) {
BaseSortItem(
label = stringResource(titleRes),
icon = Icons.Default.Refresh
.takeIf { sortingMode == LibrarySort.Type.Random },
onClick = {
screenModel.setSort(category, mode, LibrarySort.Direction.Ascending)
},
)
return@map
}
).plus(trackerSortOption).map { (titleRes, mode) ->
SortItem(
label = stringResource(titleRes),
sortDescending = sortDescending.takeIf { sortingMode == mode },
@ -264,11 +241,7 @@ private fun ColumnScope.SortPage(
} else {
LibrarySort.Direction.Descending
}
else -> if (sortDescending) {
LibrarySort.Direction.Descending
} else {
LibrarySort.Direction.Ascending
}
else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending
}
screenModel.setSort(category, mode, direction)
},
@ -310,16 +283,15 @@ private fun ColumnScope.DisplayPage(
val columns by columnPreference.collectAsState()
SliderItem(
value = columns,
valueRange = 0..10,
label = stringResource(MR.strings.pref_library_columns),
max = 10,
value = columns,
valueText = if (columns > 0) {
columns.toString()
stringResource(MR.strings.pref_library_columns_per_row, columns)
} else {
stringResource(MR.strings.label_auto)
stringResource(MR.strings.label_default)
},
onChange = columnPreference::set,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
}
@ -328,10 +300,6 @@ private fun ColumnScope.DisplayPage(
label = stringResource(MR.strings.action_display_download_badge),
pref = screenModel.libraryPreferences.downloadBadge(),
)
CheckboxItem(
label = stringResource(MR.strings.action_display_unread_badge),
pref = screenModel.libraryPreferences.unreadBadge(),
)
CheckboxItem(
label = stringResource(MR.strings.action_display_local_badge),
pref = screenModel.libraryPreferences.localBadge(),
@ -378,13 +346,12 @@ private fun ColumnScope.GroupPage(
screenModel: LibrarySettingsScreenModel,
hasCategories: Boolean,
) {
val trackers by screenModel.trackersFlow.collectAsState()
val groups = remember(hasCategories, trackers) {
val groups = remember(hasCategories, screenModel.trackers) {
buildList {
add(LibraryGroup.BY_DEFAULT)
add(LibraryGroup.BY_SOURCE)
add(LibraryGroup.BY_STATUS)
if (trackers.isNotEmpty()) {
if (screenModel.trackers.isNotEmpty()) {
add(LibraryGroup.BY_TRACK_STATUS)
}
if (hasCategories) {

View File

@ -35,7 +35,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.manga.components.MangaCover
@ -43,26 +42,19 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.selectedBackground
import tachiyomi.domain.manga.model.MangaCover as MangaCoverModel
object CommonMangaItemDefaults {
val GridHorizontalSpacer = 4.dp
val GridVerticalSpacer = 4.dp
@Suppress("ConstPropertyName")
const val BrowseFavoriteCoverAlpha = 0.34f
}
private val ContinueReadingButtonSizeSmall = 28.dp
private val ContinueReadingButtonSizeLarge = 32.dp
private val ContinueReadingButtonIconSizeSmall = 16.dp
private val ContinueReadingButtonIconSizeLarge = 20.dp
private val ContinueReadingButtonSize = 28.dp
private val ContinueReadingButtonGridPadding = 6.dp
private val ContinueReadingButtonListSpacing = 8.dp
private const val GRID_SELECTED_COVER_ALPHA = 0.76f
private const val GridSelectedCoverAlpha = 0.76f
/**
* Layout of grid list item with title overlaying the cover.
@ -70,7 +62,7 @@ private const val GRID_SELECTED_COVER_ALPHA = 0.76f
*/
@Composable
fun MangaCompactGridItem(
coverData: MangaCoverModel,
coverData: tachiyomi.domain.manga.model.MangaCover,
onClick: () -> Unit,
onLongClick: () -> Unit,
isSelected: Boolean = false,
@ -90,7 +82,7 @@ fun MangaCompactGridItem(
MangaCover.Book(
modifier = Modifier
.fillMaxWidth()
.alpha(if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha),
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
data = coverData,
)
},
@ -104,12 +96,10 @@ fun MangaCompactGridItem(
)
} else if (onClickContinueReading != null) {
ContinueReadingButton(
size = ContinueReadingButtonSizeLarge,
iconSize = ContinueReadingButtonIconSizeLarge,
onClick = onClickContinueReading,
modifier = Modifier
.padding(ContinueReadingButtonGridPadding)
.align(Alignment.BottomEnd),
onClickContinueReading = onClickContinueReading,
)
}
},
@ -158,13 +148,11 @@ private fun BoxScope.CoverTextOverlay(
)
if (onClickContinueReading != null) {
ContinueReadingButton(
size = ContinueReadingButtonSizeSmall,
iconSize = ContinueReadingButtonIconSizeSmall,
onClick = onClickContinueReading,
modifier = Modifier.padding(
end = ContinueReadingButtonGridPadding,
bottom = ContinueReadingButtonGridPadding,
),
onClickContinueReading = onClickContinueReading,
)
}
}
@ -175,7 +163,7 @@ private fun BoxScope.CoverTextOverlay(
*/
@Composable
fun MangaComfortableGridItem(
coverData: MangaCoverModel,
coverData: tachiyomi.domain.manga.model.MangaCover,
title: String,
onClick: () -> Unit,
onLongClick: () -> Unit,
@ -197,7 +185,7 @@ fun MangaComfortableGridItem(
MangaCover.Book(
modifier = Modifier
.fillMaxWidth()
.alpha(if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha),
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
data = coverData,
)
},
@ -206,12 +194,10 @@ fun MangaComfortableGridItem(
content = {
if (onClickContinueReading != null) {
ContinueReadingButton(
size = ContinueReadingButtonSizeLarge,
iconSize = ContinueReadingButtonIconSizeLarge,
onClick = onClickContinueReading,
modifier = Modifier
.padding(ContinueReadingButtonGridPadding)
.align(Alignment.BottomEnd),
onClickContinueReading = onClickContinueReading,
)
}
},
@ -323,14 +309,14 @@ private fun GridItemSelectable(
private fun Modifier.selectedOutline(
isSelected: Boolean,
color: Color,
) = drawBehind { if (isSelected) drawRect(color = color) }
) = this then drawBehind { if (isSelected) drawRect(color = color) }
/**
* Layout of list item.
*/
@Composable
fun MangaListItem(
coverData: MangaCoverModel,
coverData: tachiyomi.domain.manga.model.MangaCover,
title: String,
onClick: () -> Unit,
onLongClick: () -> Unit,
@ -368,10 +354,8 @@ fun MangaListItem(
BadgeGroup(content = badge)
if (onClickContinueReading != null) {
ContinueReadingButton(
size = ContinueReadingButtonSizeSmall,
iconSize = ContinueReadingButtonIconSizeSmall,
onClick = onClickContinueReading,
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing),
onClickContinueReading = onClickContinueReading,
)
}
}
@ -379,25 +363,23 @@ fun MangaListItem(
@Composable
private fun ContinueReadingButton(
size: Dp,
iconSize: Dp,
onClick: () -> Unit,
modifier: Modifier = Modifier,
onClickContinueReading: () -> Unit,
) {
Box(modifier = modifier) {
FilledIconButton(
onClick = onClick,
onClick = onClickContinueReading,
modifier = Modifier.size(ContinueReadingButtonSize),
shape = MaterialTheme.shapes.small,
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
),
modifier = Modifier.size(size),
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = stringResource(MR.strings.action_resume),
modifier = Modifier.size(iconSize),
modifier = Modifier.size(16.dp),
)
}
}

View File

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

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -23,6 +22,7 @@ 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

View File

@ -21,7 +21,9 @@ internal fun LibraryTabs(
getNumberOfMangaForCategory: (Category) -> Int?,
onTabItemClick: (Int) -> Unit,
) {
// SY -->
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
// SY <--
Column(
modifier = Modifier.zIndex(1f),
) {

View File

@ -38,10 +38,8 @@ fun LibraryToolbar(
onClickRefresh: () -> Unit,
onClickGlobalUpdate: () -> Unit,
onClickOpenRandomManga: () -> Unit,
onClickSyncNow: () -> Unit,
// SY -->
onClickSyncExh: (() -> Unit)?,
isSyncEnabled: Boolean,
// SY <--
searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
@ -62,10 +60,8 @@ fun LibraryToolbar(
onClickRefresh = onClickRefresh,
onClickGlobalUpdate = onClickGlobalUpdate,
onClickOpenRandomManga = onClickOpenRandomManga,
onClickSyncNow = onClickSyncNow,
// SY -->
onClickSyncExh = onClickSyncExh,
isSyncEnabled = isSyncEnabled,
// SY <--
scrollBehavior = scrollBehavior,
)
@ -81,10 +77,8 @@ private fun LibraryRegularToolbar(
onClickRefresh: () -> Unit,
onClickGlobalUpdate: () -> Unit,
onClickOpenRandomManga: () -> Unit,
onClickSyncNow: () -> Unit,
// SY -->
onClickSyncExh: (() -> Unit)?,
isSyncEnabled: Boolean,
// SY <--
scrollBehavior: TopAppBarScrollBehavior?,
) {
@ -131,6 +125,7 @@ private fun LibraryRegularToolbar(
title = stringResource(MR.strings.action_open_random_manga),
onClick = onClickOpenRandomManga,
),
).builder().apply {
// SY -->
if (onClickSyncExh != null) {
@ -141,14 +136,6 @@ private fun LibraryRegularToolbar(
),
)
}
if (isSyncEnabled) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.sync_library),
onClick = onClickSyncNow,
),
)
}
// SY <--
}.build(),
)

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.window.DialogProperties
import exh.favorites.FavoritesSyncStatus
import kotlinx.coroutines.delay
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import kotlin.time.Duration.Companion.seconds
@ -22,6 +23,7 @@ import kotlin.time.Duration.Companion.seconds
data class SyncFavoritesProgressProperties(
val title: String,
val text: String,
val canDismiss: Boolean,
val positiveButtonText: String? = null,
val positiveButton: (() -> Unit)? = null,
val negativeButtonText: String? = null,
@ -32,23 +34,18 @@ data class SyncFavoritesProgressProperties(
fun SyncFavoritesProgressDialog(
status: FavoritesSyncStatus,
setStatusIdle: () -> Unit,
openManga: (Long) -> Unit,
openManga: (Manga) -> Unit,
) {
val context = LocalContext.current
val properties by produceState<SyncFavoritesProgressProperties?>(initialValue = null, status) {
when (status) {
is FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories -> value = SyncFavoritesProgressProperties(
title = context.stringResource(SYMR.strings.favorites_sync_error),
text = context.stringResource(
SYMR.strings.favorites_sync_bad_library_state,
context.stringResource(
SYMR.strings.favorites_sync_gallery_in_multiple_categories, status.mangaTitle,
status.categories.joinToString(),
),
),
text = context.stringResource(SYMR.strings.favorites_sync_bad_library_state, status.message),
canDismiss = false,
positiveButtonText = context.stringResource(SYMR.strings.show_gallery),
positiveButton = {
openManga(status.mangaId)
openManga(status.manga)
setStatusIdle()
},
negativeButtonText = context.stringResource(MR.strings.action_ok),
@ -56,122 +53,31 @@ fun SyncFavoritesProgressDialog(
)
is FavoritesSyncStatus.CompleteWithErrors -> value = SyncFavoritesProgressProperties(
title = context.stringResource(SYMR.strings.favorites_sync_done_errors),
text = context.stringResource(
SYMR.strings.favorites_sync_done_errors_message,
status.messages.joinToString(separator = "\n") {
when (it) {
is FavoritesSyncStatus.SyncError.GallerySyncError.GalleryAddFail ->
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
context.stringResource(
SYMR.strings.favorites_sync_failed_to_add_to_local_error, it.title, it.reason,
)
is FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail ->
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
context.stringResource(
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, it.title, it.url,
)
is FavoritesSyncStatus.SyncError.GallerySyncError.UnableToAddGalleryToRemote ->
context.stringResource(SYMR.strings.favorites_sync_unable_to_add_to_remote, it.title, it.gid)
FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote ->
context.stringResource(SYMR.strings.favorites_sync_unable_to_delete)
}
},
),
text = context.stringResource(SYMR.strings.favorites_sync_done_errors_message, status.message),
canDismiss = false,
positiveButtonText = context.stringResource(MR.strings.action_ok),
positiveButton = setStatusIdle,
)
is FavoritesSyncStatus.Error -> value = SyncFavoritesProgressProperties(
title = context.stringResource(SYMR.strings.favorites_sync_error),
text = context.stringResource(SYMR.strings.favorites_sync_error_string, status.message),
canDismiss = false,
positiveButtonText = context.stringResource(MR.strings.action_ok),
positiveButton = setStatusIdle,
)
is FavoritesSyncStatus.Idle -> value = null
is FavoritesSyncStatus.Initializing -> {
is FavoritesSyncStatus.Initializing, is FavoritesSyncStatus.Processing -> {
value = SyncFavoritesProgressProperties(
title = context.stringResource(SYMR.strings.favorites_syncing),
text = context.stringResource(SYMR.strings.favorites_sync_initializing),
text = status.message,
canDismiss = false,
)
}
is FavoritesSyncStatus.SyncError -> value = SyncFavoritesProgressProperties(
title = context.stringResource(SYMR.strings.favorites_sync_error),
text = context.stringResource(
SYMR.strings.favorites_sync_error_string,
when (status) {
FavoritesSyncStatus.SyncError.NotLoggedInSyncError -> context.stringResource(SYMR.strings.please_login)
FavoritesSyncStatus.SyncError.FailedToFetchFavorites ->
context.stringResource(SYMR.strings.favorites_sync_failed_to_featch)
is FavoritesSyncStatus.SyncError.UnknownSyncError ->
context.stringResource(SYMR.strings.favorites_sync_unknown_error, status.message)
is FavoritesSyncStatus.SyncError.GallerySyncError.GalleryAddFail ->
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
context.stringResource(
SYMR.strings.favorites_sync_failed_to_add_to_local_error, status.title, status.reason,
)
is FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail ->
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
context.stringResource(
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, status.title, status.url,
)
is FavoritesSyncStatus.SyncError.GallerySyncError.UnableToAddGalleryToRemote ->
context.stringResource(SYMR.strings.favorites_sync_unable_to_add_to_remote, status.title, status.gid)
FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote ->
context.stringResource(SYMR.strings.favorites_sync_unable_to_delete)
},
),
positiveButtonText = context.stringResource(MR.strings.action_ok),
positiveButton = setStatusIdle,
)
is FavoritesSyncStatus.Processing -> {
val properties = SyncFavoritesProgressProperties(
title = context.stringResource(SYMR.strings.favorites_syncing),
text = when (status) {
FavoritesSyncStatus.Processing.VerifyingLibrary ->
context.stringResource(SYMR.strings.favorites_sync_verifying_library)
FavoritesSyncStatus.Processing.DownloadingFavorites ->
context.stringResource(SYMR.strings.favorites_sync_downloading)
FavoritesSyncStatus.Processing.CalculatingRemoteChanges ->
context.stringResource(SYMR.strings.favorites_sync_calculating_remote_changes)
FavoritesSyncStatus.Processing.CalculatingLocalChanges ->
context.stringResource(SYMR.strings.favorites_sync_calculating_local_changes)
FavoritesSyncStatus.Processing.SyncingCategoryNames ->
context.stringResource(SYMR.strings.favorites_sync_syncing_category_names)
is FavoritesSyncStatus.Processing.RemovingRemoteGalleries ->
context.stringResource(SYMR.strings.favorites_sync_removing_galleries, status.galleryCount)
is FavoritesSyncStatus.Processing.AddingGalleryToRemote ->
if (status.isThrottling) {
context.stringResource(
SYMR.strings.favorites_sync_processing_throttle,
context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, status.index, status.total),
)
} else {
context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, status.index, status.total)
}
is FavoritesSyncStatus.Processing.RemovingGalleryFromLocal ->
context.stringResource(SYMR.strings.favorites_sync_remove_from_local, status.index, status.total)
is FavoritesSyncStatus.Processing.AddingGalleryToLocal ->
if (status.isThrottling) {
context.stringResource(
SYMR.strings.favorites_sync_processing_throttle,
context.stringResource(SYMR.strings.favorites_sync_add_to_local, status.index, status.total),
)
} else {
context.stringResource(SYMR.strings.favorites_sync_add_to_local, status.index, status.total)
}
FavoritesSyncStatus.Processing.CleaningUp ->
context.stringResource(SYMR.strings.favorites_sync_cleaning_up)
},
)
value = properties
if (
status is FavoritesSyncStatus.Processing.AddingGalleryToRemote ||
status is FavoritesSyncStatus.Processing.AddingGalleryToLocal
) {
if (status is FavoritesSyncStatus.Processing && status.title != null) {
delay(5.seconds)
value = properties.copy(
text = when (status) {
is FavoritesSyncStatus.Processing.AddingGalleryToRemote ->
properties.text + "\n\n" + status.title
is FavoritesSyncStatus.Processing.AddingGalleryToLocal ->
properties.text + "\n\n" + status.title
else -> properties.text
},
value = SyncFavoritesProgressProperties(
title = context.stringResource(SYMR.strings.favorites_syncing),
text = status.delayedMessage ?: status.message,
canDismiss = false,
)
}
}
@ -206,8 +112,8 @@ fun SyncFavoritesProgressDialog(
}
},
properties = DialogProperties(
dismissOnClickOutside = false,
dismissOnBackPress = false,
dismissOnClickOutside = dialog.canDismiss,
dismissOnBackPress = dialog.canDismiss,
),
)
}

View File

@ -21,14 +21,13 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.unit.dp
import eu.kanade.domain.base.BasePreferences
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
@ -41,8 +40,6 @@ import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.active
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun ChapterSettingsDialog(
@ -66,8 +63,6 @@ fun ChapterSettingsDialog(
)
}
val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly().get() }
TabbedDialog(
onDismissRequest = onDismissRequest,
tabTitles = persistentListOf(
@ -102,7 +97,7 @@ fun ChapterSettingsDialog(
FilterPage(
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged
.takeUnless { downloadedOnly },
.takeUnless { manga?.forceDownloaded() == true },
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
onUnreadFilterChanged = onUnreadFilterChanged,
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,

View File

@ -1,406 +1,59 @@
package eu.kanade.presentation.manga
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
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.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Brush
import androidx.compose.material.icons.filled.PersonOutline
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.AttachMoney
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMaxOfOrNull
import coil3.request.ImageRequest
import coil3.request.crossfade
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun DuplicateMangaDialog(
duplicates: List<MangaWithChapterCount>,
onDismissRequest: () -> Unit,
onConfirm: () -> Unit,
onOpenManga: (manga: Manga) -> Unit,
onMigrate: (manga: Manga) -> Unit,
modifier: Modifier = Modifier,
onOpenManga: () -> Unit,
) {
val sourceManager = remember { Injekt.get<SourceManager>() }
val minHeight = LocalPreferenceMinHeight.current
val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal)
val horizontalPaddingModifier = Modifier.padding(horizontalPadding)
AdaptiveSheet(
modifier = modifier,
AlertDialog(
onDismissRequest = onDismissRequest,
) {
Column(
modifier = Modifier
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState())
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
Text(
text = stringResource(MR.strings.possible_duplicates_title),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.then(horizontalPaddingModifier)
.padding(top = MaterialTheme.padding.small),
)
Text(
text = stringResource(MR.strings.possible_duplicates_summary),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.then(horizontalPaddingModifier),
)
LazyRow(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)),
contentPadding = horizontalPadding,
title = {
Text(text = stringResource(MR.strings.are_you_sure))
},
text = {
Text(text = stringResource(MR.strings.confirm_add_duplicate_manga))
},
confirmButton = {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
items(
items = duplicates,
key = { it.manga.id },
TextButton(
onClick = {
onDismissRequest()
onOpenManga()
},
) {
DuplicateMangaListItem(
duplicate = it,
getSource = { sourceManager.getOrStub(it.manga.source) },
onMigrate = { onMigrate(it.manga) },
onDismissRequest = onDismissRequest,
onOpenManga = { onOpenManga(it.manga) },
)
Text(text = stringResource(MR.strings.action_show_manga))
}
}
Column(modifier = horizontalPaddingModifier) {
HorizontalDivider()
Spacer(modifier = Modifier.weight(1f))
TextPreferenceWidget(
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
TextButton(
onClick = {
onDismissRequest()
onConfirm()
},
modifier = Modifier.clip(CircleShape),
)
) {
Text(text = stringResource(MR.strings.action_add))
}
}
OutlinedButton(
onClick = onDismissRequest,
modifier = Modifier
.then(horizontalPaddingModifier)
.padding(bottom = MaterialTheme.padding.medium)
.heightIn(min = minHeight)
.fillMaxWidth(),
) {
Text(
modifier = Modifier.padding(vertical = MaterialTheme.padding.extraSmall),
text = stringResource(MR.strings.action_cancel),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyLarge,
)
}
}
}
},
)
}
@Composable
private fun DuplicateMangaListItem(
duplicate: MangaWithChapterCount,
getSource: () -> Source,
onDismissRequest: () -> Unit,
onOpenManga: () -> Unit,
onMigrate: () -> Unit,
) {
val source = getSource()
val manga = duplicate.manga
Column(
modifier = Modifier
.width(MangaCardWidth)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surface)
.combinedClickable(
onLongClick = { onOpenManga() },
onClick = {
onDismissRequest()
onMigrate()
},
)
.padding(MaterialTheme.padding.small),
) {
Box {
MangaCover.Book(
data = ImageRequest.Builder(LocalContext.current)
.data(manga)
.crossfade(true)
.build(),
modifier = Modifier.fillMaxWidth(),
)
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
) {
Badge(
color = MaterialTheme.colorScheme.secondary,
textColor = MaterialTheme.colorScheme.onSecondary,
text = pluralStringResource(
MR.plurals.manga_num_chapters,
duplicate.chapterCount.toInt(),
duplicate.chapterCount,
),
)
}
}
Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
Text(
text = manga.title,
style = MaterialTheme.typography.titleSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
)
if (!manga.author.isNullOrBlank()) {
MangaDetailRow(
text = manga.author!!,
iconImageVector = Icons.Filled.PersonOutline,
maxLines = 2,
)
}
if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
MangaDetailRow(
text = manga.artist!!,
iconImageVector = Icons.Filled.Brush,
maxLines = 2,
)
}
MangaDetailRow(
text = when (manga.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)
},
iconImageVector = when (manga.status) {
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
else -> Icons.Outlined.Block
},
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
if (source is StubSource) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.error,
)
}
Text(
text = source.name,
style = MaterialTheme.typography.labelSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
@Composable
private fun MangaDetailRow(
text: String,
iconImageVector: ImageVector,
maxLines: Int = 1,
) {
Row(
modifier = Modifier
.secondaryItemAlpha()
.padding(top = MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = iconImageVector,
contentDescription = null,
modifier = Modifier.size(MangaDetailsIconWidth),
)
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
)
}
}
@Composable
private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp {
val density = LocalDensity.current
val typography = MaterialTheme.typography
val textMeasurer = rememberTextMeasurer()
val smallPadding = with(density) { MaterialTheme.padding.small.roundToPx() }
val extraSmallPadding = with(density) { MaterialTheme.padding.extraSmall.roundToPx() }
val width = with(density) { MangaCardWidth.roundToPx() - (2 * smallPadding) }
val iconWidth = with(density) { MangaDetailsIconWidth.roundToPx() }
val coverHeight = width / MangaCover.Book.ratio
val constraints = Constraints(maxWidth = width)
val detailsConstraints = Constraints(maxWidth = width - iconWidth - extraSmallPadding)
return remember(
duplicates,
density,
typography,
textMeasurer,
smallPadding,
extraSmallPadding,
coverHeight,
constraints,
detailsConstraints,
) {
duplicates.fastMaxOfOrNull {
calculateMangaCardHeight(
manga = it.manga,
density = density,
typography = typography,
textMeasurer = textMeasurer,
smallPadding = smallPadding,
extraSmallPadding = extraSmallPadding,
coverHeight = coverHeight,
constraints = constraints,
detailsConstraints = detailsConstraints,
)
}
?: 0.dp
}
}
private fun calculateMangaCardHeight(
manga: Manga,
density: Density,
typography: Typography,
textMeasurer: TextMeasurer,
smallPadding: Int,
extraSmallPadding: Int,
coverHeight: Float,
constraints: Constraints,
detailsConstraints: Constraints,
): Dp {
val titleHeight = textMeasurer.measureHeight(manga.title, typography.titleSmall, 2, constraints)
val authorHeight = if (!manga.author.isNullOrBlank()) {
textMeasurer.measureHeight(manga.author!!, typography.bodySmall, 2, detailsConstraints)
} else {
0
}
val artistHeight = if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
textMeasurer.measureHeight(manga.artist!!, typography.bodySmall, 2, detailsConstraints)
} else {
0
}
val statusHeight = textMeasurer.measureHeight("", typography.bodySmall, 2, detailsConstraints)
val sourceHeight = textMeasurer.measureHeight("", typography.labelSmall, 1, constraints)
val totalHeight = coverHeight + titleHeight + authorHeight + artistHeight + statusHeight + sourceHeight
return with(density) { ((2 * smallPadding) + totalHeight + (5 * extraSmallPadding)).toDp() }
}
private fun TextMeasurer.measureHeight(
text: String,
style: TextStyle,
maxLines: Int,
constraints: Constraints,
): Int = measure(
text = text,
style = style,
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
constraints = constraints,
)
.size
.height
private val MangaCardWidth = 150.dp
private val MangaDetailsIconWidth = 16.dp

View File

@ -1,45 +0,0 @@
package eu.kanade.presentation.manga
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.manga.components.MangaNotesTextArea
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun MangaNotesScreen(
state: MangaNotesScreen.State,
navigateUp: () -> Unit,
onUpdate: (String) -> Unit,
) {
Scaffold(
topBar = { topBarScrollBehavior ->
AppBar(
titleContent = {
AppBarTitle(
title = stringResource(MR.strings.action_edit_notes),
subtitle = state.manga.title,
)
},
navigateUp = navigateUp,
scrollBehavior = topBarScrollBehavior,
)
},
) { contentPadding ->
MangaNotesTextArea(
state = state,
onUpdate = onUpdate,
modifier = Modifier
.padding(contentPadding)
.consumeWindowInsets(contentPadding)
.imePadding(),
)
}
}

View File

@ -77,7 +77,6 @@ import eu.kanade.tachiyomi.source.online.english.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino
import eu.kanade.tachiyomi.ui.manga.ChapterList
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
import eu.kanade.tachiyomi.ui.manga.MergedMangaData
import eu.kanade.tachiyomi.ui.manga.PagePreviewState
import eu.kanade.tachiyomi.util.system.copyToClipboard
import exh.metadata.MetadataUtil
@ -103,11 +102,11 @@ import tachiyomi.presentation.core.components.material.ExtendedFloatingActionBut
import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.shouldExpandFAB
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.source.local.isLocal
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.Date
@Composable
fun MangaScreen(
@ -117,7 +116,7 @@ fun MangaScreen(
isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
navigateUp: () -> Unit,
onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
@ -142,7 +141,6 @@ fun MangaScreen(
onEditCategoryClicked: (() -> Unit)?,
onEditFetchIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// SY -->
onMetadataViewerClicked: () -> Unit,
onEditInfoClicked: () -> Unit,
@ -152,7 +150,6 @@ fun MangaScreen(
onMergeWithAnotherClicked: () -> Unit,
onOpenPagePreview: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit,
previewsRowCount: Int,
// SY <--
// For bottom action menu
@ -183,7 +180,7 @@ fun MangaScreen(
nextUpdate = nextUpdate,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
navigateUp = navigateUp,
onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked,
@ -202,7 +199,6 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked,
onEditNotesClicked = onEditNotesClicked,
// SY -->
onMetadataViewerClicked = onMetadataViewerClicked,
onEditInfoClicked = onEditInfoClicked,
@ -212,7 +208,6 @@ fun MangaScreen(
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
onOpenPagePreview = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked,
previewsRowCount = previewsRowCount,
// SY <--
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
@ -230,7 +225,7 @@ fun MangaScreen(
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
nextUpdate = nextUpdate,
navigateUp = navigateUp,
onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked,
@ -249,7 +244,6 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked,
onEditNotesClicked = onEditNotesClicked,
// SY -->
onMetadataViewerClicked = onMetadataViewerClicked,
onEditInfoClicked = onEditInfoClicked,
@ -259,7 +253,6 @@ fun MangaScreen(
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
onOpenPagePreview = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked,
previewsRowCount = previewsRowCount,
// SY <--
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
@ -280,7 +273,7 @@ private fun MangaScreenSmallImpl(
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
navigateUp: () -> Unit,
onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
@ -306,7 +299,6 @@ private fun MangaScreenSmallImpl(
onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// SY -->
onMetadataViewerClicked: () -> Unit,
onEditInfoClicked: () -> Unit,
@ -316,7 +308,6 @@ private fun MangaScreenSmallImpl(
onMergeWithAnotherClicked: () -> Unit,
onOpenPagePreview: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit,
previewsRowCount: Int,
// SY <--
// For bottom action menu
@ -349,9 +340,14 @@ private fun MangaScreenSmallImpl(
}
// SY <--
BackHandler(enabled = isAnySelected) {
onAllChapterSelected(false)
val internalOnBackPressed = {
if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
}
BackHandler(onBack = internalOnBackPressed)
Scaffold(
topBar = {
@ -364,25 +360,26 @@ private fun MangaScreenSmallImpl(
val isFirstItemScrolled by remember {
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
}
val titleAlpha by animateFloatAsState(
val animatedTitleAlpha by animateFloatAsState(
if (!isFirstItemVisible) 1f else 0f,
label = "Top Bar Title",
)
val backgroundAlpha by animateFloatAsState(
val animatedBgAlpha by animateFloatAsState(
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
label = "Top Bar Background",
)
MangaToolbar(
title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.filterActive,
navigateUp = navigateUp,
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickEditNotes = onEditNotesClicked,
// SY -->
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
@ -390,11 +387,8 @@ private fun MangaScreenSmallImpl(
onClickMerge = onMergeClicked.takeIf { state.showMergeInOverflow },
// SY <--
actionModeCounter = selectedChapterCount,
onCancelActionMode = { onAllChapterSelected(false) },
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { titleAlpha },
backgroundAlphaProvider = { backgroundAlpha },
)
},
bottomBar = {
@ -432,7 +426,7 @@ private fun MangaScreenSmallImpl(
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
)
}
},
@ -442,7 +436,7 @@ private fun MangaScreenSmallImpl(
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = !isAnySelected,
enabled = { !isAnySelected },
indicatorPadding = PaddingValues(top = topPadding),
) {
val layoutDirection = LocalLayoutDirection.current
@ -467,9 +461,13 @@ private fun MangaScreenSmallImpl(
MangaInfoBox(
isTabletUi = false,
appBarPadding = topPadding,
manga = state.manga,
title = state.manga.title,
author = state.manga.author,
artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
isStubSource = remember { state.source is StubSource },
coverDataProvider = { state.manga },
status = state.manga.status,
onCoverClick = onCoverClicked,
doSearch = onSearch,
)
@ -520,10 +518,8 @@ private fun MangaScreenSmallImpl(
defaultExpandState = state.isFromSource,
description = state.manga.description,
tagsProvider = { state.manga.genre },
notes = state.manga.notes,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onEditNotes = onEditNotesClicked,
// SY -->
doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
@ -548,14 +544,13 @@ private fun MangaScreenSmallImpl(
}
}
if (state.pagePreviewsState !is PagePreviewState.Unused && previewsRowCount > 0) {
if (state.pagePreviewsState !is PagePreviewState.Unused) {
PagePreviewItems(
pagePreviewState = state.pagePreviewsState,
onOpenPage = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked,
maxWidth = maxWidth,
setMaxWidth = { maxWidth = it },
rowCount = previewsRowCount,
setMaxWidth = { maxWidth = it }
)
}
// SY <--
@ -577,7 +572,6 @@ private fun MangaScreenSmallImpl(
sharedChapterItems(
manga = state.manga,
mergedData = state.mergedData,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
chapterSwipeStartAction = chapterSwipeStartAction,
@ -603,7 +597,7 @@ fun MangaScreenLargeImpl(
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
navigateUp: () -> Unit,
onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
@ -629,7 +623,6 @@ fun MangaScreenLargeImpl(
onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// SY -->
onMetadataViewerClicked: () -> Unit,
onEditInfoClicked: () -> Unit,
@ -639,7 +632,6 @@ fun MangaScreenLargeImpl(
onMergeWithAnotherClicked: () -> Unit,
onOpenPagePreview: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit,
previewsRowCount: Int,
// SY <--
// For bottom action menu
@ -676,9 +668,14 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState()
BackHandler(enabled = isAnySelected) {
onAllChapterSelected(false)
val internalOnBackPressed = {
if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
}
BackHandler(onBack = internalOnBackPressed)
Scaffold(
topBar = {
@ -688,27 +685,25 @@ fun MangaScreenLargeImpl(
MangaToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive,
navigateUp = navigateUp,
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickEditNotes = onEditNotesClicked,
// SY -->
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
onClickMergedSettings = onMergedSettingsClicked.takeIf { state.manga.source == MERGED_SOURCE_ID },
onClickMerge = onMergeClicked.takeIf { state.showMergeInOverflow },
// SY <--
onCancelActionMode = { onAllChapterSelected(false) },
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { 1f },
backgroundAlphaProvider = { 1f },
)
},
bottomBar = {
@ -753,7 +748,7 @@ fun MangaScreenLargeImpl(
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
)
}
},
@ -761,7 +756,7 @@ fun MangaScreenLargeImpl(
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = !isAnySelected,
enabled = { !isAnySelected },
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
@ -782,9 +777,13 @@ fun MangaScreenLargeImpl(
MangaInfoBox(
isTabletUi = true,
appBarPadding = contentPadding.calculateTopPadding(),
manga = state.manga,
title = state.manga.title,
author = state.manga.author,
artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
isStubSource = remember { state.source is StubSource },
coverDataProvider = { state.manga },
status = state.manga.status,
onCoverClick = onCoverClicked,
doSearch = onSearch,
)
@ -815,10 +814,8 @@ fun MangaScreenLargeImpl(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
notes = state.manga.notes,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onEditNotes = onEditNotesClicked,
// SY -->
doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
@ -835,12 +832,11 @@ fun MangaScreenLargeImpl(
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
)
}
if (state.pagePreviewsState !is PagePreviewState.Unused && previewsRowCount > 0) {
if (state.pagePreviewsState !is PagePreviewState.Unused) {
PagePreviews(
pagePreviewState = state.pagePreviewsState,
onOpenPage = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked,
rowCount = previewsRowCount,
)
}
// SY <--
@ -876,7 +872,6 @@ fun MangaScreenLargeImpl(
sharedChapterItems(
manga = state.manga,
mergedData = state.mergedData,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
chapterSwipeStartAction = chapterSwipeStartAction,
@ -941,7 +936,6 @@ private fun SharedMangaBottomActionMenu(
private fun LazyListScope.sharedChapterItems(
manga: Manga,
mergedData: MergedMangaData?,
chapters: List<ChapterList>,
isAnyChapterSelected: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@ -985,17 +979,14 @@ private fun LazyListScope.sharedChapterItems(
?.let {
// SY -->
if (manga.isEhBasedManga()) {
MetadataUtil.EX_DATE_FORMAT
.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()))
MetadataUtil.EX_DATE_FORMAT.format(Date(it))
} else {
relativeDateText(item.chapter.dateUpload)
relativeDateText(Date(item.chapter.dateUpload))
}
// SY <--
},
readProgress = item.chapter.lastPageRead
.takeIf {
/* SY --> */(!item.chapter.read || alwaysShowReadingProgress)/* SY <-- */ && it > 0L
}
.takeIf { /* SY --> */(!item.chapter.read || alwaysShowReadingProgress)/* SY <-- */ && it > 0L }
?.let {
stringResource(
MR.strings.chapter_progress,
@ -1011,8 +1002,7 @@ private fun LazyListScope.sharedChapterItems(
read = item.chapter.read,
bookmark = item.chapter.bookmark,
selected = item.selected,
downloadIndicatorEnabled =
!isAnyChapterSelected && !(mergedData?.manga?.get(item.chapter.mangaId) ?: manga).isLocal(),
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
downloadStateProvider = { item.downloadState },
downloadProgressProvider = { item.downloadProgress },
chapterSwipeStartAction = chapterSwipeStartAction,

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.manga.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -9,13 +10,13 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.ripple
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -23,9 +24,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
@ -91,7 +91,6 @@ private fun NotDownloadedIndicator(
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = LocalHapticFeedback.current,
onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
onClick = { onClick(ChapterDownloadAction.START) },
)
@ -121,7 +120,6 @@ private fun DownloadingIndicator(
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = LocalHapticFeedback.current,
onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
onClick = { isMenuExpanded = true },
),
@ -138,8 +136,6 @@ private fun DownloadingIndicator(
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorStrokeWidth,
trackColor = Color.Transparent,
strokeCap = StrokeCap.Butt,
)
} else {
val animatedProgress by animateFloatAsState(
@ -156,9 +152,6 @@ private fun DownloadingIndicator(
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorSize / 2,
trackColor = Color.Transparent,
strokeCap = StrokeCap.Butt,
gapSize = 0.dp,
)
}
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
@ -198,7 +191,6 @@ private fun DownloadedIndicator(
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = LocalHapticFeedback.current,
onLongClick = { isMenuExpanded = true },
onClick = { isMenuExpanded = true },
),
@ -233,7 +225,6 @@ private fun ErrorIndicator(
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = LocalHapticFeedback.current,
onLongClick = { onClick(ChapterDownloadAction.START) },
onClick = { onClick(ChapterDownloadAction.START) },
),
@ -250,23 +241,26 @@ private fun ErrorIndicator(
private fun Modifier.commonClickable(
enabled: Boolean,
hapticFeedback: HapticFeedback,
onLongClick: () -> Unit,
onClick: () -> Unit,
) = this.combinedClickable(
enabled = enabled,
onLongClick = {
onLongClick()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = onClick,
role = Role.Button,
interactionSource = null,
indication = ripple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
)
) = composed {
val haptic = LocalHapticFeedback.current
Modifier.combinedClickable(
enabled = enabled,
onLongClick = {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = onClick,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
)
}
private val IndicatorSize = 26.dp
private val IndicatorPadding = 2.dp

View File

@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
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
@ -60,6 +60,6 @@ private fun MissingChaptersWarning(count: Int) {
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error.copy(alpha = SECONDARY_ALPHA),
color = MaterialTheme.colorScheme.error.copy(alpha = SecondaryItemAlpha),
)
}

View File

@ -8,6 +8,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -28,15 +29,16 @@ 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.MoreVert
import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.icons.outlined.SwapCalls
import androidx.compose.material.ripple
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
@ -88,7 +90,7 @@ fun MangaBottomActionMenu(
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 3.dp,
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
@ -197,7 +199,7 @@ private fun RowScope.Button(
.size(48.dp)
.weight(animatedWeight)
.combinedClickable(
interactionSource = null,
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onLongClick = onLongClick,
onClick = onClick,
@ -236,9 +238,7 @@ fun LibraryBottomActionMenu(
// SY -->
onClickCleanTitles: (() -> Unit)?,
onClickMigrate: (() -> Unit)?,
onClickCollectRecommendations: (() -> Unit)?,
onClickAddToMangaDex: (() -> Unit)?,
onClickResetInfo: (() -> Unit)?,
// SY <--
modifier: Modifier = Modifier,
) {
@ -251,7 +251,7 @@ fun LibraryBottomActionMenu(
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 3.dp,
) {
val haptic = LocalHapticFeedback.current
val confirm =
@ -267,10 +267,7 @@ fun LibraryBottomActionMenu(
}
}
// SY -->
val showOverflow = onClickCleanTitles != null ||
onClickAddToMangaDex != null ||
onClickResetInfo != null ||
onClickCollectRecommendations != null
val showOverflow = onClickCleanTitles != null || onClickAddToMangaDex != null
val configuration = LocalConfiguration.current
val moveMarkPrev = remember { !configuration.isTabletUi() }
var overFlowOpen by remember { mutableStateOf(false) }
@ -361,24 +358,12 @@ fun LibraryBottomActionMenu(
onClick = onClickMigrate,
)
}
if (onClickCollectRecommendations != null) {
DropdownMenuItem(
text = { Text(stringResource(SYMR.strings.rec_search_short)) },
onClick = onClickCollectRecommendations,
)
}
if (onClickAddToMangaDex != null) {
DropdownMenuItem(
text = { Text(stringResource(SYMR.strings.mangadex_add_to_follows)) },
onClick = onClickAddToMangaDex,
)
}
if (onClickResetInfo != null) {
DropdownMenuItem(
text = { Text(text = stringResource(SYMR.strings.reset_info)) },
onClick = onClickResetInfo,
)
}
}
} else {
Button(

View File

@ -24,26 +24,36 @@ import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
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.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.DISABLED_ALPHA
import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
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(
@ -68,134 +78,158 @@ fun MangaChapterListItem(
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
modifier: Modifier = Modifier,
) {
val start = getSwipeAction(
action = chapterSwipeStartAction,
read = read,
bookmark = bookmark,
downloadState = downloadStateProvider(),
background = MaterialTheme.colorScheme.primaryContainer,
onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
)
val end = getSwipeAction(
action = chapterSwipeEndAction,
read = read,
bookmark = bookmark,
downloadState = downloadStateProvider(),
background = MaterialTheme.colorScheme.primaryContainer,
onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
)
val haptic = LocalHapticFeedback.current
val density = LocalDensity.current
SwipeableActionsBox(
modifier = Modifier.clipToBounds(),
startActions = listOfNotNull(start),
endActions = listOfNotNull(end),
swipeThreshold = swipeActionThreshold,
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
val textAlpha = if (read) ReadItemAlpha else 1f
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
// Increase touch slop of swipe action to reduce accidental trigger
val configuration = LocalViewConfiguration.current
CompositionLocalProvider(
LocalViewConfiguration provides object : ViewConfiguration by configuration {
override val touchSlop: Float = configuration.touchSlop * 3f
},
) {
Row(
modifier = modifier
.selectedBackground(selected)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var textHeight by remember { mutableIntStateOf(0) }
if (!read) {
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = stringResource(MR.strings.unread),
modifier = Modifier
.height(8.dp)
.padding(end = 4.dp),
tint = MaterialTheme.colorScheme.primary,
)
}
if (bookmark) {
Icon(
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height },
color = LocalContentColor.current.copy(alpha = if (read) DISABLED_ALPHA else 1f),
)
}
val start = getSwipeAction(
action = chapterSwipeStartAction,
read = read,
bookmark = bookmark,
downloadState = downloadStateProvider(),
background = MaterialTheme.colorScheme.primaryContainer,
onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
)
val end = getSwipeAction(
action = chapterSwipeEndAction,
read = read,
bookmark = bookmark,
downloadState = downloadStateProvider(),
background = MaterialTheme.colorScheme.primaryContainer,
onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
)
Row {
val subtitleStyle = MaterialTheme.typography.bodySmall
.merge(
color = LocalContentColor.current
.copy(alpha = if (read) DISABLED_ALPHA else SECONDARY_ALPHA),
)
ProvideTextStyle(value = subtitleStyle) {
if (date != null) {
Text(
text = date,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
val swipeableActionsState = rememberSwipeableActionsState()
LaunchedEffect(Unit) {
// Haptic effect when swipe over threshold
val swipeActionThresholdPx = with(density) { swipeActionThreshold.toPx() }
snapshotFlow { swipeableActionsState.offset.value.absoluteValue > swipeActionThresholdPx }
.collect { if (it) haptic.performHapticFeedback(HapticFeedbackType.LongPress) }
}
SwipeableActionsBox(
modifier = Modifier.clipToBounds(),
state = swipeableActionsState,
startActions = listOfNotNull(start),
endActions = listOfNotNull(end),
swipeThreshold = swipeActionThreshold,
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
) {
Row(
modifier = modifier
.selectedBackground(selected)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var textHeight by remember { mutableIntStateOf(0) }
if (!read) {
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = stringResource(MR.strings.unread),
modifier = Modifier
.height(8.dp)
.padding(end = 4.dp),
tint = MaterialTheme.colorScheme.primary,
)
if (readProgress != null ||
scanlator != null/* SY --> */ ||
sourceName != null/* SY <-- */
) {
DotSeparatorText()
}
if (bookmark) {
Icon(
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = textAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height },
)
}
Row {
ProvideTextStyle(
value = MaterialTheme.typography.bodyMedium.copy(
fontSize = 12.sp,
color = LocalContentColor.current.copy(alpha = textSubtitleAlpha),
),
) {
if (date != null) {
Text(
text = date,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (
readProgress != null ||
scanlator != null/* SY --> */ ||
sourceName != null/* SY <-- */
) {
DotSeparatorText()
}
}
if (readProgress != null) {
Text(
text = readProgress,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
)
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
}
// SY -->
if (sourceName != null) {
Text(
text = sourceName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (scanlator != null) DotSeparatorText()
}
// SY <--
if (scanlator != null) {
Text(
text = scanlator,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
if (readProgress != null) {
Text(
text = readProgress,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA),
)
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
}
// SY -->
if (sourceName != null) {
Text(
text = sourceName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (scanlator != null) DotSeparatorText()
}
// SY <--
if (scanlator != null) {
Text(
text = scanlator,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = { onDownloadClick?.invoke(it) },
)
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = { onDownloadClick?.invoke(it) },
)
}
}
}
}

View File

@ -11,7 +11,7 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.Role
import coil3.compose.AsyncImage
import coil.compose.AsyncImage
import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R

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