Compare commits

..

13 Commits

Author SHA1 Message Date
Jack Hamilton
dae7a00b41 Whoops spotlessed again 2024-10-12 16:34:40 -05:00
Jack Hamilton
c3adf103d7 Sorted error, spotless'd 2024-10-12 16:33:30 -05:00
Jack Hamilton
945ae821fb Update domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-12 16:28:42 -05:00
Jack Hamilton
aefce87650 Update app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-12 16:28:27 -05:00
Jack Hamilton
c70685584f Update random item bitmask 2024-10-12 15:17:17 -05:00
Jack Hamilton
36c864d873 Update app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-12 15:13:53 -05:00
Jack Hamilton
ab0893b2d4 Changes according to feedback 2024-10-11 21:29:02 -05:00
Jack Hamilton
078758391e Update app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-11 21:07:17 -05:00
Jack Hamilton
2eb1580788 Update app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-11 21:07:08 -05:00
Jack Hamilton
d328ded17f nrevert collections import 2024-10-11 20:03:52 -05:00
Jack Hamilton
80f9dfb699 Spotless 2024-10-11 19:50:23 -05:00
Jack Hamilton
3d087f4428 Keyed random seed 2024-10-11 19:50:23 -05:00
Jack Hamilton
0ab795bfa3 Add random sort option 2024-10-11 19:49:11 -05:00
319 changed files with 2947 additions and 8276 deletions

View File

@@ -1,28 +1,8 @@
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.xml]
indent_size = 4
# noinspection EditorConfigKeyCorrectness
[*.{kt,kts}] [*.{kt,kts}]
indent_size = 4
max_line_length = 120 max_line_length = 120
indent_size = 4
insert_final_newline = true
ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = 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 = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 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

View File

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

View File

@@ -43,9 +43,9 @@ body:
attributes: attributes:
label: Crash logs label: Crash logs
description: | 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: | 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 - type: input
id: mihon-version id: mihon-version
@@ -53,7 +53,7 @@ body:
label: Mihon version label: Mihon version
description: You can find your Mihon version in **More → About**. description: You can find your Mihon version in **More → About**.
placeholder: | placeholder: |
Example: "0.18.0" Example: "0.16.5"
validations: validations:
required: true required: true
@@ -96,9 +96,9 @@ body:
required: true 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 required: true
- label: I have updated the app to version **[0.18.0](https://github.com/mihonapp/mihon/releases/latest)**. - label: I have updated the app to version **[0.16.5](https://github.com/mihonapp/mihon/releases/latest)**.
required: true 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 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 required: true

View File

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

View File

@@ -1,13 +1,6 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"], "extends": ["config:base"],
"labels": ["Dependencies"], "labels": ["Dependencies"],
"semanticCommits": "disabled", "semanticCommits": "disabled"
"packageRules": [
{
"groupName": "GitHub Actions",
"matchManagers": ["github-actions"],
"pinDigests": true,
},
],
} }

View File

@@ -1,13 +1,10 @@
name: PR build check name: PR build check
on: on:
pull_request: pull_request:
paths: paths-ignore:
- '**' - '**.md'
- '!**.md' - 'i18n/src/commonMain/moko-resources/**/strings.xml'
- '!i18n/src/commonMain/moko-resources/**/strings.xml' - 'i18n/src/commonMain/moko-resources/**/plurals.xml'
- '!i18n/src/commonMain/moko-resources/**/plurals.xml'
- 'i18n/src/commonMain/moko-resources/base/strings.xml'
- 'i18n/src/commonMain/moko-resources/base/plurals.xml'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }} group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -19,41 +16,38 @@ permissions:
jobs: jobs:
build: build:
name: Build app name: Build app
runs-on: 'ubuntu-24.04' runs-on: ubuntu-latest
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with: with:
java-version: 17 java-version: 17
distribution: temurin distribution: adopt
- name: Set up gradle - name: Set up gradle
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0 uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Check code format - name: Build app and run unit tests
run: ./gradlew spotlessCheck run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Build app
run: ./gradlew assembleRelease
- name: Run unit tests
run: ./gradlew testReleaseUnitTest
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: arm64-v8a-${{ github.sha }} name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
- name: Upload mapping - name: Upload mapping
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: mapping-${{ github.sha }} name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release path: app/build/outputs/mapping/standardRelease

View File

@@ -13,41 +13,42 @@ concurrency:
jobs: jobs:
build: build:
name: Build app name: Build app
runs-on: 'ubuntu-24.04' runs-on: ubuntu-latest
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Setup Android SDK
run: |
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with: with:
java-version: 17 java-version: 17
distribution: temurin distribution: adopt
- name: Set up gradle - name: Set up gradle
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0 uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Check code format - name: Build app and run unit tests
run: ./gradlew spotlessCheck run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Build app
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
- name: Run unit tests
run: ./gradlew testReleaseUnitTest
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: arm64-v8a-${{ github.sha }} name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
- name: Upload mapping - name: Upload mapping
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: mapping-${{ github.sha }} name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release path: app/build/outputs/mapping/standardRelease
# Sign APK and create release for tags # Sign APK and create release for tags
@@ -59,44 +60,42 @@ jobs:
- name: Sign APK - name: Sign APK
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478 uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
with: with:
releaseDirectory: app/build/outputs/apk/release releaseDirectory: app/build/outputs/apk/standard/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }} signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }} alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: '35.0.1'
- name: Clean up build artifacts - name: Clean up build artifacts
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
run: | run: |
set -e set -e
mv app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
mv app/build/outputs/apk/release/app-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
- name: Create Release - name: Create Release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8
with: with:
tag_name: ${{ env.VERSION_TAG }} tag_name: ${{ env.VERSION_TAG }}
name: Mihon ${{ env.VERSION_TAG }} name: Mihon ${{ env.VERSION_TAG }}
@@ -122,4 +121,5 @@ jobs:
mihon-x86_64-${{ env.VERSION_TAG }}.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
draft: true draft: true
prerelease: false prerelease: false
token: ${{ secrets.MIHON_BOT_TOKEN }} env:
GITHUB_TOKEN: ${{ secrets.PAT }}

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@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
with:
github-token: ${{ github.token }}
issue-inactive-days: '2'
pr-inactive-days: '2'

View File

@@ -1,23 +0,0 @@
name: Update website
on:
release:
types:
- published
- deleted
- edited
jobs:
update_website:
runs-on: 'ubuntu-24.04'
steps:
- name: Update website
run: |
curl --fail-with-body -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.MIHON_BOT_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/mihonapp/website/dispatches \
-d '{"event_type":"app_release"}'

28
.gitignore vendored
View File

@@ -1,16 +1,18 @@
# Build files
.gradle .gradle
.kotlin .kotlin
build /local.properties
/.idea/workspace.xml
# IDE files
*.iml
.idea/*
!.idea/icon.svg
/captures
# Configuration files
local.properties
# macOS specific files
.DS_Store .DS_Store
.idea/*
!.idea/icon.png
*iml
*.iml
# Built files
*/build
/build
*.apk
app/**/output.json
# Unnecessary file
*.swp

BIN
.idea/icon.png generated Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

6
.idea/icon.svg generated
View File

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 432 432">
<circle cx="216" cy="216" r="216" fill="#2e3943"/>
<path fill="#f2faff" d="M398.073 216c0 97.433-81.517 176.419-182.073 176.419-100.556 0-182.073-78.986-182.073-176.419S115.444 39.581 216 39.581c100.556 0 182.073 78.986 182.073 176.419Z"/>
<path fill="#7ebbed" fill-rule="evenodd" d="M216 359.34c81.702 0 147.934-64.175 147.934-143.34S297.702 72.66 216 72.66 68.065 136.835 68.065 216 134.298 359.34 216 359.34zm0 33.079c100.556 0 182.073-78.986 182.073-176.419S316.556 39.581 216 39.581C115.444 39.581 33.927 118.567 33.927 216S115.444 392.419 216 392.419z" clip-rule="evenodd"/>
<path fill="#031019" d="m155.273 168.033-1.227-28.215c3.68.7 8.063.875 18.052.875 12.092 0 28.041-.7 36.279-1.752 3.504-.35 4.907-.876 7.185-2.103l18.928 16.124c-1.753 2.453-2.279 3.505-4.207 8.412-1.576 3.856-8.762 26.113-11.567 35.577 12.97 2.63 20.155 4.557 29.97 8.588 1.226-8.588 1.401-13.144 1.401-28.742 0-4.03-.175-6.31-.7-9.99l30.495 1.051c-.877 4.207-1.052 5.959-1.227 12.794-.701 16.475-1.403 24.361-3.154 36.279 12.092 6.134 12.092 6.134 18.226 9.464 3.154 1.752 3.855 2.102 5.959 2.804l-10.165 32.773c-4.908-4.381-11.743-9.113-21.732-14.721-8.763 20.855-23.31 36.103-45.392 48.195-7.36-9.814-12.97-15.772-21.907-22.783 12.969-6.134 18.928-9.99 25.763-16.475 6.66-6.484 11.04-12.793 15.247-22.258-11.217-5.082-18.403-7.36-30.846-9.989-7.185 21.382-12.969 35.052-18.051 43.29-6.835 11.04-16.124 16.824-26.815 16.824-8.237 0-16.65-3.68-22.784-9.99-7.01-7.185-10.69-17.175-10.69-28.742 0-17.176 8.238-32.072 22.609-41.361 9.288-5.959 19.103-8.588 34.7-9.465 3.155-10.34 5.785-19.278 8.238-29.267-7.712.701-17.35 1.227-29.093 1.752-6.309.175-8.412.35-13.495 1.051zm26.64 53.279c-8.238 1.402-13.145 4.031-17.527 9.64-3.33 3.855-4.907 8.412-4.907 13.32 0 5.432 2.63 9.464 5.959 9.464 4.03 0 8.588-9.114 16.475-32.424z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -2,334 +2,133 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is a modified version of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- `Added` - for new features. and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `Changed ` - for changes in existing functionality.
- `Improved` - for enhancement or optimization in existing functionality.
- `Removed` - for now removed features.
- `Fixed` - for any bug fixes.
- `Other` - for technical stuff.
## [Unreleased] ## [Unreleased]
## [v0.18.0] - 2025-03-20
### Added
- Add option to always decode long strip images with SSIV ([@AntsyLich](https://github.com/AntsyLich)) ([`c5655e8`](https://github.com/mihonapp/mihon/commit/c5655e8803bc32d0931657f0b7bc6afeab70feaf))
- Change option label ([@AntsyLich](https://github.com/AntsyLich)) ([#1835](https://github.com/mihonapp/mihon/pull/1835))
- Added option to enable incognito per extension ([@sdaqo](https://github.com/sdaqo), [@AntsyLich](https://github.com/AntsyLich)) ([#157](https://github.com/mihonapp/mihon/pull/157))
- Add button to favorite manga from history screen ([@Animeboynz](https://github.com/Animeboynz)) ([#1733](https://github.com/mihonapp/mihon/pull/1733))
- Add Monochrome theme (made with e-ink displays in mind) ([@MajorTanya](https://github.com/MajorTanya)) ([#1752](https://github.com/mihonapp/mihon/pull/1752))
- Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736))
- Add private tracking support for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#1774](https://github.com/mihonapp/mihon/pull/1774))
- Add option to export minimal library information to a CSV file ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1161](https://github.com/mihonapp/mihon/pull/1161))
- Add back support for drag-and-drop category reordering ([@cuong-tran](https://github.com/cuong-tran)) ([#1427](https://github.com/mihonapp/mihon/pull/1427))
- Add option to mark duplicate read chapters as read after library update or while reading ([@AntsyLich](https://github.com/AntsyLich)) ([#1785](https://github.com/mihonapp/mihon/pull/1785), [#1791](https://github.com/mihonapp/mihon/pull/1791), [#1870](https://github.com/mihonapp/mihon/pull/1870))
- Display staff information on Anilist tracker search results ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1810](https://github.com/mihonapp/mihon/pull/1810))
- Add `id:` prefix search to library to search by internal DB ID ([@MajorTanya](https://github.com/MajorTanya)) ([#1856](https://github.com/mihonapp/mihon/pull/1856))
- Add back option to disable unread chapter badge in library ([@AntsyLich](https://github.com/AntsyLich)) ([#1871](https://github.com/mihonapp/mihon/pull/1871))
### Changed
- Sliders UI ([@AntsyLich](https://github.com/AntsyLich)) ([#1840](https://github.com/mihonapp/mihon/pull/1840))
- Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603))
- Ignore hidden files/folders for Local Source chapter list ([@BrutuZ](https://github.com/BrutuZ)) ([#1763](https://github.com/mihonapp/mihon/pull/1763))
- Migrate to newer Bangumi API ([@MajorTanya](https://github.com/MajorTanya)) ([#1748](https://github.com/mihonapp/mihon/pull/1748))
- Now showing manga starting dates in search
- Reduced request load by 2-4x in certain situations
- Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([#1833](https://github.com/mihonapp/mihon/pull/1833))
- Changed the label of chapter swipe settings and renamed the group to "Behavior" ([@AntsyLich](https://github.com/AntsyLich)) ([#1870](https://github.com/mihonapp/mihon/pull/1870))
### Fixed
- Fix MAL `main_picture` nullability breaking search if a result doesn't have a cover set ([@MajorTanya](https://github.com/MajorTanya)) ([#1618](https://github.com/mihonapp/mihon/pull/1618))
- Fix Bangumi and MAL tracking 401 errors due to Mihon sending expired credentials ([@MajorTanya](https://github.com/MajorTanya)) ([#1681](https://github.com/mihonapp/mihon/pull/1681), [#1682](https://github.com/mihonapp/mihon/pull/1682))
- Fix certain Infinix, Xiaomi devices being unable to use any "Open link in browser" actions, including tracker setup ([@MajorTanya](https://github.com/MajorTanya)) ([#1684](https://github.com/mihonapp/mihon/pull/1684)) ([#1776](https://github.com/mihonapp/mihon/pull/1776))
- Fix App's preferences referencing deleted categories ([@cuong-tran](https://github.com/cuong-tran)) ([#1734](https://github.com/mihonapp/mihon/pull/1734))
- Fix backup/restore of category related preferences ([@cuong-tran](https://github.com/cuong-tran)) ([#1726](https://github.com/mihonapp/mihon/pull/1726))
- Fix WebView sending app's package name in `X-Requested-With` header, which led to sources blocking access ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1812](https://github.com/mihonapp/mihon/pull/1812))
- Fix an issue where tracker reading progress is changed to a lower value ([@Animeboynz](https://github.com/Animeboynz)) ([#1795](https://github.com/mihonapp/mihon/pull/1795))
- Attempt to fix crash when migrating or removing entries from library ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1828](https://github.com/mihonapp/mihon/pull/1828))
### Removed
- Remove alphabetical category sort option ([@AntsyLich](https://github.com/AntsyLich)) ([#1781](https://github.com/mihonapp/mihon/pull/1781))
### Other
- Add zoned "Current time" to debug info and include year & timezone in logcat output ([@MajorTanya](https://github.com/MajorTanya)) ([#1672](https://github.com/mihonapp/mihon/pull/1672))
- Add application package ID to debug info ([@MajorTanya](https://github.com/MajorTanya)) ([#1847](https://github.com/mihonapp/mihon/pull/1847))
## [v0.17.1] - 2024-12-06
### Changed
- Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`76dcf90`](https://github.com/mihonapp/mihon/commit/76dcf903403d565056f44c66d965c1ea8affffc3))
### Improved
- Bangumi search now shows the score and summary of a search result ([@MajorTanya](https://github.com/MajorTanya)) ([#1396](https://github.com/mihonapp/mihon/pull/1396))
- Extension repo URLs are now auto-formatted ([@AntsyLich](https://github.com/AntsyLich), [@MajorTanya](https://github.com/MajorTanya)) ([`22d8aad`](https://github.com/mihonapp/mihon/commit/22d8aad598bea8f00f2831779e45a6645392ca0f))
### Fixed
- Fix "currentTab was used multiple times" ([@AntsyLich](https://github.com/AntsyLich)) ([`371c143`](https://github.com/mihonapp/mihon/commit/371c1432e218f6dcf129f05405dceb2cd351c647))
- Fix a rare crash when invoking "Mark previous as read" action ([@AntsyLich](https://github.com/AntsyLich)) ([`f508d10`](https://github.com/mihonapp/mihon/commit/f508d10ad13560d7316df8642bc93fe66c05b9a8))
- Fix long strip images not loading in some old devices ([@AntsyLich](https://github.com/AntsyLich)) ([`06efc3b`](https://github.com/mihonapp/mihon/commit/06efc3b25c5af51f42448af27a269ee459d9093d))
- Switch to hardware bitmap in reader only if device can handle it ([@AntsyLich](https://github.com/AntsyLich)) ([`e6d96bd`](https://github.com/mihonapp/mihon/commit/e6d96bd348ea5d18a005d6465222ad5f5123103e))
- Add option to lower the threshold for hardware bitmaps ([@AntsyLich](https://github.com/AntsyLich)) ([`dcddac5`](https://github.com/mihonapp/mihon/commit/dcddac5daaff3ec89c8507c35dc13d345ffdb6d7))
- Improve hardware bitmap threshold option ([@AntsyLich](https://github.com/AntsyLich)) ([`d6dfd24`](https://github.com/mihonapp/mihon/commit/d6dfd24548eaa05a8c3e478068fe2e08f2ee4473))
- Always use software bitmap on certain devices ([@MajorTanya](https://github.com/MajorTanya)) ([#1543](https://github.com/mihonapp/mihon/pull/1543))
- Fix crash after removing last category while it's active in library ([@cuong-tran](https://github.com/cuong-tran)) ([#1450](https://github.com/mihonapp/mihon/pull/1450))
- Fix reader transition color scheme in auto background mode ([@cuong-tran](https://github.com/cuong-tran)) ([#1487](https://github.com/mihonapp/mihon/pull/1487))
- Fix app update error notification disappearing ([@cuong-tran](https://github.com/cuong-tran)) ([#1476](https://github.com/mihonapp/mihon/pull/1476))
- Fix browser not opening in some cases in Honor devices ([@AntsyLich](https://github.com/AntsyLich), [@MajorTanya](https://github.com/MajorTanya)) ([#1520](https://github.com/mihonapp/mihon/pull/1520))
## [v0.17.0] - 2024-10-26
### Added ### Added
- Option to disable reader zoom out ([@Splintorien](https://github.com/Splintorien)) ([#302](https://github.com/mihonapp/mihon/pull/302)) - Option to disable reader zoom out ([@Splintorien](https://github.com/Splintorien)) ([#302](https://github.com/mihonapp/mihon/pull/302))
- Source name and tracker urls to app generated `ComicInfo.xml` file ([@Shamicen](https://github.com/Shamicen)) ([#459](https://github.com/mihonapp/mihon/pull/459)) - Source name and tracker urls to app generated `ComicInfo.xml` file ([@Shamicen](https://github.com/Shamicen)) ([#459](https://github.com/mihonapp/mihon/pull/459))
- Option to migrate in Duplicate entry dialog ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492)) - Option to migrate in Duplicate entry dialog ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492))
- Upcoming screen to visualize expected update dates ([@sirlag](https://github.com/sirlag)) ([#420](https://github.com/mihonapp/mihon/pull/420)) - Upcoming screen to visualize expected update dates ([@sirlag](https://github.com/sirlag)) ([#420](https://github.com/mihonapp/mihon/pull/420))
- Only show upcoming updates in the future ([@sirlag](https://github.com/sirlag)) ([#606](https://github.com/mihonapp/mihon/pull/606))
- Add Quantity Badge to Upcoming Screen ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1250](https://github.com/mihonapp/mihon/pull/1250))
- Crash screen error message to the top of the crash log generated from that screen ([@FooIbar](https://github.com/FooIbar)) ([#742](https://github.com/mihonapp/mihon/pull/742)) - Crash screen error message to the top of the crash log generated from that screen ([@FooIbar](https://github.com/FooIbar)) ([#742](https://github.com/mihonapp/mihon/pull/742))
- Support for 7Zip and RAR5 archives ([@FooIbar](https://github.com/FooIbar)) ([#949](https://github.com/mihonapp/mihon/pull/949)) - Support for 7Zip and RAR5 archives ([@FooIbar](https://github.com/FooIbar), [@null2264](https://github.com/null2264)) ([#949](https://github.com/mihonapp/mihon/pull/949), [#967](https://github.com/mihonapp/mihon/pull/967))
- Extra configuration options to e-ink page flashes ([@sirlag](https://github.com/sirlag)) ([#625](https://github.com/mihonapp/mihon/pull/625)) - Extra configuration options to e-ink page flashes ([@sirlag](https://github.com/sirlag)) ([#625](https://github.com/mihonapp/mihon/pull/625))
- 8-bit+ AVIF image support ([@WerctFourth](https://github.com/WerctFourth)) ([#971](https://github.com/mihonapp/mihon/pull/971)) - 8-bit+ AVIF image support ([@WerctFourth](https://github.com/WerctFourth)) ([#971](https://github.com/mihonapp/mihon/pull/971))
- Smart update dialog message when no predicted released date exists ([@Animeboynz](https://github.com/Animeboynz)) ([#977](https://github.com/mihonapp/mihon/pull/977)) - Smart update dialog message when no predicted released date exists ([@Animeboynz](https://github.com/Animeboynz)) ([#977](https://github.com/mihonapp/mihon/pull/977))
- Save global search "Has result" choice ([@AntsyLich](https://github.com/AntsyLich)) ([`5a61ca5`](https://github.com/mihonapp/mihon/commit/5a61ca5535fe0d9e8e7bcb9e665ba2f9cb0cf649))
- Option to copy reader panel to clipboard ([@Animeboynz](https://github.com/Animeboynz)) ([#1003](https://github.com/mihonapp/mihon/pull/1003)) - Option to copy reader panel to clipboard ([@Animeboynz](https://github.com/Animeboynz)) ([#1003](https://github.com/mihonapp/mihon/pull/1003))
- Copy Tracker URL option to tracker sheet ([@mm12](https://github.com/mm12)) ([#1101](https://github.com/mihonapp/mihon/pull/1101)) - Copy Tracker URL option to tracker sheet ([@mm12](https://github.com/mm12)) ([#1101](https://github.com/mihonapp/mihon/pull/1101))
- A button to exclude all scanlators in exclude scanlators dialog ([@AntsyLich](https://github.com/AntsyLich)) ([`84b2164`](https://github.com/mihonapp/mihon/commit/84b2164787a795f3fd757c325cbfb6ef660ac3a3)) - A button to exclude all scanlators in exclude scanlators dialog ([@AntsyLich](https://github.com/AntsyLich)) ([`84b2164`](https://github.com/mihonapp/mihon/commit/84b2164787a795f3fd757c325cbfb6ef660ac3a3))
- Open in browser option to reader menu ([@mm12](https://github.com/mm12)) ([#1110](https://github.com/mihonapp/mihon/pull/1110)) - Open in browser option to reader menu ([@mm12](https://github.com/mm12)) ([#1110](https://github.com/mihonapp/mihon/pull/1110))
- Reorder reader menu overflow items ([@AntsyLich](https://github.com/AntsyLich)) ([`788235f`](https://github.com/mihonapp/mihon/commit/788235feeca241228eac0561339dd07b5ea0b77d))
- Option to skip downloading duplicate read chapters ([@shabnix](https://github.com/shabnix)) ([#1125](https://github.com/mihonapp/mihon/pull/1125)) - Option to skip downloading duplicate read chapters ([@shabnix](https://github.com/shabnix)) ([#1125](https://github.com/mihonapp/mihon/pull/1125))
- Add confirmation dialog when adding repo via URI ([@Animeboynz](https://github.com/Animeboynz)) ([#1158](https://github.com/mihonapp/mihon/pull/1158))
- Add "show entry" action to download notifications ([@mm12](https://github.com/mm12), [@AntsyLich](https://github.com/AntsyLich)) ([#1159](https://github.com/mihonapp/mihon/pull/1159))
- Option to update trackers when chapter marked as read ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1177](https://github.com/mihonapp/mihon/pull/1177), [#1365](https://github.com/mihonapp/mihon/pull/1365), [#1374](https://github.com/mihonapp/mihon/pull/1374))
- Toast to restart app when User-Agent is changed ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1204](https://github.com/mihonapp/mihon/pull/1204))
- Added more profile compilation status (p) ([`c8bb78d`](https://github.com/mihonapp/mihon/commit/c8bb78d91afc2824baaca999f0095559c49d1306))
- Add option to opt out of Analytics and Crashlytics ([@Animeboynz](https://github.com/Animeboynz)) ([#1237](https://github.com/mihonapp/mihon/pull/1237))
- Rework Firebase setup ([@AntsyLich](https://github.com/AntsyLich)) ([`15e3f28`](https://github.com/mihonapp/mihon/commit/15e3f28aa36bec3c31f212c572ab57ce960cc862))
- Added random library sort ([@jackhamilton](https://github.com/jackhamilton)) ([#1317](https://github.com/mihonapp/mihon/pull/1317))
- Make sure random library sort is at the bottom ([@AntsyLich](https://github.com/AntsyLich)) ([`2e2c8d3`](https://github.com/mihonapp/mihon/commit/2e2c8d36c1e23bf274c7c19f1242e14b0c7afbc1))
- Confirmation dialog when removing privately installed extensions ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1320](https://github.com/mihonapp/mihon/pull/1320))
- Option to backup non-library read entries ([@Animeboynz](https://github.com/Animeboynz), [@jobobby04](https://github.com/jobobby04), [@AntsyLich](https://github.com/AntsyLich)) ([#1324](https://github.com/mihonapp/mihon/pull/1324))
### Changed ### Changed
- Read archive files from memory instead of temporarily extracting to internal storage ([@FooIbar](https://github.com/FooIbar)) ([#326](https://github.com/mihonapp/mihon/pull/326)) - Read archive files from memory instead of extracting files to internal storage ([@FooIbar](https://github.com/FooIbar)) ([#326](https://github.com/mihonapp/mihon/pull/326))
- Fix dual page split ([@FooIbar](https://github.com/FooIbar)) ([#485](https://github.com/mihonapp/mihon/pull/485)) - Try to get resource from Extension before checking in the app ([@beer-psi](https://github.com/beer-psi)) ([#433](https://github.com/mihonapp/mihon/pull/433))
- Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`8160b47`](https://github.com/mihonapp/mihon/commit/8160b47ff5fbbd9b32caeb462b5be881fabd3449)) - Default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`8160b47`](https://github.com/mihonapp/mihon/commit/8160b47ff5fbbd9b32caeb462b5be881fabd3449))
- Wait for sources to be initialized before performing source related tasks ([@jobobby04](https://github.com/jobobby04)) ([`a08e03f`](https://github.com/mihonapp/mihon/commit/a08e03f5cbf3f4e6be1de35f97ef8ebb26a1210e)) - Wait for sources to be initialized before performing source related tasks ([@jobobby04](https://github.com/jobobby04)) ([`a08e03f`](https://github.com/mihonapp/mihon/commit/a08e03f5cbf3f4e6be1de35f97ef8ebb26a1210e))
- Duplicate entry dialog UI ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492)) - Duplicate entry dialog UI ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492))
- Extension trust system - Extension trust system ([@AntsyLich](https://github.com/AntsyLich), [@Animeboynz](https://github.com/Animeboynz) ([#570](https://github.com/mihonapp/mihon/pull/570), [#1057](https://github.com/mihonapp/mihon/pull/1057))
- Store extension repo details from `repo.json` in database ([@sirlag](https://github.com/sirlag)) ([#506](https://github.com/mihonapp/mihon/pull/506))
- Fix extension repo migration not triggering ([@AntsyLich](https://github.com/AntsyLich)) ([`9672ea8`](https://github.com/mihonapp/mihon/commit/9672ea8b1b06f464800e310c96e060ead182f7ca))
- Refactor the ExtensionRepoService to use DTOs ([@MajorTanya](https://github.com/MajorTanya)) ([#573](https://github.com/mihonapp/mihon/pull/573))
- Fix extension repo name is used to construct URL instead of baseUrl ([@MajorTanya](https://github.com/MajorTanya)) ([#572](https://github.com/mihonapp/mihon/pull/572))
- Fix crash with `TypeReference` issue when creating extension repo ([@AntsyLich](https://github.com/AntsyLich)) ([#574](https://github.com/mihonapp/mihon/pull/574), [`e020ae5`](https://github.com/mihonapp/mihon/commit/e020ae5ed558e80742ef0ad8bfa0f69af0959d5a))
- Fix mishap in [`e020ae5`](https://github.com/mihonapp/mihon/commit/e020ae5ed558e80742ef0ad8bfa0f69af0959d5a) ([@AntsyLich](https://github.com/AntsyLich)) ([`6965e59`](https://github.com/mihonapp/mihon/commit/6965e59a643c67a2bf81b3c69ec70268e5da5797))
- Backup and Restore ([@Animeboynz](https://github.com/Animeboynz)) ([#1057](https://github.com/mihonapp/mihon/pull/1057))
- Trust extension by repo ([@AntsyLich](https://github.com/AntsyLich)) ([#570](https://github.com/mihonapp/mihon/pull/570))
- From M2 ripple to M3 ([@FooIbar](https://github.com/FooIbar)) ([#675](https://github.com/mihonapp/mihon/pull/675))
- Increased continue reading button size ([@AntsyLich](https://github.com/AntsyLich), [@Animeboynz](https://github.com/Animeboynz)) ([`e17f70f`](https://github.com/mihonapp/mihon/commit/e17f70f7226ea031fc1f962c9dfea3e404ba53ad))
- Global search "Has result" choice is now sticky ([@AntsyLich](https://github.com/AntsyLich)) ([`5a61ca5`](https://github.com/mihonapp/mihon/commit/5a61ca5535fe0d9e8e7bcb9e665ba2f9cb0cf649))
- Make category backup/restore not dependant on library backup ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) - Make category backup/restore not dependant on library backup ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596))
- Rename backup restore error log file ([@AntsyLich](https://github.com/AntsyLich)) ([`2858ef8`](https://github.com/mihonapp/mihon/commit/2858ef835fec8d7278b1d0cad1b5664104d1e4b0))
- Keyboard type in add extension repo dialog ([@xbjfk](https://github.com/xbjfk)) ([#764](https://github.com/mihonapp/mihon/pull/764))
- Adjust collapse/open animation on manga description ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`1c16fc7`](https://github.com/mihonapp/mihon/commit/1c16fc79c2ac4c4be30308fed84ffb371dab5902))
- Kitsu domain to `kitsu.app` ([@MajorTanya](https://github.com/MajorTanya)) ([#1106](https://github.com/mihonapp/mihon/pull/1106)) - Kitsu domain to `kitsu.app` ([@MajorTanya](https://github.com/MajorTanya)) ([#1106](https://github.com/mihonapp/mihon/pull/1106))
- Respect privacy settings in extension update notification ([@Animeboynz](https://github.com/Animeboynz)) ([#1156](https://github.com/mihonapp/mihon/pull/1156)) - Respect privacy settings in extension update notification ([@Animeboynz](https://github.com/Animeboynz)) ([#1156](https://github.com/mihonapp/mihon/pull/1156))
- Hide keyboard when a Tracker SearchResultItem is clicked ([@Animeboynz](https://github.com/Animeboynz)) ([#1168](https://github.com/mihonapp/mihon/pull/1168))
- Enable 'Split Tall Images' by default ([@Smol-Ame](https://github.com/Smol-Ame)) ([#1185](https://github.com/mihonapp/mihon/pull/1185))
- Ignore "intent://" urls on webview ([@bapeey](https://github.com/bapeey)) ([#1193](https://github.com/mihonapp/mihon/pull/1193))
- Make reader chapter navigator slightly wider on small screens (p) ([#1202](https://github.com/mihonapp/mihon/pull/1202))
- Re-enable fetching chapters list for entries with licenced status ([@Animeboynz](https://github.com/Animeboynz)) ([#1230](https://github.com/mihonapp/mihon/pull/1230))
- Change casing for Extention Repos String ([@Animeboynz](https://github.com/Animeboynz)) ([#1248](https://github.com/mihonapp/mihon/pull/1248))
- Retain remote last chapter read if it's higher than the local one for EnhancedTracker ([@brewkunz](https://github.com/brewkunz)) ([#1301](https://github.com/mihonapp/mihon/pull/1301))
- Adjust expandable fab animation (p) ([`eb6092b`](https://github.com/mihonapp/mihon/commit/eb6092bd0cfa09694985a8bafdd8bbf2815190a1))
- "Invalidate downloads index" to "Reindex downloads" ([@AntsyLich](https://github.com/AntsyLich)) ([`d2afbfe`](https://github.com/mihonapp/mihon/commit/d2afbfe4ede283076aae40633c79c3f90b4390e7))
### Improved ### Improvement
- Reader performance - Long strip reader performance ([@FooIbar](https://github.com/FooIbar), [@wwww-wwww](https://github.com/wwww-wwww)) ([#687](https://github.com/mihonapp/mihon/pull/687))
- Avoid unnecessary copying when processing reader image ([@FooIbar](https://github.com/FooIbar)) ([#691](https://github.com/mihonapp/mihon/pull/691))
- Significantly improve performance when loading extremely long images in long strip mode ([@FooIbar](https://github.com/FooIbar)) ([#692](https://github.com/mihonapp/mihon/pull/692))
- Use `Bitmap.Config.HARDWARE` if possible to improve image loading speed ([@wwww-wwww](https://github.com/wwww-wwww)) ([#687](https://github.com/mihonapp/mihon/pull/687))
- Improve preloading in long strip mode ([@FooIbar](https://github.com/FooIbar)) ([#1076](https://github.com/mihonapp/mihon/pull/1076))
- Performance when looking up specific files ([@raxod502](https://github.com/raxod502)) ([#728](https://github.com/mihonapp/mihon/pull/728)) - Performance when looking up specific files ([@raxod502](https://github.com/raxod502)) ([#728](https://github.com/mihonapp/mihon/pull/728))
- Chapter number parsing ([@Naputt1](https://github.com/Naputt1)) ([`6a80305`](https://github.com/mihonapp/mihon/commit/6a80305d6c572da6c08c0c69f5c25ff26ecf7383)) - Chapter number parsing ([@Naputt1](https://github.com/Naputt1)) ([`6a80305`](https://github.com/mihonapp/mihon/commit/6a80305d6c572da6c08c0c69f5c25ff26ecf7383))
- Error message on restoring if backup decoding fails ([@vetleledaal](https://github.com/vetleledaal)) ([#1056](https://github.com/mihonapp/mihon/pull/1056)) - Error message on restoring if backup decoding fails ([@vetleledaal](https://github.com/vetleledaal)) ([#1056](https://github.com/mihonapp/mihon/pull/1056))
### Removed
- Legacy download folder names no longer supported ([@AntsyLich](https://github.com/AntsyLich)) ([`e55e5f6`](https://github.com/mihonapp/mihon/commit/e55e5f6f64f872475d370d6ce0c186e2601776e4))
- Remove legacy broken source and history backup ([@AntsyLich](https://github.com/AntsyLich)) ([`518abf0`](https://github.com/mihonapp/mihon/commit/518abf032ccb9bb45d197927be2a5faca4167d29))
- Remove more unnecessary permissions from Firebase dependency ([@AntsyLich](https://github.com/AntsyLich)) ([`02af9b1`](https://github.com/mihonapp/mihon/commit/02af9b1acf9f590d29560bc3fc90d206e8e6e1af))
- Fix mishap in `02af9b1` ([@AntsyLich](https://github.com/AntsyLich)) ([`f22767d`](https://github.com/mihonapp/mihon/commit/f22767d863a0fa001f93f24092cd5ade87350502))
### Fixed ### Fixed
- Extracting `ComicInfo.xml` from local source archives ([@FooIbar](https://github.com/FooIbar)) ([#325](https://github.com/mihonapp/mihon/pull/325)) - Creating `ComicInfo.xml` file for local source ([@FooIbar](https://github.com/FooIbar)) ([#325](https://github.com/mihonapp/mihon/pull/325))
- Chapter download indicator ([@ivaniskandar](https://github.com/ivaniskandar)) ([`d8b9a9f`](https://github.com/mihonapp/mihon/commit/d8b9a9f593911569ff2bceb49b4f020978d0d2e1)) - Chapter download indicator ([@ivaniskandar](https://github.com/ivaniskandar)) ([`d8b9a9f`](https://github.com/mihonapp/mihon/commit/d8b9a9f593911569ff2bceb49b4f020978d0d2e1))
- Issues with shizuku in a multi user setup ([@Redjard](https://github.com/Redjard)) ([#494](https://github.com/mihonapp/mihon/pull/494)) - Issues with shizuku in a multi user setup ([@Redjard](https://github.com/Redjard)) ([#494](https://github.com/mihonapp/mihon/pull/494))
- Fix reader page image not being decoded until it's visible ([@FooIbar](https://github.com/FooIbar)) ([#563](https://github.com/mihonapp/mihon/pull/563)) - Occasional black bar when scrolling in long strip reader ([@FooIbar](https://github.com/FooIbar)) ([#563](https://github.com/mihonapp/mihon/pull/563))
- Reader chapter progress slider visuals ([@FooIbar](https://github.com/FooIbar)) ([#674](https://github.com/mihonapp/mihon/pull/674))
- Extension being marked as not installed instead of untrusted after updating with private installer ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) - Extension being marked as not installed instead of untrusted after updating with private installer ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42))
- Extension update counter not updating due to extension being marked as untrusted ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) - Extension update counter not updating due to extension being marked as untrusted ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42))
- `Key "extension-XXX-YYY" was already used` crash ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) - `Key "extension-XXX-YYY" was already used` crash ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42))
- Navigation layout tap zones shifting after zooming out in webtoon readers ([@FooIbar](https://github.com/FooIbar)) ([#767](https://github.com/mihonapp/mihon/pull/767)) - Navigation layout tap zones shifting after zooming out in webtoon readers ([@FooIbar](https://github.com/FooIbar)) ([#767](https://github.com/mihonapp/mihon/pull/767))
- Some extension not loading due to missing classes ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#783](https://github.com/mihonapp/mihon/pull/783)) - Some extension not loading due to missing classes ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#783](https://github.com/mihonapp/mihon/pull/783))
- Theme colors in accordance to upstream changes ([@CrepeTF](https://github.com/CrepeTF), [@AntsyLich](https://github.com/AntsyLich)) ([#766](https://github.com/mihonapp/mihon/pull/766), [#963](https://github.com/mihonapp/mihon/pull/963), [#976](https://github.com/mihonapp/mihon/pull/976), [9a34ace](https://github.com/mihonapp/mihon/commit/9a34ace09c66274e6c2b3f9446058a0fa99d4bd0)) - Theme colors in accordance to upstream changes ([@CrepeTF](https://github.com/CrepeTF), [@AntsyLich](https://github.com/AntsyLich)) ([#766](https://github.com/mihonapp/mihon/pull/766), [#963](https://github.com/mihonapp/mihon/pull/963), [#976](https://github.com/mihonapp/mihon/pull/976))
- Crash when requesting folder access on non-conforming devices ([@mainrs](https://github.com/mainrs)) ([#726](https://github.com/mihonapp/mihon/pull/726)) - Crash when requesting folder access on non-conforming devices ([@mainrs](https://github.com/mainrs)) ([#726](https://github.com/mihonapp/mihon/pull/726))
- Fix unexpected skips in strong skipping mode ([@FooIbar](https://github.com/FooIbar)) ([#940](https://github.com/mihonapp/mihon/pull/940))
- Bugged color for Date/Scanlator in chapter list for read chapters ([@ivaniskandar](https://github.com/ivaniskandar)) ([`15d9992`](https://github.com/mihonapp/mihon/commit/15d999229fcce865001d5fa77d0163e6e80e38db)) - Bugged color for Date/Scanlator in chapter list for read chapters ([@ivaniskandar](https://github.com/ivaniskandar)) ([`15d9992`](https://github.com/mihonapp/mihon/commit/15d999229fcce865001d5fa77d0163e6e80e38db))
- Categories having same `order` after restoring backup ([@Cologler](https://github.com/Cologler)) ([`119bcbf`](https://github.com/mihonapp/mihon/commit/119bcbf8ed2415664922ea77fadf0da1165d1732)) - Categories having same `order` after restoring backup ([@Cologler](https://github.com/Cologler)) ([`119bcbf`](https://github.com/mihonapp/mihon/commit/119bcbf8ed2415664922ea77fadf0da1165d1732))
- Filter by "Tracking" temporarily stuck after signing out of tracker ([@AntsyLich](https://github.com/AntsyLich)) ([#987](https://github.com/mihonapp/mihon/pull/987)) - Filter by "Tracking" temporarily stuck after signing out of tracker ([@AntsyLich](https://github.com/AntsyLich)) ([#987](https://github.com/mihonapp/mihon/pull/987))
- Fix login prompts despite being logged in to trackers in Manga screen ([@AntsyLich](https://github.com/AntsyLich)) ([`cbcd8bd`](https://github.com/mihonapp/mihon/commit/cbcd8bd6682023f728568f2b44da26124618aed7))
- JXL image downloading and loading ([@FooIbar](https://github.com/FooIbar)) ([#993](https://github.com/mihonapp/mihon/pull/993)) - JXL image downloading and loading ([@FooIbar](https://github.com/FooIbar)) ([#993](https://github.com/mihonapp/mihon/pull/993))
- Crash when using `%` in category name ([@Animeboynz](https://github.com/Animeboynz), [@FooIbar](https://github.com/FooIbar)) ([#1030](https://github.com/mihonapp/mihon/pull/1030)) - Crash when using `%` in category name ([@Animeboynz](https://github.com/Animeboynz), [@FooIbar](https://github.com/FooIbar)) ([#1030](https://github.com/mihonapp/mihon/pull/1030))
- Fix item disappearing when fast scrolling ([@cuong-tran](https://github.com/cuong-tran)) ([#1035](https://github.com/mihonapp/mihon/pull/1035))
- Library is backed up while being disabled ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) - Library is backed up while being disabled ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596))
- Crash on list with only sticky header ([@cuong-tran](https://github.com/cuong-tran)) ([#1083](https://github.com/mihonapp/mihon/pull/1083)) - Crash on list with 0 item but only sticky header ([@cuong-tran](https://github.com/cuong-tran)) ([#1083](https://github.com/mihonapp/mihon/pull/1083))
- Crash when trying to clear cookies of some source ([@FooIbar](https://github.com/FooIbar)) ([#1084](https://github.com/mihonapp/mihon/pull/1084)) - Crash when trying to clear cookies of some source ([@FooIbar](https://github.com/FooIbar)) ([#1084](https://github.com/mihonapp/mihon/pull/1084))
- MAL search results not showing start dates ([@MajorTanya](https://github.com/MajorTanya)) ([#1098](https://github.com/mihonapp/mihon/pull/1098)) - MAL search results not showing start dates ([@MajorTanya](https://github.com/MajorTanya)) ([#1098](https://github.com/mihonapp/mihon/pull/1098))
- Android SDK 35 API collision ([@AntsyLich](https://github.com/AntsyLich)) ([`fdb9617`](https://github.com/mihonapp/mihon/commit/fdb96179c6373eb0a8e7d6daea671a315d5ce5f0)) - Android SDK 35 API collision ([@AntsyLich](https://github.com/AntsyLich)) ([`fdb9617`](https://github.com/mihonapp/mihon/commit/fdb96179c6373eb0a8e7d6daea671a315d5ce5f0))
- Manga next update calculation when considering custom fetch interval ([@cuong-tran](https://github.com/cuong-tran)) ([#1206](https://github.com/mihonapp/mihon/pull/1206))
- WheelPicker Manual Input ([@Animeboynz](https://github.com/Animeboynz)) ([#1209](https://github.com/mihonapp/mihon/pull/1209))
- EnhancedTracker not auto binding when adding manga to library ([@brewkunz](https://github.com/brewkunz)) ([#1298](https://github.com/mihonapp/mihon/pull/1298))
- Step count in settings slider ([@abdurisaq](https://github.com/abdurisaq)) ([#1356](https://github.com/mihonapp/mihon/pull/1356))
- Freezing in some screens due to blocking call ([@cuong-tran](https://github.com/cuong-tran)) ([#1364](https://github.com/mihonapp/mihon/pull/1364))
- Crash when removing non-existent tracked entry from tracker ([@cuong-tran](https://github.com/cuong-tran)) ([#1380](https://github.com/mihonapp/mihon/pull/1380))
### Other
- Code cleanup
- Minor refactor of theming when expressions ([@MajorTanya](https://github.com/MajorTanya)) ([#396](https://github.com/mihonapp/mihon/pull/396))
- Inside `WorkerInfoScreen` ([@AntsyLich](https://github.com/AntsyLich)) ([`5aec8f8`](https://github.com/mihonapp/mihon/commit/5aec8f8018236a38106483da08f9cbc28261ac9b))
- Inside `ChapterDownloadIndicator`, `MangaChapterListItem` ([@AntsyLich](https://github.com/AntsyLich)) ([`b7e091d`](https://github.com/mihonapp/mihon/commit/b7e091d5d039e00cababc7daf555280df6cf9c03))
- MangaCoverFetcher ([@ivaniskandar](https://github.com/ivaniskandar)) ([`1365695`](https://github.com/mihonapp/mihon/commit/13656959ae0606736f6ca9eb62699dc23e467c2f))
- Cleanup `LibraryScreenModel` `LibraryMap.applySort` and some more ([@AntsyLich](https://github.com/AntsyLich)) ([`2beb89d`](https://github.com/mihonapp/mihon/commit/2beb89d53163a6d288f8acdebe0f5d26fea8ab3e))
- Address `overridePendingTransition` deprecation ([@MajorTanya](https://github.com/MajorTanya)) ([#410](https://github.com/mihonapp/mihon/pull/410))
- Prioritize extension classes and files over app ([@beer-psi](https://github.com/beer-psi)) ([#433](https://github.com/mihonapp/mihon/pull/433))
- Use compose pager implementation ([@ivaniskandar](https://github.com/ivaniskandar)) ([`84984ef`](https://github.com/mihonapp/mihon/commit/84984ef7e1d7242924120cd2f171cb9dd75bc916))
- Switch to coil3 from coil2 ([@ivaniskandar](https://github.com/ivaniskandar)) ([`f72b6e4`](https://github.com/mihonapp/mihon/commit/f72b6e4d7c1f2f93d705402e4d80c94160bef54d))
- Fix GIF not playing ([@jobobby04](https://github.com/jobobby04)) ([`59bedb3`](https://github.com/mihonapp/mihon/commit/59bedb33ff59ad5db1df2e93567a2266fb63eacc))
- Accommodate db for sync support ([@kaiserbh](https://github.com/kaiserbh)) ([#450](https://github.com/mihonapp/mihon/pull/450))
- Fix webtoon last visible item position calculation ([@FooIbar](https://github.com/FooIbar)) ([#562](https://github.com/mihonapp/mihon/pull/562))
- Migrate from `com.google.accompanist:accompanist-webview` to `io.github.kevinnzou:compose-webview` ([@sirlag](https://github.com/sirlag)) ([#569](https://github.com/mihonapp/mihon/pull/569))
- Rewrite migrations ([@ghostbear](https://github.com/ghostbear)) ([#577](https://github.com/mihonapp/mihon/pull/577))
- Further improve migration ([@ghostbear](https://github.com/ghostbear)) ([#588](https://github.com/mihonapp/mihon/pull/588))
- Fix migrations not running ([@ghostbear](https://github.com/ghostbear)) ([#604](https://github.com/mihonapp/mihon/pull/604))
- Fix MigratorTest after updating to Kotlin 2 ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
- Add MigratorTest to build script ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
- Fix UI freeze after migration ([@AntsyLich](https://github.com/AntsyLich)) ([`3f1d28c`](https://github.com/mihonapp/mihon/commit/3f1d28c3833e6b868152149ed02b3fb8c54eccef))
- Fix some migrations never running ([@MajorTanya](https://github.com/MajorTanya), [@AntsyLich](https://github.com/AntsyLich)) ([#1030](https://github.com/mihonapp/mihon/pull/1030))
- Add ProGuard rule to keep `mihon` namespace classes ([@MajorTanya](https://github.com/MajorTanya)) ([#605](https://github.com/mihonapp/mihon/pull/605))
- Use gradle plugins to share build configuration instead of subprojects ([@AntsyLich](https://github.com/AntsyLich)) ([`e448e40`](https://github.com/mihonapp/mihon/commit/e448e40406e8d9916120a278e42829a6f1b25a7a))
- Remove dependency on compose material 2 components ([@AntsyLich](https://github.com/AntsyLich)) ([`fb94230`](https://github.com/mihonapp/mihon/commit/fb9423028eb017c110cb805f2d0601e5b02e50f9))
- Upload PR build artifacts to GitHub ([@FooIbar](https://github.com/FooIbar)) ([#941](https://github.com/mihonapp/mihon/pull/941))
- Refactor archive support with libarchive ([@FooIbar](https://github.com/FooIbar)) ([#949](https://github.com/mihonapp/mihon/pull/949))
- Add safeguard to prevent ArchiveInputStream from being closed twice ([@null2264](https://github.com/null2264)) ([#967](https://github.com/mihonapp/mihon/pull/967))
- Move archive related code to :core:archive ([@AntsyLich](https://github.com/AntsyLich)) ([`bd7b354`](https://github.com/mihonapp/mihon/commit/bd7b35419861df6d426d6ec0a188391910d0f615))
- Replace detekt with ktlint via spotless ([@AntsyLich](https://github.com/AntsyLich)) ([#1130](https://github.com/mihonapp/mihon/pull/1130), [#1136](https://github.com/mihonapp/mihon/pull/1136), [#1138](https://github.com/mihonapp/mihon/pull/1138))
- Refrain from running spotless on weblate files ([@AntsyLich](https://github.com/AntsyLich)) ([`32d2c2a`](https://github.com/mihonapp/mihon/commit/32d2c2ac1bc224cbda2f09a4023d7d120ea0e954))
- Use feature flags in compose compiler plugin ([@AntsyLich](https://github.com/AntsyLich)) ([`8f9a325`](https://github.com/mihonapp/mihon/commit/8f9a325895bb7b94c2ec92dd969094fc30b3b5e2))
- PagerPageHolder: lazy init loading indicator ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`a45eb5e`](https://github.com/mihonapp/mihon/commit/a45eb5e5288159dbbbbb5f92140ce0dd32a8f3ab))
- Collect MangaScreen state with lifecycle ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`03eb756`](https://github.com/mihonapp/mihon/commit/03eb756ecba0692d88d3a76254afc4c157fa225b))
- Add stable marker to Manga data class ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`03eb756`](https://github.com/mihonapp/mihon/commit/03eb756ecba0692d88d3a76254afc4c157fa225b))
- Use DTOs to parse tracking API responses ([@MajorTanya](https://github.com/MajorTanya)) ([#1103](https://github.com/mihonapp/mihon/pull/1103))
- Fix Kitsu ratingTwenty being typed as String ([@MajorTanya](https://github.com/MajorTanya)) ([#1191](https://github.com/mihonapp/mihon/pull/1191))
- Fix Kitsu `synopsis` nullability ([@MajorTanya](https://github.com/MajorTanya)) ([#1233](https://github.com/mihonapp/mihon/pull/1233))
- Fix AniList `ALSearchItem.status` nullibility ([@Secozzi](https://github.com/Secozzi)) ([#1297](https://github.com/mihonapp/mihon/pull/1297))
- Migrate some classpaths to gradle plugins ([@AntsyLich](https://github.com/AntsyLich)) ([`fc1c804`](https://github.com/mihonapp/mihon/commit/fc1c804bfda1d76c0399bbb6214e75b3def951cc))
- Add crashlytics to standard builds ([@AntsyLich](https://github.com/AntsyLich)) ([`3c611b9`](https://github.com/mihonapp/mihon/commit/3c611b95fb79e5ac972019b76c7b24f46a3087fd))
- Switch to stable compose ([@AntsyLich](https://github.com/AntsyLich)) ([`2baffa6`](https://github.com/mihonapp/mihon/commit/2baffa62cade1abd978d5fd03151b47fc87fd31e))
- Switch from inorichi injekt to kohesive Injekt ([@AntsyLich](https://github.com/AntsyLich)) ([#1205](https://github.com/mihonapp/mihon/pull/1205))
- Use custom injekt register with inorichi patch ([@AntsyLich](https://github.com/AntsyLich)) ([`83fd474`](https://github.com/mihonapp/mihon/commit/83fd4746eda1b99f35292b0c2211e606a421b3eb))
- Use TextFieldState in BasicTextField where applicable (p) ([#1201](https://github.com/mihonapp/mihon/pull/1201))
- Bump NDK version ([@AntsyLich](https://github.com/AntsyLich)) ([#1203](https://github.com/mihonapp/mihon/pull/1203))
- Move firebase permission removal to standard flavor ([@AntsyLich](https://github.com/AntsyLich)) ([`be671b4`](https://github.com/mihonapp/mihon/commit/be671b42cefd70180644e01bb065a18cb7701bf9))
- Adjust distinct checker in WidgetManager and run on default dispatcher (p) ([`9b8ab6a`](https://github.com/mihonapp/mihon/commit/9b8ab6acc25a5f99c9c5eebf9cc250975931c57c))
- Update resources exclusion rules (p) ([`481cfed`](https://github.com/mihonapp/mihon/commit/481cfedf08576cecfbb35616837bd8f627d8f959))
- Bump compile sdk to 35 (p) ([`37419cd`](https://github.com/mihonapp/mihon/commit/37419cdc26c2b5c4f8583fc2ba439b08fab42856))
- ChapterNavigator: dispatch page change only when needed (p) ([`f84d9a0`](https://github.com/mihonapp/mihon/commit/f84d9a08b4af768b1e9920c43cc445c86f5427fc))
- Remove usage of deprecated accompanist SystemUiController ([@AntsyLich](https://github.com/AntsyLich)) ([`2ba3f06`](https://github.com/mihonapp/mihon/commit/2ba3f0612c08c7021fed2f6d96cd538da2f34a13))
- Run PR check when base strings are changed ([@AntsyLich](https://github.com/AntsyLich)) ([`4051f18`](https://github.com/mihonapp/mihon/commit/4051f180a2e36e8a2cde6c55f0bea7952fdc4704))
- Fix PR build check ([@AntsyLich](https://github.com/AntsyLich)) ([`9503082`](https://github.com/mihonapp/mihon/commit/9503082d44b5bd868ee1bfc42741dc978d1d9047))
- Cleanup .gitignore files ([@AntsyLich](https://github.com/AntsyLich)) ([`afa5002`](https://github.com/mihonapp/mihon/commit/afa50029882655af8d5eea40aed7644fce4564d8))
- Pass uncaught exception to default handler in GlobalExceptionHandler (so it's reported to crashlytics) ([@AntsyLich](https://github.com/AntsyLich)) ([`f3a2f56`](https://github.com/mihonapp/mihon/commit/f3a2f566c8a09ab862758ae69b43da2a2cd8f1db))
## [v0.16.5] - 2024-04-09 ## [v0.16.5] - 2024-04-09
### Added ### Added
- Relative date for up to a week in the future ([@sirlag](https://github.com/sirlag)) ([#415](https://github.com/mihonapp/mihon/pull/415)) - Setting to install custom color profiles to get true colors ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
- Advance setting to install custom color profiles ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
### Changed ### Changed
- Permanently enable 32-bit color mode ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) - Permanently enable 32-bit color mode ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
### Fixed ### Fixed
- Wrong dates in Updates and History tab due to time zone issues ([@sirlag](https://github.com/sirlag)) ([#402](https://github.com/mihonapp/mihon/pull/402)) - Fix wrong dates in Updates and History tab due to time zone issues ([@sirlag](https://github.com/sirlag)) ([#402](https://github.com/mihonapp/mihon/pull/402), [#415](https://github.com/mihonapp/mihon/pull/415))
- Fix extra date header introduced by parent PR ([@sirlag](https://github.com/sirlag)) ([#415](https://github.com/mihonapp/mihon/pull/415)) - Fix app infinitely retries tracker update instead of failing after 3 tries ([@MajorTanya](https://github.com/MajorTanya)) ([#411](https://github.com/mihonapp/mihon/pull/411))
- Fix build time in about screen displayed in UTC ([@AntsyLich](https://github.com/AntsyLich)) ([`aed53d3`](https://github.com/mihonapp/mihon/commit/aed53d3bdc85ce0e899fbb90b9f9cad0f1b86480)) - Fix crash on Pixel devices ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651))
- App infinitely retries tracker update instead of failing after 3 tries ([@MajorTanya](https://github.com/MajorTanya)) ([#411](https://github.com/mihonapp/mihon/pull/411)) - Fix crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466))
- Crash on Pixel devices (was introduced due to compose update) ([@AntsyLich](https://github.com/AntsyLich)) ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651)) - Fix crash in track date selection dialog ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5))
- Crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466)) - Fix dates for saved images on Samsung devices ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552))
- Crash when putting app in background while track date selection dialog is open ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5)) - Fix colors getting distorted when opening CMYK jpeg images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
- Dates for saved images not following the specification (fixes date issue mainly on Samsung devices) ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552))
- Colors getting distorted when opening CMYK jpeg images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
## [v0.16.4] - 2024-02-27 ## [v0.16.4] - 2024-02-26
### Changed ### Fixed
- Don't include custom user agent for MAL (circumvents MAL block) ([@AntsyLich](https://github.com/AntsyLich)) ([`085ad8d`](https://github.com/mihonapp/mihon/commit/085ad8d44637c375a8ed24aba3a6f75f5b0cc9ee)) - Circumvent MAL block ([@AntsyLich](https://github.com/AntsyLich)) ([`085ad8d`](https://github.com/mihonapp/mihon/commit/085ad8d44637c375a8ed24aba3a6f75f5b0cc9ee))
## [v0.16.3] - 2024-01-30 ## [v0.16.3] - 2024-01-30
### Added ### Added
- Copy extension debug info when clicking logo or name in the extension details screen ([@MajorTanya](https://github.com/MajorTanya)) ([#271](https://github.com/mihonapp/mihon/pull/271)) - Copy extension debug info when clicking logo or name in the extension details screen ([@MajorTanya](https://github.com/MajorTanya)) ([#271](https://github.com/mihonapp/mihon/pull/271))
### Changed ### Changed
- Hide display cutoff setting in reader settings sheet if fullscreen is disabled ([@Riztard](https://github.com/Riztard)) ([#241](https://github.com/mihonapp/mihon/pull/241)) - Rename extension update error file to `mihon_update_errors.txt` ([@mjishnu](https://github.com/mjishnu)) ([#253](https://github.com/mihonapp/mihon/pull/253))
- Library update error filename to `mihon_update_errors.txt` from `tachiyomi_update_errors.txt` ([@mjishnu](https://github.com/mjishnu)) ([#253](https://github.com/mihonapp/mihon/pull/253)) - Hide display cutoff setting in reader settings sheet if fullscreen is off ([@Riztard](https://github.com/Riztard)) ([#241](https://github.com/mihonapp/mihon/pull/241))
### Fixed ### Fixed
- Bottom sheet UI issues on non-tablet devices ([@theolm](https://github.com/theolm)) ([#182](https://github.com/mihonapp/mihon/pull/182)) - Fix bottom sheet display issues on non-Tablet UI ([@theolm](https://github.com/theolm)) ([#182](https://github.com/mihonapp/mihon/pull/182))
- Crash when switching screen while a list is scrolling ([@theolm](https://github.com/theolm)) ([#272](https://github.com/mihonapp/mihon/pull/272)) - Fix crash when switching screen while a list is scrolling ([@theolm](https://github.com/theolm)) ([#272](https://github.com/mihonapp/mihon/pull/272))
- Newly installed extensions not being recognized by Mihon ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#275](https://github.com/mihonapp/mihon/pull/275)) - Fix newly installed extensions not being recognized by Mihon ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#275](https://github.com/mihonapp/mihon/pull/275))
- Failing to refresh MAL token being inferred as token expiration ([@AntsyLich](https://github.com/AntsyLich)) ([`0f4de03`](https://github.com/mihonapp/mihon/commit/0f4de03d7a77b52490dc9a95e96a308b93b26e4f)) - Fix error handling when refreshing MAL OAuth token ([@AntsyLich](https://github.com/AntsyLich)) ([`0f4de03`](https://github.com/mihonapp/mihon/commit/0f4de03d7a77b52490dc9a95e96a308b93b26e4f))
### Other
- Add `detekt` (kotlin code analyzer) to the project ([@theolm](https://github.com/theolm)) ([#216](https://github.com/mihonapp/mihon/pull/216))
## [v0.16.2] - 2024-01-28 ## [v0.16.2] - 2024-01-28
### Added
- Scanlator filter is now part of Backup ([@jobobby04](https://github.com/jobobby04)) ([#166](https://github.com/mihonapp/mihon/pull/166))
### Changed ### Changed
- Backup now contains scanlator filter of a series ([@jobobby04](https://github.com/jobobby04)) ([#166](https://github.com/mihonapp/mihon/pull/166)) - Rename crash log filename to `mihon_crash_logs.txt` ([@MajorTanya](https://github.com/MajorTanya)) ([#234](https://github.com/mihonapp/mihon/pull/234))
- App icon scaling ([@AntsyLich](https://github.com/AntsyLich)) ([`26815c7`](https://github.com/mihonapp/mihon/commit/26815c7356111394665467c1e81255ac9ee33c1a))
- Tracker OAuth client to Mihon's (fixes login issue for Shikimori tracker) ([@AntsyLich](https://github.com/AntsyLich)) ([`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5))
- Tracker user agents ([@AntsyLich](https://github.com/AntsyLich), [@kitsumed](https://github.com/kitsumed)) ([`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5))
- Crash log filename to `mihon_crash_logs.txt` from `tachiyomi_crash_logs.txt` ([@MajorTanya](https://github.com/MajorTanya)) ([#234](https://github.com/mihonapp/mihon/pull/234))
- Don't try to refresh MAL token after refresh token expires ([@AntsyLich](https://github.com/AntsyLich)) ([`32188f9`](https://github.com/mihonapp/mihon/commit/32188f9f65009a18250674ef1bd6e57d351c1fba))
### Fixed ### Fixed
- "Flash screen on page change" making the screen full black ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5)) - "Flash screen on page change" Making the screen goes blank ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5))
- Faulty MangaUpdates score in database ([@AntsyLich](https://github.com/AntsyLich)) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2)) - App icon scaling ([@AntsyLich](https://github.com/AntsyLich)) ([`26815c7`](https://github.com/mihonapp/mihon/commit/26815c7356111394665467c1e81255ac9ee33c1a))
- Updating extension not reflecting correctly ([@AntsyLich](https://github.com/AntsyLich)) ([`cb06898`](https://github.com/mihonapp/mihon/commit/cb068984303f811692531bf6f14902ae118d8ac7)) - Updating extension not reflecting correctly ([@AntsyLich](https://github.com/AntsyLich)) ([`cb06898`](https://github.com/mihonapp/mihon/commit/cb068984303f811692531bf6f14902ae118d8ac7))
- Inconsistent button height in "Data and storage" for some languages ([@theolm](https://github.com/theolm)) ([#202](https://github.com/mihonapp/mihon/pull/202)) - Inconsistent button height with some languages in "Data and storage" ([@theolm](https://github.com/theolm)) ([#202](https://github.com/mihonapp/mihon/pull/202))
- Chapter not being marked as read locally when refreshing Enhanced Trackers ([@Secozzi](https://github.com/Secozzi)) ([#219](https://github.com/mihonapp/mihon/pull/219)) - Fix chapter not being marked as read in some cases with Enhanced Trackers ([@Secozzi](https://github.com/Secozzi)) ([#219](https://github.com/mihonapp/mihon/pull/219))
- And various tracker related fixes ([@AntsyLich](https://github.com/AntsyLich), [@kitsumed](https://github.com/kitsumed), [@Secozzi](https://github.com/Secozzi)) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2), [`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5), [`32188f9`](https://github.com/mihonapp/mihon/commit/32188f9f65009a18250674ef1bd6e57d351c1fba))
### Other
- Make `last_modified_at` field in database be `0` on insert ([@kaiserbh](https://github.com/kaiserbh)) ([#113](https://github.com/mihonapp/mihon/pull/113))
- Remove usage of `.not()` where possible in code ([@AntsyLich](https://github.com/AntsyLich)) ([`3940740`](https://github.com/mihonapp/mihon/commit/39407407f282dbb7fa972b12053c26b3e3bd66d8))
- Use type-safe project accessors ([@theolm](https://github.com/theolm)) ([#194](https://github.com/mihonapp/mihon/pull/194))
- Legacy tracker model properties now has the same type as the domain ones ([@AntsyLich](https://github.com/AntsyLich)) ([#245](https://github.com/mihonapp/mihon/pull/245))
## [v0.16.1] - 2024-01-18 ## [v0.16.1] - 2024-01-18
### Changed
- Branding to Mihon (for references we missed) ([@AntsyLich](https://github.com/AntsyLich)) ([`6539406`](https://github.com/mihonapp/mihon/commit/653940613d661eb371aab3b3c3a8181e4e308c43))
- Preview builds are now called Beta builds ([@AntsyLich](https://github.com/AntsyLich)) ([`3c3a1cd`](https://github.com/mihonapp/mihon/commit/3c3a1cd448ab1f653ddd12b2afe0cba38968d1b9))
### Fixed ### Fixed
- App icon not following the [specification](https://developer.android.com/develop/ui/views/launch/icon_design_adaptive) ([@AntsyLich](https://github.com/AntsyLich)) ([`1849715`](https://github.com/mihonapp/mihon/commit/18497154183356bb0d469b27827f9f7d6b7a3130)) - App Icon not filled ([@AntsyLich](https://github.com/AntsyLich)) ([`1849715`](https://github.com/mihonapp/mihon/commit/18497154183356bb0d469b27827f9f7d6b7a3130))
- MangaUpdates default score being set to -1.0 ([@AntsyLich](https://github.com/AntsyLich)) ([`99fd273`](https://github.com/mihonapp/mihon/commit/99fd2731f5d9d374700e89fa67d4d5bf611bbafa)) - MangaUpdates default score being set to -1.0 ([@AntsyLich](https://github.com/AntsyLich)) ([`99fd273`](https://github.com/mihonapp/mihon/commit/99fd2731f5d9d374700e89fa67d4d5bf611bbafa))
## [v0.16.0] - 2024-01-16 ## [v0.16.0] - 2024-01-16
### Changed
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.18.0...main "The end of 立ち読み (Tachiyomi) is the beginning of みほん (Mihon)"
[v0.18.0]: https://github.com/mihonapp/mihon/compare/v0.17.1...v0.18.0 Credit to LinkCable, the icon designer, for this poetic quote.
[v0.17.1]: https://github.com/mihonapp/mihon/compare/v0.17.0...v0.17.1
[v0.17.0]: https://github.com/mihonapp/mihon/compare/v0.16.5...v0.17.0 What's New?
Well, nothing, except you now you need Android 8+ to install the app.
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.16.5...HEAD
[v0.16.5]: https://github.com/mihonapp/mihon/compare/v0.16.4...v0.16.5 [v0.16.5]: https://github.com/mihonapp/mihon/compare/v0.16.4...v0.16.5
[v0.16.4]: https://github.com/mihonapp/mihon/compare/v0.16.3...v0.16.4 [v0.16.4]: https://github.com/mihonapp/mihon/compare/v0.16.3...v0.16.4
[v0.16.3]: https://github.com/mihonapp/mihon/compare/v0.16.2...v0.16.3 [v0.16.3]: https://github.com/mihonapp/mihon/compare/v0.16.2...v0.16.3
[v0.16.2]: https://github.com/mihonapp/mihon/compare/v0.16.1...v0.16.2 [v0.16.2]: https://github.com/mihonapp/mihon/compare/v0.16.1...v0.16.2
[v0.16.1]: https://github.com/mihonapp/mihon/compare/v0.16.0...v0.16.1 [v0.16.1]: https://github.com/mihonapp/mihon/compare/v0.16.0...v0.16.1
[v0.16.0]: https://github.com/mihonapp/mihon/compare/a9c7cbf...v0.16.0 [v0.16.0]: https://github.com/mihonapp/mihon/releases/tag/v0.16.0

View File

@@ -68,7 +68,7 @@ The developer(s) of this application does not have any affiliation with the cont
<pre> <pre>
Copyright © 2015 Javier Tomás Copyright © 2015 Javier Tomás
Copyright © 2024 Mihon Open Source Project Copyright © 2024 The Mihon Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

3
app/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/build
*iml
*.iml

View File

@@ -1,7 +1,7 @@
import mihon.buildlogic.Config
import mihon.buildlogic.getBuildTime import mihon.buildlogic.getBuildTime
import mihon.buildlogic.getCommitCount import mihon.buildlogic.getCommitCount
import mihon.buildlogic.getGitSha import mihon.buildlogic.getGitSha
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("mihon.android.application") id("mihon.android.application")
@@ -11,7 +11,7 @@ plugins {
alias(libs.plugins.aboutLibraries) alias(libs.plugins.aboutLibraries)
} }
if (Config.includeTelemetry) { if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
pluginManager.apply { pluginManager.apply {
apply(libs.plugins.google.services.get().pluginId) apply(libs.plugins.google.services.get().pluginId)
apply(libs.plugins.firebase.crashlytics.get().pluginId) apply(libs.plugins.firebase.crashlytics.get().pluginId)
@@ -20,71 +20,69 @@ if (Config.includeTelemetry) {
shortcutHelper.setFilePath("./shortcuts.xml") shortcutHelper.setFilePath("./shortcuts.xml")
val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android { android {
namespace = "eu.kanade.tachiyomi" namespace = "eu.kanade.tachiyomi"
defaultConfig { defaultConfig {
applicationId = "app.mihon" applicationId = "app.mihon"
versionCode = 11 versionCode = 7
versionName = "0.18.0" versionName = "0.16.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"") buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}") buildConfigField("boolean", "INCLUDE_UPDATER", "false")
buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}") buildConfigField("boolean", "PREVIEW", "false")
ndk {
abiFilters += supportedAbis
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
splits {
abi {
isEnable = true
reset()
include(*supportedAbis.toTypedArray())
isUniversalApk = true
}
}
buildTypes { buildTypes {
val debug by getting { named("debug") {
applicationIdSuffix = ".dev"
versionNameSuffix = "-${getCommitCount()}" versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug"
isPseudoLocalesEnabled = true isPseudoLocalesEnabled = true
} }
val release by getting { named("release") {
isMinifyEnabled = Config.enableCodeShrink isShrinkResources = true
isShrinkResources = Config.enableCodeShrink isMinifyEnabled = true
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"")
}
val commonMatchingFallbacks = listOf(release.name)
create("foss") {
initWith(release)
applicationIdSuffix = ".foss"
matchingFallbacks.addAll(commonMatchingFallbacks)
} }
create("preview") { create("preview") {
initWith(release) initWith(getByName("release"))
buildConfigField("boolean", "PREVIEW", "true")
applicationIdSuffix = ".debug" signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
versionNameSuffix = debug.versionNameSuffix val debugType = getByName("debug")
signingConfig = debug.signingConfig versionNameSuffix = debugType.versionNameSuffix
applicationIdSuffix = debugType.applicationIdSuffix
matchingFallbacks.addAll(commonMatchingFallbacks)
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
} }
create("benchmark") { create("benchmark") {
initWith(release) initWith(getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
isDebuggable = false isDebuggable = false
isProfileable = true isProfileable = true
versionNameSuffix = "-benchmark" versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark" applicationIdSuffix = ".benchmark"
signingConfig = debug.signingConfig
matchingFallbacks.addAll(commonMatchingFallbacks)
} }
} }
@@ -93,46 +91,39 @@ android {
getByName("benchmark").res.srcDirs("src/debug/res") getByName("benchmark").res.srcDirs("src/debug/res")
} }
splits { flavorDimensions.add("default")
abi {
isEnable = true productFlavors {
isUniversalApk = true create("standard") {
reset() buildConfigField("boolean", "INCLUDE_UPDATER", "true")
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") 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"
} }
} }
packaging { packaging {
jniLibs { resources.excludes.addAll(
keepDebugSymbols += listOf( listOf(
"libandroidx.graphics.path",
"libarchive-jni",
"libconscrypt_jni",
"libimagedecoder",
"libquickjs",
"libsqlite3x",
)
.map { "**/$it.so" }
}
resources {
excludes += setOf(
"kotlin-tooling-metadata.json", "kotlin-tooling-metadata.json",
"META-INF/DEPENDENCIES",
"LICENSE.txt", "LICENSE.txt",
"META-INF/**/*.properties", "META-INF/LICENSE",
"META-INF/**/LICENSE.txt", "META-INF/**/LICENSE.txt",
"META-INF/*.properties", "META-INF/*.properties",
"META-INF/*.version", "META-INF/**/*.properties",
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/NOTICE",
"META-INF/README.md", "META-INF/README.md",
"META-INF/NOTICE",
"META-INF/*.version",
),
) )
} }
}
dependenciesInfo { dependenciesInfo {
includeInApk = Config.includeDependencyInfo includeInApk = false
includeInBundle = Config.includeDependencyInfo
} }
buildFeatures { buildFeatures {
@@ -151,24 +142,6 @@ android {
} }
} }
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",
)
}
}
dependencies { dependencies {
implementation(projects.i18n) implementation(projects.i18n)
implementation(projects.core.archive) implementation(projects.core.archive)
@@ -180,7 +153,6 @@ dependencies {
implementation(projects.domain) implementation(projects.domain)
implementation(projects.presentationCore) implementation(projects.presentationCore)
implementation(projects.presentationWidget) implementation(projects.presentationWidget)
implementation(projects.telemetry)
// Compose // Compose
implementation(compose.activity) implementation(compose.activity)
@@ -269,11 +241,15 @@ dependencies {
implementation(libs.swipe) implementation(libs.swipe)
implementation(libs.compose.webview) implementation(libs.compose.webview)
implementation(libs.compose.grid) implementation(libs.compose.grid)
implementation(libs.reorderable)
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)
// Crash reports/analytics
"standardImplementation"(platform(libs.firebase.bom))
"standardImplementation"(libs.firebase.analytics)
"standardImplementation"(libs.firebase.crashlytics)
// Shizuku // Shizuku
implementation(libs.bundles.shizuku) implementation(libs.bundles.shizuku)
@@ -303,6 +279,28 @@ androidComponents {
} }
} }
tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> {
compilerOptions.freeCompilerArgs.addAll(
"-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=coil3.annotation.ExperimentalCoilApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}
buildscript { buildscript {
dependencies { dependencies {
classpath(kotlinx.gradle) classpath(kotlinx.gradle)

View File

@@ -1,12 +1,11 @@
package eu.kanade.core.util package eu.kanade.core.util
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators( fun <T : R, R : Any> List<T>.insertSeparators(
generator: (before: T?, after: T?) -> R?, generator: (T?, T?) -> R?,
): List<R> { ): List<R> {
if (isEmpty()) return emptyList() if (isEmpty()) return emptyList()
val newList = mutableListOf<R>() val newList = mutableListOf<R>()
@@ -20,24 +19,6 @@ fun <T : R, R : Any> List<T>.insertSeparators(
return newList 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) { fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) { if (shouldAdd) {
add(value) 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]. * 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) @OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> { inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) } 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 } fastForEach { if (predicate(it)) --count }
return 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

@@ -13,11 +13,9 @@ import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.source.interactor.GetLanguagesWithSources import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting 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.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
@@ -111,7 +109,7 @@ class DomainModule : InjektModule {
addFactory { RenameCategory(get()) } addFactory { RenameCategory(get()) }
addFactory { ReorderCategory(get()) } addFactory { ReorderCategory(get()) }
addFactory { UpdateCategory(get()) } addFactory { UpdateCategory(get()) }
addFactory { DeleteCategory(get(), get(), get()) } addFactory { DeleteCategory(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) } addFactory { GetDuplicateLibraryManga(get()) }
@@ -153,7 +151,7 @@ class DomainModule : InjektModule {
addFactory { UpdateChapter(get()) } addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() } addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) } addFactory { GetAvailableScanlators(get()) }
addFactory { FilterChaptersForDownload(get(), get(), get()) } addFactory { FilterChaptersForDownload(get(), get(), get()) }
@@ -193,7 +191,5 @@ class DomainModule : InjektModule {
addFactory { DeleteExtensionRepo(get()) } addFactory { DeleteExtensionRepo(get()) }
addFactory { ReplaceExtensionRepo(get()) } addFactory { ReplaceExtensionRepo(get()) }
addFactory { UpdateExtensionRepo(get(), get()) } addFactory { UpdateExtensionRepo(get(), get()) }
addFactory { ToggleIncognito(get()) }
addFactory { GetIncognitoState(get(), get(), get()) }
} }
} }

View File

@@ -2,7 +2,6 @@ package eu.kanade.domain.base
import android.content.Context import android.content.Context
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.GLUtil
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -31,8 +30,4 @@ class BasePreferences(
} }
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "") 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

@@ -19,7 +19,6 @@ import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.chapter.model.toChapterUpdate import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.chapter.service.ChapterRecognition import tachiyomi.domain.chapter.service.ChapterRecognition
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import java.lang.Long.max import java.lang.Long.max
@@ -35,7 +34,6 @@ class SyncChaptersWithSource(
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators, private val getExcludedScanlators: GetExcludedScanlators,
private val libraryPreferences: LibraryPreferences,
) { ) {
/** /**
@@ -147,18 +145,12 @@ class SyncChaptersWithSource(
return emptyList() return emptyList()
} }
val changedOrDuplicateReadUrls = mutableSetOf<String>() val reAdded = mutableListOf<Chapter>()
val deletedChapterNumbers = TreeSet<Double>() val deletedChapterNumbers = TreeSet<Double>()
val deletedReadChapterNumbers = TreeSet<Double>() val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>() val deletedBookmarkedChapterNumbers = TreeSet<Double>()
val readChapterNumbers = dbChapters
.asSequence()
.filter { it.read && it.isRecognizedNumber }
.map { it.chapterNumber }
.toSet()
removedChapters.forEach { chapter -> removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber) if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
@@ -168,20 +160,12 @@ class SyncChaptersWithSource(
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to 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 // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the chapters from most to less recent, which is common. // Sources MUST return the chapters from most to less recent, which is common.
var itemCount = newChapters.size var itemCount = newChapters.size
var updatedToAdd = newChapters.map { toAddItem -> var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) 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 if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
chapter = chapter.copy( chapter = chapter.copy(
@@ -194,7 +178,7 @@ class SyncChaptersWithSource(
chapter = chapter.copy(dateFetch = it) chapter = chapter.copy(dateFetch = it)
} }
changedOrDuplicateReadUrls.add(chapter.url) reAdded.add(chapter)
chapter chapter
} }
@@ -218,8 +202,12 @@ class SyncChaptersWithSource(
// Note that last_update actually represents last time the chapter list changed at all // Note that last_update actually represents last time the chapter list changed at all
updateManga.awaitUpdateLastUpdate(manga.id) updateManga.awaitUpdateLastUpdate(manga.id)
val reAddedUrls = reAdded.map { it.url }.toHashSet()
val excludedScanlators = getExcludedScanlators.await(manga.id).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

@@ -22,7 +22,7 @@ val Manga.readerOrientation: Long
val Manga.downloadedFilter: TriState val Manga.downloadedFilter: TriState
get() { get() {
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS if (forceDownloaded()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) { return when (downloadedFilterRaw) {
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
@@ -34,6 +34,9 @@ fun Manga.chaptersFiltered(): Boolean {
downloadedFilter != TriState.DISABLED || downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED bookmarkedFilter != TriState.DISABLED
} }
fun Manga.forceDownloaded(): Boolean {
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
}
fun Manga.toSManga(): SManga = SManga.create().also { fun Manga.toSManga(): SManga = SManga.create().also {
it.url = url it.url = url

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 disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedSource() = preferenceStore.getLong( fun lastUsedSource() = preferenceStore.getLong(

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

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

View File

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

View File

@@ -1,7 +1,8 @@
package eu.kanade.domain.ui.model package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
enum class AppTheme(val titleRes: StringResource?) { enum class AppTheme(val titleRes: StringResource?) {
@@ -12,14 +13,13 @@ enum class AppTheme(val titleRes: StringResource?) {
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk), MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
// TODO: re-enable for preview // TODO: re-enable for preview
NORD(MR.strings.theme_nord.takeUnless { isReleaseBuildType }), NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri), STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
TAKO(MR.strings.theme_tako), TAKO(MR.strings.theme_tako),
TEALTURQUOISE(MR.strings.theme_tealturquoise), TEALTURQUOISE(MR.strings.theme_tealturquoise),
TIDAL_WAVE(MR.strings.theme_tidalwave), TIDAL_WAVE(MR.strings.theme_tidalwave),
YINYANG(MR.strings.theme_yinyang), YINYANG(MR.strings.theme_yinyang),
YOTSUBA(MR.strings.theme_yotsuba), YOTSUBA(MR.strings.theme_yotsuba),
MONOCHROME(MR.strings.theme_monochrome),
// Deprecated // Deprecated
DARK_BLUE(null), DARK_BLUE(null),

View File

@@ -35,10 +35,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -50,7 +48,6 @@ import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
@@ -75,7 +72,6 @@ fun ExtensionDetailsScreen(
onClickClearCookies: () -> Unit, onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val url = remember(state.extension) { val url = remember(state.extension) {
@@ -144,11 +140,9 @@ fun ExtensionDetailsScreen(
contentPadding = paddingValues, contentPadding = paddingValues,
extension = state.extension, extension = state.extension,
sources = state.sources, sources = state.sources,
incognitoMode = state.isIncognito,
onClickSourcePreferences = onClickSourcePreferences, onClickSourcePreferences = onClickSourcePreferences,
onClickUninstall = onClickUninstall, onClickUninstall = onClickUninstall,
onClickSource = onClickSource, onClickSource = onClickSource,
onClickIncognito = onClickIncognito,
) )
} }
} }
@@ -158,11 +152,9 @@ private fun ExtensionDetails(
contentPadding: PaddingValues, contentPadding: PaddingValues,
extension: Extension.Installed, extension: Extension.Installed,
sources: ImmutableList<ExtensionSourceItem>, sources: ImmutableList<ExtensionSourceItem>,
incognitoMode: Boolean,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
var showNsfwWarning by remember { mutableStateOf(false) } var showNsfwWarning by remember { mutableStateOf(false) }
@@ -179,7 +171,6 @@ private fun ExtensionDetails(
item { item {
DetailsHeader( DetailsHeader(
extension = extension, extension = extension,
extIncognitoMode = incognitoMode,
onClickUninstall = onClickUninstall, onClickUninstall = onClickUninstall,
onClickAppInfo = { onClickAppInfo = {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
@@ -191,7 +182,6 @@ private fun ExtensionDetails(
onClickAgeRating = { onClickAgeRating = {
showNsfwWarning = true showNsfwWarning = true
}, },
onExtIncognitoChange = onClickIncognito,
) )
} }
@@ -219,11 +209,9 @@ private fun ExtensionDetails(
@Composable @Composable
private fun DetailsHeader( private fun DetailsHeader(
extension: Extension, extension: Extension,
extIncognitoMode: Boolean,
onClickAgeRating: () -> Unit, onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickAppInfo: (() -> Unit)?, onClickAppInfo: (() -> Unit)?,
onExtIncognitoChange: (Boolean) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -231,8 +219,9 @@ private fun DetailsHeader(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = MaterialTheme.padding.medium)
.padding( .padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium, top = MaterialTheme.padding.medium,
bottom = MaterialTheme.padding.small, bottom = MaterialTheme.padding.small,
) )
@@ -324,9 +313,12 @@ private fun DetailsHeader(
} }
Row( Row(
modifier = Modifier modifier = Modifier.padding(
.padding(horizontal = MaterialTheme.padding.medium) start = MaterialTheme.padding.medium,
.padding(top = MaterialTheme.padding.small), end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) { ) {
OutlinedButton( OutlinedButton(
@@ -349,24 +341,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() HorizontalDivider()
} }
} }

View File

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

View File

@@ -193,6 +193,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 @Composable
fun ChangeCategoryDialog( fun ChangeCategoryDialog(
initialSelection: ImmutableList<CheckboxState<Category>>, initialSelection: ImmutableList<CheckboxState<Category>>,

View File

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

View File

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

View File

@@ -38,7 +38,6 @@ fun HistoryScreen(
onSearchQueryChange: (String?) -> Unit, onSearchQueryChange: (String?) -> Unit,
onClickCover: (mangaId: Long) -> Unit, onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onClickFavorite: (mangaId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
) { ) {
Scaffold( Scaffold(
@@ -85,7 +84,6 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) }, onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
) )
} }
} }
@@ -99,7 +97,6 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit, onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit, onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit, onClickDelete: (HistoryWithRelations) -> Unit,
onClickFavorite: (HistoryWithRelations) -> Unit,
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
@@ -129,7 +126,6 @@ private fun HistoryScreenContent(
onClickCover = { onClickCover(value) }, onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) }, onClickResume = { onClickResume(value) },
onClickDelete = { onClickDelete(value) }, onClickDelete = { onClickDelete(value) },
onClickFavorite = { onClickFavorite(value) },
) )
} }
} }
@@ -156,7 +152,6 @@ internal fun HistoryScreenPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = { _, _ -> run {} }, onClickResume = { _, _ -> run {} },
onDialogChange = {}, onDialogChange = {},
onClickFavorite = {},
) )
} }
} }

View File

@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -40,7 +39,6 @@ fun HistoryItem(
onClickCover: () -> Unit, onClickCover: () -> Unit,
onClickResume: () -> Unit, onClickResume: () -> Unit,
onClickDelete: () -> Unit, onClickDelete: () -> Unit,
onClickFavorite: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( 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) { IconButton(onClick = onClickDelete) {
Icon( Icon(
imageVector = Icons.Outlined.Delete, imageVector = Icons.Outlined.Delete,
@@ -117,7 +105,6 @@ private fun HistoryItemPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = {}, onClickResume = {},
onClickDelete = {}, onClickDelete = {},
onClickFavorite = {},
) )
} }
} }

View File

@@ -9,7 +9,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -20,7 +19,8 @@ import androidx.compose.ui.platform.LocalConfiguration
import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.TriState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@@ -117,7 +117,10 @@ private fun ColumnScope.FilterPage(
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) }, onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
) )
// TODO: re-enable when custom intervals are ready for stable // TODO: re-enable when custom intervals are ready for stable
if ((!isReleaseBuildType) && LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions) { if (
(isDevFlavor || isPreviewBuildType) &&
LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions
) {
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState() val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
TriStateItem( TriStateItem(
label = stringResource(MR.strings.action_filter_interval_custom), label = stringResource(MR.strings.action_filter_interval_custom),
@@ -163,13 +166,13 @@ private fun ColumnScope.SortPage(
val sortingMode = category.sort.type val sortingMode = category.sort.type
val sortDescending = !category.sort.isAscending val sortDescending = !category.sort.isAscending
val options = remember(trackers.isEmpty()) { val trackerSortOption = if (trackers.isEmpty()) {
val trackerMeanPair = if (trackers.isNotEmpty()) { emptyList()
MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean
} else { } else {
null listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
} }
listOfNotNull(
listOf(
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical, MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical,
MR.strings.action_sort_total to LibrarySort.Type.TotalChapters, MR.strings.action_sort_total to LibrarySort.Type.TotalChapters,
MR.strings.action_sort_last_read to LibrarySort.Type.LastRead, MR.strings.action_sort_last_read to LibrarySort.Type.LastRead,
@@ -178,12 +181,8 @@ private fun ColumnScope.SortPage(
MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter, 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_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded, MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded,
trackerMeanPair,
MR.strings.action_sort_random to LibrarySort.Type.Random, MR.strings.action_sort_random to LibrarySort.Type.Random,
) ).plus(trackerSortOption).map { (titleRes, mode) ->
}
options.map { (titleRes, mode) ->
if (mode == LibrarySort.Type.Random) { if (mode == LibrarySort.Type.Random) {
BaseSortItem( BaseSortItem(
label = stringResource(titleRes), label = stringResource(titleRes),
@@ -252,16 +251,15 @@ private fun ColumnScope.DisplayPage(
val columns by columnPreference.collectAsState() val columns by columnPreference.collectAsState()
SliderItem( SliderItem(
value = columns,
valueRange = 0..10,
label = stringResource(MR.strings.pref_library_columns), label = stringResource(MR.strings.pref_library_columns),
max = 10,
value = columns,
valueText = if (columns > 0) { valueText = if (columns > 0) {
columns.toString() stringResource(MR.strings.pref_library_columns_per_row, columns)
} else { } else {
stringResource(MR.strings.label_auto) stringResource(MR.strings.label_default)
}, },
onChange = columnPreference::set, onChange = columnPreference::set,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
} }
@@ -270,10 +268,6 @@ private fun ColumnScope.DisplayPage(
label = stringResource(MR.strings.action_display_download_badge), label = stringResource(MR.strings.action_display_download_badge),
pref = screenModel.libraryPreferences.downloadBadge(), pref = screenModel.libraryPreferences.downloadBadge(),
) )
CheckboxItem(
label = stringResource(MR.strings.action_display_unread_badge),
pref = screenModel.libraryPreferences.unreadBadge(),
)
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_local_badge), label = stringResource(MR.strings.action_display_local_badge),
pref = screenModel.libraryPreferences.localBadge(), pref = screenModel.libraryPreferences.localBadge(),

View File

@@ -21,12 +21,11 @@ internal fun LibraryTabs(
getNumberOfMangaForCategory: (Category) -> Int?, getNumberOfMangaForCategory: (Category) -> Int?,
onTabItemClick: (Int) -> Unit, onTabItemClick: (Int) -> Unit,
) { ) {
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
Column( Column(
modifier = Modifier.zIndex(1f), modifier = Modifier.zIndex(1f),
) { ) {
PrimaryScrollableTabRow( PrimaryScrollableTabRow(
selectedTabIndex = currentPageIndex, selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp, edgePadding = 0.dp,
// TODO: use default when width is fixed upstream // TODO: use default when width is fixed upstream
// https://issuetracker.google.com/issues/242879624 // https://issuetracker.google.com/issues/242879624
@@ -34,7 +33,7 @@ internal fun LibraryTabs(
) { ) {
categories.forEachIndexed { index, category -> categories.forEachIndexed { index, category ->
Tab( Tab(
selected = currentPageIndex == index, selected = pagerState.currentPage == index,
onClick = { onTabItemClick(index) }, onClick = { onTabItemClick(index) },
text = { text = {
TabText( TabText(

View File

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

View File

@@ -87,7 +87,7 @@ fun MangaScreen(
isTabletUi: Boolean, isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
navigateUp: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
@@ -141,7 +141,7 @@ fun MangaScreen(
nextUpdate = nextUpdate, nextUpdate = nextUpdate,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
navigateUp = navigateUp, onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
@@ -176,7 +176,7 @@ fun MangaScreen(
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
nextUpdate = nextUpdate, nextUpdate = nextUpdate,
navigateUp = navigateUp, onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
@@ -214,7 +214,7 @@ private fun MangaScreenSmallImpl(
nextUpdate: Instant?, nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
navigateUp: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
@@ -265,13 +265,14 @@ private fun MangaScreenSmallImpl(
) )
} }
BackHandler(onBack = { val internalOnBackPressed = {
if (isAnySelected) { if (isAnySelected) {
onAllChapterSelected(false) onAllChapterSelected(false)
} else { } else {
navigateUp() onBackClicked()
} }
}) }
BackHandler(onBack = internalOnBackPressed)
Scaffold( Scaffold(
topBar = { topBar = {
@@ -284,18 +285,20 @@ private fun MangaScreenSmallImpl(
val isFirstItemScrolled by remember { val isFirstItemScrolled by remember {
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 } derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
} }
val titleAlpha by animateFloatAsState( val animatedTitleAlpha by animateFloatAsState(
if (!isFirstItemVisible) 1f else 0f, if (!isFirstItemVisible) 1f else 0f,
label = "Top Bar Title", label = "Top Bar Title",
) )
val backgroundAlpha by animateFloatAsState( val animatedBgAlpha by animateFloatAsState(
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f, if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
label = "Top Bar Background", label = "Top Bar Background",
) )
MangaToolbar( MangaToolbar(
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.filterActive, hasFilters = state.filterActive,
navigateUp = navigateUp, onBackClicked = internalOnBackPressed,
onClickFilter = onFilterClicked, onClickFilter = onFilterClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked, onClickDownload = onDownloadActionClicked,
@@ -303,11 +306,8 @@ private fun MangaScreenSmallImpl(
onClickRefresh = onRefresh, onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked, onClickMigrate = onMigrateClicked,
actionModeCounter = selectedChapterCount, actionModeCounter = selectedChapterCount,
onCancelActionMode = { onAllChapterSelected(false) },
onSelectAll = { onAllChapterSelected(true) }, onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() }, onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { titleAlpha },
backgroundAlphaProvider = { backgroundAlpha },
) )
}, },
bottomBar = { bottomBar = {
@@ -458,7 +458,7 @@ fun MangaScreenLargeImpl(
nextUpdate: Instant?, nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
navigateUp: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
@@ -515,13 +515,14 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
BackHandler(onBack = { val internalOnBackPressed = {
if (isAnySelected) { if (isAnySelected) {
onAllChapterSelected(false) onAllChapterSelected(false)
} else { } else {
navigateUp() onBackClicked()
} }
}) }
BackHandler(onBack = internalOnBackPressed)
Scaffold( Scaffold(
topBar = { topBar = {
@@ -531,20 +532,19 @@ fun MangaScreenLargeImpl(
MangaToolbar( MangaToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height }, modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive, hasFilters = state.filterActive,
navigateUp = navigateUp, onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked, onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked, onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked, onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh, onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked, onClickMigrate = onMigrateClicked,
onCancelActionMode = { onAllChapterSelected(false) },
actionModeCounter = selectedChapterCount, actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) }, onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() }, onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { 1f },
backgroundAlphaProvider = { 1f },
) )
}, },
bottomBar = { bottomBar = {

View File

@@ -19,7 +19,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -108,7 +109,7 @@ fun SetIntervalDialog(
} }
Spacer(Modifier.height(MaterialTheme.padding.small)) Spacer(Modifier.height(MaterialTheme.padding.small))
if (onValueChanged != null && (!isReleaseBuildType)) { if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
Text(stringResource(MR.strings.manga_interval_custom_amount)) Text(stringResource(MR.strings.manga_interval_custom_amount))
BoxWithConstraints( BoxWithConstraints(

View File

@@ -1,12 +1,18 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -14,12 +20,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.DownloadDropdownMenu import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -29,8 +35,9 @@ import tachiyomi.presentation.core.theme.active
@Composable @Composable
fun MangaToolbar( fun MangaToolbar(
title: String, title: String,
titleAlphaProvider: () -> Float,
hasFilters: Boolean, hasFilters: Boolean,
navigateUp: () -> Unit, onBackClicked: () -> Unit,
onClickFilter: () -> Unit, onClickFilter: () -> Unit,
onClickShare: (() -> Unit)?, onClickShare: (() -> Unit)?,
onClickDownload: ((DownloadAction) -> Unit)?, onClickDownload: ((DownloadAction) -> Unit)?,
@@ -40,29 +47,47 @@ fun MangaToolbar(
// For action mode // For action mode
actionModeCounter: Int, actionModeCounter: Int,
onCancelActionMode: () -> Unit,
onSelectAll: () -> Unit, onSelectAll: () -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
titleAlphaProvider: () -> Float,
backgroundAlphaProvider: () -> Float,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
) {
Column(
modifier = modifier,
) { ) {
val isActionMode = actionModeCounter > 0 val isActionMode = actionModeCounter > 0
AppBar( TopAppBar(
titleContent = { title = {
if (isActionMode) { Text(
AppBarTitle(actionModeCounter.toString()) text = if (isActionMode) actionModeCounter.toString() else title,
} else { maxLines = 1,
AppBarTitle(title, modifier = Modifier.alpha(titleAlphaProvider())) overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()),
)
},
navigationIcon = {
IconButton(onClick = onBackClicked) {
UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode })
} }
}, },
modifier = modifier,
backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
navigateUp = navigateUp,
actions = { actions = {
if (isActionMode) {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = onSelectAll,
),
AppBar.Action(
title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack,
onClick = onInvertSelection,
),
),
)
} else {
var downloadExpanded by remember { mutableStateOf(false) } var downloadExpanded by remember { mutableStateOf(false) }
if (onClickDownload != null) { if (onClickDownload != null) {
val onDismissRequest = { downloadExpanded = false } val onDismissRequest = { downloadExpanded = false }
@@ -75,24 +100,8 @@ fun MangaToolbar(
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
AppBarActions( AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder().apply { actions = persistentListOf<AppBar.AppBarAction>().builder()
if (isActionMode) { .apply {
add(
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = onSelectAll,
),
)
add(
AppBar.Action(
title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack,
onClick = onInvertSelection,
),
)
return@apply
}
if (onClickDownload != null) { if (onClickDownload != null) {
add( add(
AppBar.Action( AppBar.Action(
@@ -143,8 +152,13 @@ fun MangaToolbar(
} }
.build(), .build(),
) )
}
}, },
isActionMode = isActionMode, colors = TopAppBarDefaults.topAppBarColors(
onCancelActionMode = onCancelActionMode, containerColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
),
) )
} }
}

View File

@@ -1,6 +1,12 @@
package eu.kanade.presentation.more package eu.kanade.presentation.more
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label import androidx.compose.material.icons.automirrored.outlined.Label
@@ -34,6 +40,7 @@ fun MoreScreen(
onDownloadedOnlyChange: (Boolean) -> Unit, onDownloadedOnlyChange: (Boolean) -> Unit,
incognitoMode: Boolean, incognitoMode: Boolean,
onIncognitoModeChange: (Boolean) -> Unit, onIncognitoModeChange: (Boolean) -> Unit,
isFDroid: Boolean,
onClickDownloadQueue: () -> Unit, onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit, onClickCategories: () -> Unit,
onClickStats: () -> Unit, onClickStats: () -> Unit,
@@ -43,7 +50,19 @@ fun MoreScreen(
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
Scaffold { contentPadding -> Scaffold(
topBar = {
Column(
modifier = Modifier.windowInsetsPadding(
WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
),
) {
if (isFDroid) {
// Don't really care about slow updaters now
}
}
},
) { contentPadding ->
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) { ) {

View File

@@ -4,6 +4,7 @@ import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
@@ -13,13 +14,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -31,24 +30,17 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import eu.kanade.tachiyomi.util.system.telemetryIncluded
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.injectLazy
internal class PermissionStep : OnboardingStep { internal class PermissionStep : OnboardingStep {
private val privacyPreferences: PrivacyPreferences by injectLazy()
private var notificationGranted by mutableStateOf(false) private var notificationGranted by mutableStateOf(false)
private var batteryGranted by mutableStateOf(false) private var batteryGranted by mutableStateOf(false)
@@ -81,7 +73,7 @@ internal class PermissionStep : OnboardingStep {
} }
Column { Column {
PermissionCheckbox( PermissionItem(
title = stringResource(MR.strings.onboarding_permission_install_apps), title = stringResource(MR.strings.onboarding_permission_install_apps),
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description), subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
granted = installGranted, granted = installGranted,
@@ -97,7 +89,7 @@ internal class PermissionStep : OnboardingStep {
// no-op. resulting checks is being done on resume // no-op. resulting checks is being done on resume
}, },
) )
PermissionCheckbox( PermissionItem(
title = stringResource(MR.strings.onboarding_permission_notifications), title = stringResource(MR.strings.onboarding_permission_notifications),
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description), subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
granted = notificationGranted, granted = notificationGranted,
@@ -105,43 +97,18 @@ internal class PermissionStep : OnboardingStep {
) )
} }
PermissionCheckbox( PermissionItem(
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts), title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description), subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
granted = batteryGranted, granted = batteryGranted,
onButtonClick = { onButtonClick = {
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = "package:${context.packageName}".toUri() data = Uri.parse("package:${context.packageName}")
} }
context.startActivity(intent) context.startActivity(intent)
}, },
) )
if (!telemetryIncluded) return@Column
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
val crashlyticsPref = privacyPreferences.crashlytics()
val crashlytics by crashlyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
granted = crashlytics,
onToggleChange = crashlyticsPref::set,
)
val analyticsPref = privacyPreferences.analytics()
val analytics by analyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
granted = analytics,
onToggleChange = analyticsPref::set,
)
} }
} }
@@ -160,7 +127,7 @@ internal class PermissionStep : OnboardingStep {
} }
@Composable @Composable
private fun PermissionCheckbox( private fun PermissionItem(
title: String, title: String,
subtitle: String, subtitle: String,
granted: Boolean, granted: Boolean,
@@ -190,26 +157,4 @@ internal class PermissionStep : OnboardingStep {
colors = ListItemDefaults.colors(containerColor = Color.Transparent), colors = ListItemDefaults.colors(containerColor = Color.Transparent),
) )
} }
@Composable
private fun PermissionSwitch(
title: String,
subtitle: String,
granted: Boolean,
modifier: Modifier = Modifier,
onToggleChange: (Boolean) -> Unit,
) {
ListItem(
modifier = modifier,
headlineContent = { Text(text = title) },
supportingContent = { Text(text = subtitle) },
trailingContent = {
Switch(
checked = granted,
onCheckedChange = onToggleChange,
)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
} }

View File

@@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings package eu.kanade.presentation.more.settings
import androidx.annotation.IntRange
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@@ -18,7 +17,7 @@ sealed class Preference {
sealed class PreferenceItem<T> : Preference() { sealed class PreferenceItem<T> : Preference() {
abstract val subtitle: String? abstract val subtitle: String?
abstract val icon: ImageVector? abstract val icon: ImageVector?
abstract val onValueChanged: suspend (value: T) -> Boolean abstract val onValueChanged: suspend (newValue: T) -> Boolean
/** /**
* A basic [PreferenceItem] that only displays texts. * A basic [PreferenceItem] that only displays texts.
@@ -26,58 +25,57 @@ sealed class Preference {
data class TextPreference( data class TextPreference(
override val title: String, override val title: String,
override val subtitle: String? = null, override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val onClick: (() -> Unit)? = null, val onClick: (() -> Unit)? = null,
) : PreferenceItem<String>() { ) : PreferenceItem<String>()
override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Boolean = { true }
}
/** /**
* A [PreferenceItem] that provides a two-state toggleable option. * A [PreferenceItem] that provides a two-state toggleable option.
*/ */
data class SwitchPreference( data class SwitchPreference(
val preference: PreferenceData<Boolean>, val pref: PreferenceData<Boolean>,
override val title: String, override val title: String,
override val subtitle: String? = null, override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true }, override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true },
) : PreferenceItem<Boolean>() { ) : PreferenceItem<Boolean>()
override val icon: ImageVector? = null
}
/** /**
* A [PreferenceItem] that provides a slider to select an integer number. * A [PreferenceItem] that provides a slider to select an integer number.
*/ */
data class SliderPreference( data class SliderPreference(
val value: Int, val value: Int,
override val title: String, val min: Int = 0,
val valueRange: IntProgression = 0..1, val max: Int,
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 }, override val title: String = "",
override val subtitle: String? = null, override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Int) -> Boolean = { true }, override val onValueChanged: suspend (newValue: Int) -> Boolean = { true },
) : PreferenceItem<Int>() { ) : PreferenceItem<Int>()
override val icon: ImageVector? = null
}
/** /**
* A [PreferenceItem] that displays a list of entries as a dialog. * A [PreferenceItem] that displays a list of entries as a dialog.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
data class ListPreference<T>( data class ListPreference<T>(
val preference: PreferenceData<T>, val pref: PreferenceData<T>,
val entries: ImmutableMap<T, String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? = val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) }, { v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: T) -> Boolean = { true }, override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
val entries: ImmutableMap<T, String>,
) : PreferenceItem<T>() { ) : PreferenceItem<T>() {
internal fun internalSet(value: Any) = preference.set(value as T) internal fun internalSet(newValue: Any) = pref.set(newValue as T)
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T) internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
@Composable @Composable
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) = internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
@@ -89,14 +87,15 @@ sealed class Preference {
*/ */
data class BasicListPreference( data class BasicListPreference(
val value: String, val value: String,
val entries: ImmutableMap<String, String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? = val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) }, { v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: String) -> Boolean = { true }, override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val entries: ImmutableMap<String, String>,
) : PreferenceItem<String>() ) : PreferenceItem<String>()
/** /**
@@ -104,51 +103,52 @@ sealed class Preference {
* Multiple entries can be selected at the same time. * Multiple entries can be selected at the same time.
*/ */
data class MultiSelectListPreference( data class MultiSelectListPreference(
val preference: PreferenceData<Set<String>>, val pref: PreferenceData<Set<String>>,
val entries: ImmutableMap<String, String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: Set<String>, entries: ImmutableMap<String, String>) -> String? = val subtitleProvider: @Composable (
{ v, e -> value: Set<String>,
val combined = remember(v, e) { entries: ImmutableMap<String, String>,
v.mapNotNull { e[it] } ) -> String? = { v, e ->
.joinToString() val combined = remember(v) {
.takeUnless { it.isBlank() } v.map { e[it] }
} .takeIf { it.isNotEmpty() }
?: stringResource(MR.strings.none) ?.joinToString()
} ?: stringResource(MR.strings.none)
subtitle?.format(combined) subtitle?.format(combined)
}, },
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true }, override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
val entries: ImmutableMap<String, String>,
) : PreferenceItem<Set<String>>() ) : PreferenceItem<Set<String>>()
/** /**
* A [PreferenceItem] that shows a EditText in the dialog. * A [PreferenceItem] that shows a EditText in the dialog.
*/ */
data class EditTextPreference( data class EditTextPreference(
val preference: PreferenceData<String>, val pref: PreferenceData<String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: String) -> Boolean = { true }, override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
) : PreferenceItem<String>() { ) : PreferenceItem<String>()
override val icon: ImageVector? = null
}
/** /**
* A [PreferenceItem] for individual tracker. * A [PreferenceItem] for individual tracker.
*/ */
data class TrackerPreference( data class TrackerPreference(
val tracker: Tracker, val tracker: Tracker,
override val title: String,
val login: () -> Unit, val login: () -> Unit,
val logout: () -> Unit, val logout: () -> Unit,
) : PreferenceItem<String>() { ) : PreferenceItem<String>() {
override val title: String = ""
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = null override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Boolean = { true } override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
} }
data class InfoPreference( data class InfoPreference(
@@ -157,17 +157,17 @@ sealed class Preference {
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = null override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Boolean = { true } override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
} }
data class CustomPreference( data class CustomPreference(
override val title: String, override val title: String,
val content: @Composable () -> Unit, val content: @Composable (PreferenceItem<String>) -> Unit,
) : PreferenceItem<Unit>() { ) : PreferenceItem<String>() {
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = null override val icon: ImageVector? = null
override val onValueChanged: suspend (value: Unit) -> Boolean = { true } override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
} }
} }

View File

@@ -5,8 +5,6 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -14,20 +12,16 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.InfoWidget import eu.kanade.presentation.more.settings.widget.InfoWidget
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.more.settings.widget.PrefsVerticalPadding
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TitleFontSize
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.BaseSliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false } val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
@@ -66,7 +60,7 @@ internal fun PreferenceItem(
) { ) {
when (item) { when (item) {
is Preference.PreferenceItem.SwitchPreference -> { is Preference.PreferenceItem.SwitchPreference -> {
val value by item.preference.collectAsState() val value by item.pref.collectAsState()
SwitchPreferenceWidget( SwitchPreferenceWidget(
title = item.title, title = item.title,
subtitle = item.subtitle, subtitle = item.subtitle,
@@ -75,33 +69,29 @@ internal fun PreferenceItem(
onCheckedChanged = { newValue -> onCheckedChanged = { newValue ->
scope.launch { scope.launch {
if (item.onValueChanged(newValue)) { if (item.onValueChanged(newValue)) {
item.preference.set(newValue) item.pref.set(newValue)
} }
} }
}, },
) )
} }
is Preference.PreferenceItem.SliderPreference -> { is Preference.PreferenceItem.SliderPreference -> {
BaseSliderItem( // TODO: use different composable?
SliderItem(
label = item.title, label = item.title,
min = item.min,
max = item.max,
value = item.value, value = item.value,
valueRange = item.valueRange,
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(), valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
steps = item.steps,
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
onChange = { onChange = {
scope.launch { scope.launch {
item.onValueChanged(it) item.onValueChanged(it)
} }
}, },
modifier = Modifier.padding(
horizontal = PrefsHorizontalPadding,
vertical = PrefsVerticalPadding,
),
) )
} }
is Preference.PreferenceItem.ListPreference<*> -> { is Preference.PreferenceItem.ListPreference<*> -> {
val value by item.preference.collectAsState() val value by item.pref.collectAsState()
ListPreferenceWidget( ListPreferenceWidget(
value = value, value = value,
title = item.title, title = item.title,
@@ -128,14 +118,14 @@ internal fun PreferenceItem(
) )
} }
is Preference.PreferenceItem.MultiSelectListPreference -> { is Preference.PreferenceItem.MultiSelectListPreference -> {
val values by item.preference.collectAsState() val values by item.pref.collectAsState()
MultiSelectListPreferenceWidget( MultiSelectListPreferenceWidget(
preference = item, preference = item,
values = values, values = values,
onValuesChange = { newValues -> onValuesChange = { newValues ->
scope.launch { scope.launch {
if (item.onValueChanged(newValues)) { if (item.onValueChanged(newValues)) {
item.preference.set(newValues.toMutableSet()) item.pref.set(newValues.toMutableSet())
} }
} }
}, },
@@ -150,7 +140,7 @@ internal fun PreferenceItem(
) )
} }
is Preference.PreferenceItem.EditTextPreference -> { is Preference.PreferenceItem.EditTextPreference -> {
val values by item.preference.collectAsState() val values by item.pref.collectAsState()
EditTextPreferenceWidget( EditTextPreferenceWidget(
title = item.title, title = item.title,
subtitle = item.subtitle, subtitle = item.subtitle,
@@ -158,7 +148,7 @@ internal fun PreferenceItem(
value = values, value = values,
onConfirm = { onConfirm = {
val accepted = item.onValueChanged(it) val accepted = item.onValueChanged(it)
if (accepted) item.preference.set(it) if (accepted) item.pref.set(it)
accepted accepted
}, },
) )
@@ -177,7 +167,7 @@ internal fun PreferenceItem(
InfoWidget(text = item.title) InfoWidget(text = item.title)
} }
is Preference.PreferenceItem.CustomPreference -> { is Preference.PreferenceItem.CustomPreference -> {
item.content() item.content(item)
} }
} }
} }

View File

@@ -47,8 +47,8 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
import eu.kanade.tachiyomi.ui.more.OnboardingScreen import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isShizukuInstalled import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
@@ -61,7 +61,6 @@ import logcat.LogPriority
import okhttp3.Headers import okhttp3.Headers
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -97,7 +96,7 @@ object SettingsAdvancedScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = networkPreferences.verboseLogging(), pref = networkPreferences.verboseLogging(),
title = stringResource(MR.strings.pref_verbose_logging), title = stringResource(MR.strings.pref_verbose_logging),
subtitle = stringResource(MR.strings.pref_verbose_logging_summary), subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
onValueChanged = { onValueChanged = {
@@ -236,7 +235,8 @@ object SettingsAdvancedScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = networkPreferences.dohProvider(), pref = networkPreferences.dohProvider(),
title = stringResource(MR.strings.pref_dns_over_https),
entries = persistentMapOf( entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled), -1 to stringResource(MR.strings.disabled),
PREF_DOH_CLOUDFLARE to "Cloudflare", PREF_DOH_CLOUDFLARE to "Cloudflare",
@@ -252,14 +252,13 @@ object SettingsAdvancedScreen : SearchableSettings {
PREF_DOH_NJALLA to "Njalla", PREF_DOH_NJALLA to "Njalla",
PREF_DOH_SHECAN to "Shecan", PREF_DOH_SHECAN to "Shecan",
), ),
title = stringResource(MR.strings.pref_dns_over_https),
onValueChanged = { onValueChanged = {
context.toast(MR.strings.requires_app_restart) context.toast(MR.strings.requires_app_restart)
true true
}, },
), ),
Preference.PreferenceItem.EditTextPreference( Preference.PreferenceItem.EditTextPreference(
preference = userAgentPref, pref = userAgentPref,
title = stringResource(MR.strings.pref_user_agent_string), title = stringResource(MR.strings.pref_user_agent_string),
onValueChanged = { onValueChanged = {
try { try {
@@ -335,31 +334,6 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_reader), title = stringResource(MR.strings.pref_category_reader),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
preference = basePreferences.hardwareBitmapThreshold(),
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
.mapIndexed { index, option ->
val display = if (index == 0) {
stringResource(MR.strings.pref_hardware_bitmap_threshold_default, option)
} else {
option.toString()
}
option to display
}
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
subtitleProvider = { value, options ->
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
},
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
),
Preference.PreferenceItem.SwitchPreference(
preference = basePreferences.alwaysDecodeLongStripWithSSIV(),
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2),
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_display_profile), title = stringResource(MR.strings.pref_display_profile),
subtitle = basePreferences.displayProfile().get(), subtitle = basePreferences.displayProfile().get(),
@@ -408,19 +382,19 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.label_extensions), title = stringResource(MR.strings.label_extensions),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = extensionInstallerPref, pref = extensionInstallerPref,
title = stringResource(MR.strings.ext_installer_pref),
entries = extensionInstallerPref.entries entries = extensionInstallerPref.entries
.filter { .filter {
// TODO: allow private option in stable versions once URL handling is more fleshed out // TODO: allow private option in stable versions once URL handling is more fleshed out
if (isReleaseBuildType) { if (isPreviewBuildType || isDevFlavor) {
it != BasePreferences.ExtensionInstaller.PRIVATE
} else {
true true
} else {
it != BasePreferences.ExtensionInstaller.PRIVATE
} }
} }
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.ext_installer_pref),
onValueChanged = { onValueChanged = {
if (it == BasePreferences.ExtensionInstaller.SHIZUKU && if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
!context.isShizukuInstalled !context.isShizukuInstalled

View File

@@ -82,7 +82,7 @@ object SettingsAppearanceScreen : SearchableSettings {
} }
}, },
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = amoledPref, pref = amoledPref,
title = stringResource(MR.strings.pref_dark_theme_pure_black), title = stringResource(MR.strings.pref_dark_theme_pure_black),
enabled = themeMode != ThemeMode.LIGHT, enabled = themeMode != ThemeMode.LIGHT,
onValueChanged = { onValueChanged = {
@@ -116,28 +116,28 @@ object SettingsAppearanceScreen : SearchableSettings {
onClick = { navigator.push(AppLanguageScreen()) }, onClick = { navigator.push(AppLanguageScreen()) },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = uiPreferences.tabletUiMode(), pref = uiPreferences.tabletUiMode(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
entries = TabletUiMode.entries entries = TabletUiMode.entries
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
onValueChanged = { onValueChanged = {
context.toast(MR.strings.requires_app_restart) context.toast(MR.strings.requires_app_restart)
true true
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = uiPreferences.dateFormat(), pref = uiPreferences.dateFormat(),
title = stringResource(MR.strings.pref_date_format),
entries = DateFormats entries = DateFormats
.associateWith { .associateWith {
val formattedDate = UiPreferences.dateFormat(it).format(now) val formattedDate = UiPreferences.dateFormat(it).format(now)
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)" "${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
} }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_date_format),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = uiPreferences.relativeTime(), pref = uiPreferences.relativeTime(),
title = stringResource(MR.strings.pref_relative_format), title = stringResource(MR.strings.pref_relative_format),
subtitle = stringResource( subtitle = stringResource(
MR.strings.pref_relative_format_summary, MR.strings.pref_relative_format_summary,

View File

@@ -43,7 +43,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.label_sources), title = stringResource(MR.strings.label_sources),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = sourcePreferences.hideInLibraryItems(), pref = sourcePreferences.hideInLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_library_items), title = stringResource(MR.strings.pref_hide_in_library_items),
), ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
@@ -59,7 +59,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_nsfw_content), title = stringResource(MR.strings.pref_category_nsfw_content),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = sourcePreferences.showNsfwSource(), pref = sourcePreferences.showNsfwSource(),
title = stringResource(MR.strings.pref_show_nsfw_source), title = stringResource(MR.strings.pref_show_nsfw_source),
subtitle = stringResource(MR.strings.requires_app_restart), subtitle = stringResource(MR.strings.requires_app_restart),
onValueChanged = { onValueChanged = {

View File

@@ -7,9 +7,7 @@ import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -17,8 +15,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MultiChoiceSegmentedButtonRow import androidx.compose.material3.MultiChoiceSegmentedButtonRow
@@ -26,15 +22,12 @@ import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
@@ -52,14 +45,10 @@ import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.export.LibraryExporter
import eu.kanade.tachiyomi.data.export.LibraryExporter.ExportOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath import tachiyomi.core.common.storage.displayablePath
@@ -68,11 +57,8 @@ import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -109,7 +95,6 @@ object SettingsDataScreen : SearchableSettings {
getBackupAndRestoreGroup(backupPreferences = backupPreferences), getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(), getDataGroup(),
getExportGroup(),
) )
} }
@@ -254,7 +239,8 @@ object SettingsDataScreen : SearchableSettings {
// Automatic backups // Automatic backups
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = backupPreferences.backupInterval(), pref = backupPreferences.backupInterval(),
title = stringResource(MR.strings.pref_backup_interval),
entries = persistentMapOf( entries = persistentMapOf(
0 to stringResource(MR.strings.off), 0 to stringResource(MR.strings.off),
6 to stringResource(MR.strings.update_6hour), 6 to stringResource(MR.strings.update_6hour),
@@ -263,7 +249,6 @@ object SettingsDataScreen : SearchableSettings {
48 to stringResource(MR.strings.update_48hour), 48 to stringResource(MR.strings.update_48hour),
168 to stringResource(MR.strings.update_weekly), 168 to stringResource(MR.strings.update_weekly),
), ),
title = stringResource(MR.strings.pref_backup_interval),
onValueChanged = { onValueChanged = {
BackupCreateJob.setupTask(context, it) BackupCreateJob.setupTask(context, it)
true true
@@ -321,147 +306,10 @@ object SettingsDataScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.autoClearChapterCache(), pref = libraryPreferences.autoClearChapterCache(),
title = stringResource(MR.strings.pref_auto_clear_chapter_cache), title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
), ),
), ),
) )
} }
@Composable
private fun getExportGroup(): Preference.PreferenceGroup {
var showDialog by remember { mutableStateOf(false) }
var exportOptions by remember {
mutableStateOf(
ExportOptions(
includeTitle = true,
includeAuthor = true,
includeArtist = true,
),
)
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
val getFavorites = remember { Injekt.get<GetFavorites>() }
var favorites by remember { mutableStateOf<List<Manga>>(emptyList()) }
LaunchedEffect(Unit) {
favorites = getFavorites.await()
}
val saveFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("text/csv"),
) { uri ->
uri?.let {
scope.launch {
LibraryExporter.exportToCsv(
context = context,
uri = it,
favorites = favorites,
options = exportOptions,
onExportComplete = {
scope.launch(Dispatchers.Main) {
context.toast(MR.strings.library_exported)
}
},
)
}
}
}
if (showDialog) {
ColumnSelectionDialog(
options = exportOptions,
onConfirm = { options ->
exportOptions = options
saveFileLauncher.launch("mihon_library.csv")
},
onDismissRequest = { showDialog = false },
)
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.export),
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.library_list),
onClick = { showDialog = true },
),
),
)
}
@Composable
private fun ColumnSelectionDialog(
options: ExportOptions,
onConfirm: (ExportOptions) -> Unit,
onDismissRequest: () -> Unit,
) {
var titleSelected by remember { mutableStateOf(options.includeTitle) }
var authorSelected by remember { mutableStateOf(options.includeAuthor) }
var artistSelected by remember { mutableStateOf(options.includeArtist) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = {
Text(text = stringResource(MR.strings.migration_dialog_what_to_include))
},
text = {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = titleSelected,
onCheckedChange = { checked ->
titleSelected = checked
if (!checked) {
authorSelected = false
artistSelected = false
}
},
)
Text(text = stringResource(MR.strings.title))
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = authorSelected,
onCheckedChange = { authorSelected = it },
enabled = titleSelected,
)
Text(text = stringResource(MR.strings.author))
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = artistSelected,
onCheckedChange = { artistSelected = it },
enabled = titleSelected,
)
Text(text = stringResource(MR.strings.artist))
}
}
},
confirmButton = {
TextButton(
onClick = {
onConfirm(
ExportOptions(
includeTitle = titleSelected,
includeAuthor = authorSelected,
includeArtist = artistSelected,
),
)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_save))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
)
}
} }

View File

@@ -15,6 +15,7 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
@@ -34,20 +35,20 @@ object SettingsDownloadScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetCategories>() } val getCategories = remember { Injekt.get<GetCategories>() }
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList()) val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() })
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() } val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
return listOf( return listOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.downloadOnlyOverWifi(), pref = downloadPreferences.downloadOnlyOverWifi(),
title = stringResource(MR.strings.connected_to_wifi), title = stringResource(MR.strings.connected_to_wifi),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.saveChaptersAsCBZ(), pref = downloadPreferences.saveChaptersAsCBZ(),
title = stringResource(MR.strings.save_chapter_as_cbz), title = stringResource(MR.strings.save_chapter_as_cbz),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.splitTallImages(), pref = downloadPreferences.splitTallImages(),
title = stringResource(MR.strings.split_tall_images), title = stringResource(MR.strings.split_tall_images),
subtitle = stringResource(MR.strings.split_tall_images_summary), subtitle = stringResource(MR.strings.split_tall_images_summary),
), ),
@@ -72,11 +73,12 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_delete_chapters), title = stringResource(MR.strings.pref_category_delete_chapters),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.removeAfterMarkedAsRead(), pref = downloadPreferences.removeAfterMarkedAsRead(),
title = stringResource(MR.strings.pref_remove_after_marked_as_read), title = stringResource(MR.strings.pref_remove_after_marked_as_read),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = downloadPreferences.removeAfterReadSlots(), pref = downloadPreferences.removeAfterReadSlots(),
title = stringResource(MR.strings.pref_remove_after_read),
entries = persistentMapOf( entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled), -1 to stringResource(MR.strings.disabled),
0 to stringResource(MR.strings.last_read_chapter), 0 to stringResource(MR.strings.last_read_chapter),
@@ -85,10 +87,9 @@ object SettingsDownloadScreen : SearchableSettings {
3 to stringResource(MR.strings.fourth_to_last), 3 to stringResource(MR.strings.fourth_to_last),
4 to stringResource(MR.strings.fifth_to_last), 4 to stringResource(MR.strings.fifth_to_last),
), ),
title = stringResource(MR.strings.pref_remove_after_read),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.removeBookmarkedChapters(), pref = downloadPreferences.removeBookmarkedChapters(),
title = stringResource(MR.strings.pref_remove_bookmarked_chapters), title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
), ),
getExcludedCategoriesPreference( getExcludedCategoriesPreference(
@@ -105,11 +106,11 @@ object SettingsDownloadScreen : SearchableSettings {
categories: () -> List<Category>, categories: () -> List<Category>,
): Preference.PreferenceItem.MultiSelectListPreference { ): Preference.PreferenceItem.MultiSelectListPreference {
return Preference.PreferenceItem.MultiSelectListPreference( return Preference.PreferenceItem.MultiSelectListPreference(
preference = downloadPreferences.removeExcludeCategories(), pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
entries = categories() entries = categories()
.associate { it.id.toString() to it.visualName } .associate { it.id.toString() to it.visualName }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
) )
} }
@@ -149,11 +150,11 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_auto_download), title = stringResource(MR.strings.pref_category_auto_download),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadNewChaptersPref, pref = downloadNewChaptersPref,
title = stringResource(MR.strings.pref_download_new), title = stringResource(MR.strings.pref_download_new),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = downloadNewUnreadChaptersOnlyPref, pref = downloadNewUnreadChaptersOnlyPref,
title = stringResource(MR.strings.pref_download_new_unread_chapters_only), title = stringResource(MR.strings.pref_download_new_unread_chapters_only),
enabled = downloadNewChapters, enabled = downloadNewChapters,
), ),
@@ -164,8 +165,8 @@ object SettingsDownloadScreen : SearchableSettings {
included = included, included = included,
excluded = excluded, excluded = excluded,
), ),
enabled = downloadNewChapters,
onClick = { showDialog = true }, onClick = { showDialog = true },
enabled = downloadNewChapters,
), ),
), ),
) )
@@ -179,7 +180,8 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.download_ahead), title = stringResource(MR.strings.download_ahead),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = downloadPreferences.autoDownloadWhileReading(), pref = downloadPreferences.autoDownloadWhileReading(),
title = stringResource(MR.strings.auto_download_while_reading),
entries = listOf(0, 2, 3, 5, 10) entries = listOf(0, 2, 3, 5, 10)
.associateWith { .associateWith {
if (it == 0) { if (it == 0) {
@@ -189,7 +191,6 @@ object SettingsDownloadScreen : SearchableSettings {
} }
} }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.auto_download_while_reading),
), ),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
), ),

View File

@@ -24,6 +24,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags import tachiyomi.domain.category.interactor.ResetCategoryFlags
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@@ -35,8 +36,6 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_EXISTING
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_NEW
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -54,12 +53,12 @@ object SettingsLibraryScreen : SearchableSettings {
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetCategories>() } val getCategories = remember { Injekt.get<GetCategories>() }
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() } val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList()) val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() })
return listOf( return listOf(
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences), getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),
getGlobalUpdateGroup(allCategories, libraryPreferences), getGlobalUpdateGroup(allCategories, libraryPreferences),
getBehaviorGroup(libraryPreferences), getChapterSwipeActionsGroup(libraryPreferences),
) )
} }
@@ -91,12 +90,12 @@ object SettingsLibraryScreen : SearchableSettings {
onClick = { navigator.push(CategoryScreen()) }, onClick = { navigator.push(CategoryScreen()) },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = libraryPreferences.defaultCategory(), pref = libraryPreferences.defaultCategory(),
entries = ids.zip(labels).toMap().toImmutableMap(),
title = stringResource(MR.strings.default_category), title = stringResource(MR.strings.default_category),
entries = ids.zip(labels).toMap().toImmutableMap(),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.categorizedDisplaySettings(), pref = libraryPreferences.categorizedDisplaySettings(),
title = stringResource(MR.strings.categorized_display_settings), title = stringResource(MR.strings.categorized_display_settings),
onValueChanged = { onValueChanged = {
if (!it) { if (!it) {
@@ -148,7 +147,8 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_library_update), title = stringResource(MR.strings.pref_category_library_update),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = autoUpdateIntervalPref, pref = autoUpdateIntervalPref,
title = stringResource(MR.strings.pref_library_update_interval),
entries = persistentMapOf( entries = persistentMapOf(
0 to stringResource(MR.strings.update_never), 0 to stringResource(MR.strings.update_never),
12 to stringResource(MR.strings.update_12hour), 12 to stringResource(MR.strings.update_12hour),
@@ -157,22 +157,21 @@ object SettingsLibraryScreen : SearchableSettings {
72 to stringResource(MR.strings.update_72hour), 72 to stringResource(MR.strings.update_72hour),
168 to stringResource(MR.strings.update_weekly), 168 to stringResource(MR.strings.update_weekly),
), ),
title = stringResource(MR.strings.pref_library_update_interval),
onValueChanged = { onValueChanged = {
LibraryUpdateJob.setupTask(context, it) LibraryUpdateJob.setupTask(context, it)
true true
}, },
), ),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
preference = libraryPreferences.autoUpdateDeviceRestrictions(), pref = libraryPreferences.autoUpdateDeviceRestrictions(),
enabled = autoUpdateInterval > 0,
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
entries = persistentMapOf( entries = persistentMapOf(
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi), DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered), DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
DEVICE_CHARGING to stringResource(MR.strings.charging), DEVICE_CHARGING to stringResource(MR.strings.charging),
), ),
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
enabled = autoUpdateInterval > 0,
onValueChanged = { onValueChanged = {
// Post to event looper to allow the preference to be updated. // Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) } ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
@@ -189,22 +188,22 @@ object SettingsLibraryScreen : SearchableSettings {
onClick = { showCategoriesDialog = true }, onClick = { showCategoriesDialog = true },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.autoUpdateMetadata(), pref = libraryPreferences.autoUpdateMetadata(),
title = stringResource(MR.strings.pref_library_update_refresh_metadata), title = stringResource(MR.strings.pref_library_update_refresh_metadata),
subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary), subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary),
), ),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
preference = libraryPreferences.autoUpdateMangaRestrictions(), pref = libraryPreferences.autoUpdateMangaRestrictions(),
title = stringResource(MR.strings.pref_library_update_smart_update),
entries = persistentMapOf( entries = persistentMapOf(
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read), MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started), MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed), MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period), MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
), ),
title = stringResource(MR.strings.pref_library_update_smart_update),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.newShowUpdatesCount(), pref = libraryPreferences.newShowUpdatesCount(),
title = stringResource(MR.strings.pref_library_update_show_tab_badge), title = stringResource(MR.strings.pref_library_update_show_tab_badge),
), ),
), ),
@@ -212,14 +211,15 @@ object SettingsLibraryScreen : SearchableSettings {
} }
@Composable @Composable
private fun getBehaviorGroup( private fun getChapterSwipeActionsGroup(
libraryPreferences: LibraryPreferences, libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_behavior), title = stringResource(MR.strings.pref_chapter_swipe),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = libraryPreferences.swipeToStartAction(), pref = libraryPreferences.swipeToStartAction(),
title = stringResource(MR.strings.pref_chapter_swipe_start),
entries = persistentMapOf( entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled), stringResource(MR.strings.disabled),
@@ -230,10 +230,10 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download), stringResource(MR.strings.action_download),
), ),
title = stringResource(MR.strings.pref_chapter_swipe_start),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = libraryPreferences.swipeToEndAction(), pref = libraryPreferences.swipeToEndAction(),
title = stringResource(MR.strings.pref_chapter_swipe_end),
entries = persistentMapOf( entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled), stringResource(MR.strings.disabled),
@@ -244,17 +244,6 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download), stringResource(MR.strings.action_download),
), ),
title = stringResource(MR.strings.pref_chapter_swipe_end),
),
Preference.PreferenceItem.MultiSelectListPreference(
preference = libraryPreferences.markDuplicateReadChapterAsRead(),
entries = persistentMapOf(
MARK_DUPLICATE_CHAPTER_READ_EXISTING to
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_existing),
MARK_DUPLICATE_CHAPTER_READ_NEW to
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_new),
),
title = stringResource(MR.strings.pref_mark_duplicate_read_chapter_read),
), ),
), ),
) )

View File

@@ -33,33 +33,33 @@ object SettingsReaderScreen : SearchableSettings {
return listOf( return listOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPref.defaultReadingMode(), pref = readerPref.defaultReadingMode(),
title = stringResource(MR.strings.pref_viewer_type),
entries = ReadingMode.entries.drop(1) entries = ReadingMode.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) } .associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_type),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPref.doubleTapAnimSpeed(), pref = readerPref.doubleTapAnimSpeed(),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
entries = persistentMapOf( entries = persistentMapOf(
1 to stringResource(MR.strings.double_tap_anim_speed_0), 1 to stringResource(MR.strings.double_tap_anim_speed_0),
500 to stringResource(MR.strings.double_tap_anim_speed_normal), 500 to stringResource(MR.strings.double_tap_anim_speed_normal),
250 to stringResource(MR.strings.double_tap_anim_speed_fast), 250 to stringResource(MR.strings.double_tap_anim_speed_fast),
), ),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPref.showReadingMode(), pref = readerPref.showReadingMode(),
title = stringResource(MR.strings.pref_show_reading_mode), title = stringResource(MR.strings.pref_show_reading_mode),
subtitle = stringResource(MR.strings.pref_show_reading_mode_summary), subtitle = stringResource(MR.strings.pref_show_reading_mode_summary),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPref.showNavigationOverlayOnStart(), pref = readerPref.showNavigationOverlayOnStart(),
title = stringResource(MR.strings.pref_show_navigation_mode), title = stringResource(MR.strings.pref_show_navigation_mode),
subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary), subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPref.pageTransitions(), pref = readerPref.pageTransitions(),
title = stringResource(MR.strings.pref_page_transitions), title = stringResource(MR.strings.pref_page_transitions),
), ),
getDisplayGroup(readerPreferences = readerPref), getDisplayGroup(readerPreferences = readerPref),
@@ -80,39 +80,39 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_display), title = stringResource(MR.strings.pref_category_display),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.defaultOrientationType(), pref = readerPreferences.defaultOrientationType(),
title = stringResource(MR.strings.pref_rotation_type),
entries = ReaderOrientation.entries.drop(1) entries = ReaderOrientation.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) } .associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_rotation_type),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.readerTheme(), pref = readerPreferences.readerTheme(),
title = stringResource(MR.strings.pref_reader_theme),
entries = persistentMapOf( entries = persistentMapOf(
1 to stringResource(MR.strings.black_background), 1 to stringResource(MR.strings.black_background),
2 to stringResource(MR.strings.gray_background), 2 to stringResource(MR.strings.gray_background),
0 to stringResource(MR.strings.white_background), 0 to stringResource(MR.strings.white_background),
3 to stringResource(MR.strings.automatic_background), 3 to stringResource(MR.strings.automatic_background),
), ),
title = stringResource(MR.strings.pref_reader_theme),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = fullscreenPref, pref = fullscreenPref,
title = stringResource(MR.strings.pref_fullscreen), title = stringResource(MR.strings.pref_fullscreen),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.cutoutShort(), pref = readerPreferences.cutoutShort(),
title = stringResource(MR.strings.pref_cutout_short), title = stringResource(MR.strings.pref_cutout_short),
enabled = fullscreen && enabled = fullscreen &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.keepScreenOn(), pref = readerPreferences.keepScreenOn(),
title = stringResource(MR.strings.pref_keep_screen_on), title = stringResource(MR.strings.pref_keep_screen_on),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.showPageNumber(), pref = readerPreferences.showPageNumber(),
title = stringResource(MR.strings.pref_show_page_number), title = stringResource(MR.strings.pref_show_page_number),
), ),
), ),
@@ -135,41 +135,43 @@ object SettingsReaderScreen : SearchableSettings {
title = "E-Ink", title = "E-Ink",
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.flashOnPageChange(), pref = readerPreferences.flashOnPageChange(),
title = stringResource(MR.strings.pref_flash_page), title = stringResource(MR.strings.pref_flash_page),
subtitle = stringResource(MR.strings.pref_flash_page_summ), subtitle = stringResource(MR.strings.pref_flash_page_summ),
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION, value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15, min = 1,
max = 15,
title = stringResource(MR.strings.pref_flash_duration), title = stringResource(MR.strings.pref_flash_duration),
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
enabled = flashPageState,
onValueChanged = { onValueChanged = {
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
true true
}, },
enabled = flashPageState,
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = flashInterval, value = flashInterval,
valueRange = 1..10, min = 1,
max = 10,
title = stringResource(MR.strings.pref_flash_page_interval), title = stringResource(MR.strings.pref_flash_page_interval),
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
enabled = flashPageState,
onValueChanged = { onValueChanged = {
flashIntervalPref.set(it) flashIntervalPref.set(it)
true true
}, },
enabled = flashPageState,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = flashColorPref, pref = flashColorPref,
title = stringResource(MR.strings.pref_flash_with),
entries = persistentMapOf( entries = persistentMapOf(
ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black), ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black),
ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white), ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white),
ReaderPreferences.FlashColor.WHITE_BLACK ReaderPreferences.FlashColor.WHITE_BLACK
to stringResource(MR.strings.pref_flash_style_white_black), to stringResource(MR.strings.pref_flash_style_white_black),
), ),
title = stringResource(MR.strings.pref_flash_with),
enabled = flashPageState, enabled = flashPageState,
), ),
), ),
@@ -182,19 +184,19 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_reading), title = stringResource(MR.strings.pref_category_reading),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.skipRead(), pref = readerPreferences.skipRead(),
title = stringResource(MR.strings.pref_skip_read_chapters), title = stringResource(MR.strings.pref_skip_read_chapters),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.skipFiltered(), pref = readerPreferences.skipFiltered(),
title = stringResource(MR.strings.pref_skip_filtered_chapters), title = stringResource(MR.strings.pref_skip_filtered_chapters),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.skipDupe(), pref = readerPreferences.skipDupe(),
title = stringResource(MR.strings.pref_skip_dupe_chapters), title = stringResource(MR.strings.pref_skip_dupe_chapters),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.alwaysShowChapterTransition(), pref = readerPreferences.alwaysShowChapterTransition(),
title = stringResource(MR.strings.pref_always_show_chapter_transition), title = stringResource(MR.strings.pref_always_show_chapter_transition),
), ),
), ),
@@ -217,15 +219,16 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pager_viewer), title = stringResource(MR.strings.pager_viewer),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = navModePref, pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) } .mapIndexed { index, it -> index to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.pagerNavInverted(), pref = readerPreferences.pagerNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = persistentListOf( entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE, ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL, ReaderPreferences.TappingInvertMode.HORIZONTAL,
@@ -234,41 +237,40 @@ object SettingsReaderScreen : SearchableSettings {
) )
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = imageScaleTypePref, pref = imageScaleTypePref,
title = stringResource(MR.strings.pref_image_scale_type),
entries = ReaderPreferences.ImageScaleType entries = ReaderPreferences.ImageScaleType
.mapIndexed { index, it -> index + 1 to stringResource(it) } .mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_image_scale_type),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.zoomStart(), pref = readerPreferences.zoomStart(),
title = stringResource(MR.strings.pref_zoom_start),
entries = ReaderPreferences.ZoomStart entries = ReaderPreferences.ZoomStart
.mapIndexed { index, it -> index + 1 to stringResource(it) } .mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_zoom_start),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.cropBorders(), pref = readerPreferences.cropBorders(),
title = stringResource(MR.strings.pref_crop_borders), title = stringResource(MR.strings.pref_crop_borders),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.landscapeZoom(), pref = readerPreferences.landscapeZoom(),
title = stringResource(MR.strings.pref_landscape_zoom), title = stringResource(MR.strings.pref_landscape_zoom),
enabled = imageScaleType == 1, enabled = imageScaleType == 1,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.navigateToPan(), pref = readerPreferences.navigateToPan(),
title = stringResource(MR.strings.pref_navigate_pan), title = stringResource(MR.strings.pref_navigate_pan),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = dualPageSplitPref, pref = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split), title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = { onValueChanged = {
rotateToFitPref.set(false) rotateToFitPref.set(false)
@@ -276,13 +278,13 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.dualPageInvertPaged(), pref = readerPreferences.dualPageInvertPaged(),
title = stringResource(MR.strings.pref_dual_page_invert), title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit, enabled = dualPageSplit,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = rotateToFitPref, pref = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate), title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = { onValueChanged = {
dualPageSplitPref.set(false) dualPageSplitPref.set(false)
@@ -290,7 +292,7 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.dualPageRotateToFitInvert(), pref = readerPreferences.dualPageRotateToFitInvert(),
title = stringResource(MR.strings.pref_page_rotate_invert), title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit, enabled = rotateToFit,
), ),
@@ -316,15 +318,16 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.webtoon_viewer), title = stringResource(MR.strings.webtoon_viewer),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = navModePref, pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) } .mapIndexed { index, it -> index to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.webtoonNavInverted(), pref = readerPreferences.webtoonNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = persistentListOf( entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE, ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL, ReaderPreferences.TappingInvertMode.HORIZONTAL,
@@ -333,37 +336,35 @@ object SettingsReaderScreen : SearchableSettings {
) )
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = webtoonSidePadding, value = webtoonSidePadding,
valueRange = ReaderPreferences.let {
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
},
title = stringResource(MR.strings.pref_webtoon_side_padding), title = stringResource(MR.strings.pref_webtoon_side_padding),
subtitle = numberFormat.format(webtoonSidePadding / 100f), subtitle = numberFormat.format(webtoonSidePadding / 100f),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
onValueChanged = { onValueChanged = {
webtoonSidePaddingPref.set(it) webtoonSidePaddingPref.set(it)
true true
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.readerHideThreshold(), pref = readerPreferences.readerHideThreshold(),
title = stringResource(MR.strings.pref_hide_threshold),
entries = persistentMapOf( entries = persistentMapOf(
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest), ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high), ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low), ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest), ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
), ),
title = stringResource(MR.strings.pref_hide_threshold),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.cropBordersWebtoon(), pref = readerPreferences.cropBordersWebtoon(),
title = stringResource(MR.strings.pref_crop_borders), title = stringResource(MR.strings.pref_crop_borders),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = dualPageSplitPref, pref = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split), title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = { onValueChanged = {
rotateToFitPref.set(false) rotateToFitPref.set(false)
@@ -371,13 +372,13 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.dualPageInvertWebtoon(), pref = readerPreferences.dualPageInvertWebtoon(),
title = stringResource(MR.strings.pref_dual_page_invert), title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit, enabled = dualPageSplit,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = rotateToFitPref, pref = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate), title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = { onValueChanged = {
dualPageSplitPref.set(false) dualPageSplitPref.set(false)
@@ -385,16 +386,16 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.dualPageRotateToFitInvertWebtoon(), pref = readerPreferences.dualPageRotateToFitInvertWebtoon(),
title = stringResource(MR.strings.pref_page_rotate_invert), title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit, enabled = rotateToFit,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.webtoonDoubleTapZoomEnabled(), pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
title = stringResource(MR.strings.pref_double_tap_zoom), title = stringResource(MR.strings.pref_double_tap_zoom),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.webtoonDisableZoomOut(), pref = readerPreferences.webtoonDisableZoomOut(),
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out), title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
), ),
), ),
@@ -409,11 +410,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_navigation), title = stringResource(MR.strings.pref_reader_navigation),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readWithVolumeKeysPref, pref = readWithVolumeKeysPref,
title = stringResource(MR.strings.pref_read_with_volume_keys), title = stringResource(MR.strings.pref_read_with_volume_keys),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.readWithVolumeKeysInverted(), pref = readerPreferences.readWithVolumeKeysInverted(),
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted), title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
enabled = readWithVolumeKeys, enabled = readWithVolumeKeys,
), ),
@@ -427,11 +428,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_actions), title = stringResource(MR.strings.pref_reader_actions),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.readWithLongTap(), pref = readerPreferences.readWithLongTap(),
title = stringResource(MR.strings.pref_read_with_long_tap), title = stringResource(MR.strings.pref_read_with_long_tap),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.folderPerManga(), pref = readerPreferences.folderPerManga(),
title = stringResource(MR.strings.pref_create_folder_per_manga), title = stringResource(MR.strings.pref_create_folder_per_manga),
subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary), subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary),
), ),

View File

@@ -7,11 +7,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
import eu.kanade.tachiyomi.util.system.telemetryIncluded
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@@ -30,29 +28,16 @@ object SettingsSecurityScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() }
return buildList(2) {
add(getSecurityGroup(securityPreferences))
if (!telemetryIncluded) return@buildList
add(getFirebaseGroup(privacyPreferences))
}
}
@Composable
private fun getSecurityGroup(
securityPreferences: SecurityPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current val context = LocalContext.current
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
val authSupported = remember { context.isAuthenticationSupported() } val authSupported = remember { context.isAuthenticationSupported() }
val useAuthPref = securityPreferences.useAuthenticator() val useAuthPref = securityPreferences.useAuthenticator()
val useAuth by useAuthPref.collectAsState() val useAuth by useAuthPref.collectAsState()
return Preference.PreferenceGroup( return listOf(
title = stringResource(MR.strings.pref_security),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = useAuthPref, pref = useAuthPref,
title = stringResource(MR.strings.lock_with_biometrics), title = stringResource(MR.strings.lock_with_biometrics),
enabled = authSupported, enabled = authSupported,
onValueChanged = { onValueChanged = {
@@ -62,7 +47,9 @@ object SettingsSecurityScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = securityPreferences.lockAppAfter(), pref = securityPreferences.lockAppAfter(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
entries = LockAfterValues entries = LockAfterValues
.associateWith { .associateWith {
when (it) { when (it) {
@@ -72,50 +59,24 @@ object SettingsSecurityScreen : SearchableSettings {
} }
} }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
onValueChanged = { onValueChanged = {
(context as FragmentActivity).authenticate( (context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_when_idle), title = context.stringResource(MR.strings.lock_when_idle),
) )
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = securityPreferences.hideNotificationContent(), pref = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content), title = stringResource(MR.strings.hide_notification_content),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = securityPreferences.secureScreen(), pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
entries = SecurityPreferences.SecureScreenMode.entries entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.secure_screen),
), ),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
),
)
}
@Composable
private fun getFirebaseGroup(
privacyPreferences: PrivacyPreferences,
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_firebase),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
preference = privacyPreferences.crashlytics(),
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
),
Preference.PreferenceItem.SwitchPreference(
preference = privacyPreferences.analytics(),
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.firebase_summary)),
),
) )
} }
} }

View File

@@ -40,7 +40,6 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.track.model.AutoTrackState
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
@@ -54,7 +53,6 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@@ -87,7 +85,6 @@ object SettingsTrackingScreen : SearchableSettings {
val trackPreferences = remember { Injekt.get<TrackPreferences>() } val trackPreferences = remember { Injekt.get<TrackPreferences>() }
val trackerManager = remember { Injekt.get<TrackerManager>() } val trackerManager = remember { Injekt.get<TrackerManager>() }
val sourceManager = remember { Injekt.get<SourceManager>() } val sourceManager = remember { Injekt.get<SourceManager>() }
val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead()
var dialog by remember { mutableStateOf<Any?>(null) } var dialog by remember { mutableStateOf<Any?>(null) }
dialog?.run { dialog?.run {
@@ -125,45 +122,44 @@ object SettingsTrackingScreen : SearchableSettings {
return listOf( return listOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = trackPreferences.autoUpdateTrack(), pref = trackPreferences.autoUpdateTrack(),
title = stringResource(MR.strings.pref_auto_update_manga_sync), title = stringResource(MR.strings.pref_auto_update_manga_sync),
), ),
Preference.PreferenceItem.ListPreference(
preference = trackPreferences.autoUpdateTrackOnMarkRead(),
entries = AutoTrackState.entries
.associateWith { stringResource(it.titleRes) }
.toPersistentMap(),
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
),
Preference.PreferenceGroup( Preference.PreferenceGroup(
title = stringResource(MR.strings.services), title = stringResource(MR.strings.services),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.myAnimeList.name,
tracker = trackerManager.myAnimeList, tracker = trackerManager.myAnimeList,
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.myAnimeList) }, logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.aniList.name,
tracker = trackerManager.aniList, tracker = trackerManager.aniList,
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.aniList) }, logout = { dialog = LogoutDialog(trackerManager.aniList) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.kitsu.name,
tracker = trackerManager.kitsu, tracker = trackerManager.kitsu,
login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) }, login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) },
logout = { dialog = LogoutDialog(trackerManager.kitsu) }, logout = { dialog = LogoutDialog(trackerManager.kitsu) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.mangaUpdates.name,
tracker = trackerManager.mangaUpdates, tracker = trackerManager.mangaUpdates,
login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) }, login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) },
logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) }, logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.shikimori.name,
tracker = trackerManager.shikimori, tracker = trackerManager.shikimori,
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.shikimori) }, logout = { dialog = LogoutDialog(trackerManager.shikimori) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.bangumi.name,
tracker = trackerManager.bangumi, tracker = trackerManager.bangumi,
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.bangumi) }, logout = { dialog = LogoutDialog(trackerManager.bangumi) },
@@ -177,6 +173,7 @@ object SettingsTrackingScreen : SearchableSettings {
enhancedTrackers.first enhancedTrackers.first
.map { service -> .map { service ->
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = service.name,
tracker = service, tracker = service,
login = { (service as EnhancedTracker).loginNoop() }, login = { (service as EnhancedTracker).loginNoop() },
logout = service::logout, logout = service::logout,

View File

@@ -35,9 +35,7 @@ import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.toDateTimestampString import eu.kanade.tachiyomi.util.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.updaterEnabled
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@@ -99,7 +97,7 @@ object AboutScreen : Screen() {
) )
} }
if (updaterEnabled) { if (BuildConfig.INCLUDE_UPDATER) {
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.check_for_updates), title = stringResource(MR.strings.check_for_updates),
@@ -123,7 +121,7 @@ object AboutScreen : Screen() {
versionName = result.release.version, versionName = result.release.version,
changelogInfo = result.release.info, changelogInfo = result.release.info,
releaseLink = result.release.releaseLink, releaseLink = result.release.releaseLink,
downloadLink = result.release.downloadLink, downloadLink = result.release.getDownloadLink(),
) )
navigator.push(updateScreen) navigator.push(updateScreen)
}, },
@@ -247,7 +245,7 @@ object AboutScreen : Screen() {
} }
} }
} }
isPreviewBuildType -> { BuildConfig.PREVIEW -> {
"Beta r${BuildConfig.COMMIT_COUNT}".let { "Beta r${BuildConfig.COMMIT_COUNT}".let {
if (withBuildDate) { if (withBuildDate) {
"$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})" "$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})"

View File

@@ -45,8 +45,8 @@ fun ReaderAppBars(
onClickTopAppBar: () -> Unit, onClickTopAppBar: () -> Unit,
bookmarked: Boolean, bookmarked: Boolean,
onToggleBookmarked: () -> Unit, onToggleBookmarked: () -> Unit,
onOpenInWebView: (() -> Unit)?,
onOpenInBrowser: (() -> Unit)?, onOpenInBrowser: (() -> Unit)?,
onOpenInWebView: (() -> Unit)?,
onShare: (() -> Unit)?, onShare: (() -> Unit)?,
viewer: Viewer?, viewer: Viewer?,
@@ -56,7 +56,7 @@ fun ReaderAppBars(
enabledPrevious: Boolean, enabledPrevious: Boolean,
currentPage: Int, currentPage: Int,
totalPages: Int, totalPages: Int,
onPageIndexChange: (Int) -> Unit, onSliderValueChange: (Int) -> Unit,
readingMode: ReadingMode, readingMode: ReadingMode,
onClickReadingMode: () -> Unit, onClickReadingMode: () -> Unit,
@@ -120,14 +120,6 @@ fun ReaderAppBars(
onClick = onToggleBookmarked, onClick = onToggleBookmarked,
), ),
) )
onOpenInWebView?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_web_view),
onClick = it,
),
)
}
onOpenInBrowser?.let { onOpenInBrowser?.let {
add( add(
AppBar.OverflowAction( AppBar.OverflowAction(
@@ -136,6 +128,14 @@ fun ReaderAppBars(
), ),
) )
} }
onOpenInWebView?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_web_view),
onClick = it,
),
)
}
onShare?.let { onShare?.let {
add( add(
AppBar.OverflowAction( AppBar.OverflowAction(
@@ -176,8 +176,9 @@ fun ReaderAppBars(
enabledPrevious = enabledPrevious, enabledPrevious = enabledPrevious,
currentPage = currentPage, currentPage = currentPage,
totalPages = totalPages, totalPages = totalPages,
onPageIndexChange = onPageIndexChange, onSliderValueChange = onSliderValueChange,
) )
BottomReaderBar( BottomReaderBar(
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
readingMode = readingMode, readingMode = readingMode,

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -17,6 +16,7 @@ import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -29,7 +29,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
@@ -39,8 +38,8 @@ import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.util.isTabletUi import eu.kanade.presentation.util.isTabletUi
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Slider
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import kotlin.math.roundToInt
@Composable @Composable
fun ChapterNavigator( fun ChapterNavigator(
@@ -51,7 +50,7 @@ fun ChapterNavigator(
enabledPrevious: Boolean, enabledPrevious: Boolean,
currentPage: Int, currentPage: Int,
totalPages: Int, totalPages: Int,
onPageIndexChange: (Int) -> Unit, onSliderValueChange: (Int) -> Unit,
) { ) {
val isTabletUi = isTabletUi() val isTabletUi = isTabletUi()
val horizontalPadding = if (isTabletUi) 24.dp else 8.dp val horizontalPadding = if (isTabletUi) 24.dp else 8.dp
@@ -98,11 +97,7 @@ fun ChapterNavigator(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Box(contentAlignment = Alignment.CenterEnd) {
Text(text = currentPage.toString()) Text(text = currentPage.toString())
// Taking up full length so the slider doesn't shift when 'currentPage' length changes
Text(text = totalPages.toString(), color = Color.Transparent)
}
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val sliderDragged by interactionSource.collectIsDraggedAsState() val sliderDragged by interactionSource.collectIsDraggedAsState()
@@ -115,11 +110,14 @@ fun ChapterNavigator(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = currentPage, value = currentPage.toFloat(),
valueRange = 1..totalPages, valueRange = 1f..totalPages.toFloat(),
onValueChange = f@{ steps = totalPages - 2,
if (it == currentPage) return@f onValueChange = {
onPageIndexChange(it - 1) val new = it.roundToInt() - 1
if (new != currentPage) {
onSliderValueChange(new)
}
}, },
interactionSource = interactionSource, interactionSource = interactionSource,
) )
@@ -160,7 +158,7 @@ private fun ChapterNavigatorPreview() {
enabledPrevious = true, enabledPrevious = true,
currentPage = currentPage, currentPage = currentPage,
totalPages = 10, totalPages = 10,
onPageIndexChange = { currentPage = (it + 1) }, onSliderValueChange = { currentPage = it },
) )
} }
} }

View File

@@ -2,7 +2,6 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -37,12 +36,12 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (customBrightness) { if (customBrightness) {
val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState() val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState()
SliderItem( SliderItem(
value = customBrightnessValue,
valueRange = -75..100,
steps = 0,
label = stringResource(MR.strings.pref_custom_brightness), label = stringResource(MR.strings.pref_custom_brightness),
min = -75,
max = 100,
value = customBrightnessValue,
valueText = customBrightnessValue.toString(),
onChange = { screenModel.preferences.customBrightnessValue().set(it) }, onChange = { screenModel.preferences.customBrightnessValue().set(it) },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
} }
@@ -54,52 +53,48 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (colorFilter) { if (colorFilter) {
val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState() val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState()
SliderItem( SliderItem(
value = colorFilterValue.red,
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_r_value), label = stringResource(MR.strings.color_filter_r_value),
max = 255,
value = colorFilterValue.red,
valueText = colorFilterValue.red.toString(),
onChange = { newRValue -> onChange = { newRValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, RED_MASK, 16) getColorValue(it, newRValue, RED_MASK, 16)
} }
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
value = colorFilterValue.green,
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_g_value), label = stringResource(MR.strings.color_filter_g_value),
max = 255,
value = colorFilterValue.green,
valueText = colorFilterValue.green.toString(),
onChange = { newGValue -> onChange = { newGValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newGValue, GREEN_MASK, 8) getColorValue(it, newGValue, GREEN_MASK, 8)
} }
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
value = colorFilterValue.blue,
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_b_value), label = stringResource(MR.strings.color_filter_b_value),
max = 255,
value = colorFilterValue.blue,
valueText = colorFilterValue.blue.toString(),
onChange = { newBValue -> onChange = { newBValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newBValue, BLUE_MASK, 0) getColorValue(it, newBValue, BLUE_MASK, 0)
} }
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
value = colorFilterValue.alpha,
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_a_value), label = stringResource(MR.strings.color_filter_a_value),
max = 255,
value = colorFilterValue.alpha,
valueText = colorFilterValue.alpha.toString(),
onChange = { newAValue -> onChange = { newAValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newAValue, ALPHA_MASK, 24) getColorValue(it, newAValue, ALPHA_MASK, 24)
} }
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState() val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()

View File

@@ -2,7 +2,6 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -98,21 +97,21 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
if (flashPageState) { if (flashPageState) {
SliderItem( SliderItem(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION, value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15,
label = stringResource(MR.strings.pref_flash_duration), label = stringResource(MR.strings.pref_flash_duration),
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) }, onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, min = 1,
max = 15,
) )
SliderItem( SliderItem(
value = flashInterval, value = flashInterval,
valueRange = 1..10,
label = stringResource(MR.strings.pref_flash_page_interval), label = stringResource(MR.strings.pref_flash_page_interval),
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
onChange = { onChange = {
flashIntervalPref.set(it) flashIntervalPref.set(it)
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, min = 1,
max = 10,
) )
SettingsChipRow(MR.strings.pref_flash_with) { SettingsChipRow(MR.strings.pref_flash_with) {
flashColors.map { (labelRes, value) -> flashColors.map { (labelRes, value) ->

View File

@@ -2,7 +2,6 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -153,14 +152,14 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState() val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
SliderItem( SliderItem(
value = webtoonSidePadding,
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
label = stringResource(MR.strings.pref_webtoon_side_padding), label = stringResource(MR.strings.pref_webtoon_side_padding),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
value = webtoonSidePadding,
valueText = numberFormat.format(webtoonSidePadding / 100f), valueText = numberFormat.format(webtoonSidePadding / 100f),
onChange = { onChange = {
screenModel.preferences.webtoonSidePadding().set(it) screenModel.preferences.webtoonSidePadding().set(it)
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
CheckboxItem( CheckboxItem(

View File

@@ -13,7 +13,6 @@ import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
import eu.kanade.presentation.theme.colorscheme.MonetColorScheme import eu.kanade.presentation.theme.colorscheme.MonetColorScheme
import eu.kanade.presentation.theme.colorscheme.MonochromeColorScheme
import eu.kanade.presentation.theme.colorscheme.NordColorScheme import eu.kanade.presentation.theme.colorscheme.NordColorScheme
import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme
import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
@@ -80,7 +79,6 @@ private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
AppTheme.GREEN_APPLE to GreenAppleColorScheme, AppTheme.GREEN_APPLE to GreenAppleColorScheme,
AppTheme.LAVENDER to LavenderColorScheme, AppTheme.LAVENDER to LavenderColorScheme,
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme, AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
AppTheme.MONOCHROME to MonochromeColorScheme,
AppTheme.NORD to NordColorScheme, AppTheme.NORD to NordColorScheme,
AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme, AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme,
AppTheme.TAKO to TakoColorScheme, AppTheme.TAKO to TakoColorScheme,

View File

@@ -1,84 +0,0 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
internal object MonochromeColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFFFFFFF),
onPrimary = Color(0xFF000000),
primaryContainer = Color(0xFFFFFFFF),
onPrimaryContainer = Color(0xFF000000),
secondary = Color(0xFFFFFFFF),
onSecondary = Color(0xFF000000),
secondaryContainer = Color(0xFF777777),
onSecondaryContainer = Color(0xFF000000),
tertiary = Color(0xFF777777),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFFFFFFF),
onTertiaryContainer = Color(0xFF000000),
error = Color(0xFFFFFFFF),
onError = Color(0xFF000000),
errorContainer = Color(0xFFFFFFFF),
onErrorContainer = Color(0xFF000000),
background = Color(0xFF000000),
onBackground = Color(0xFFFFFFFF),
surface = Color(0xFF000000),
onSurface = Color(0xFFFFFFFF),
surfaceVariant = Color(0xFF000000),
onSurfaceVariant = Color(0xFFFFFFFF),
outline = Color(0xFFFFFFFF),
outlineVariant = Color(0xFFFFFFFF),
scrim = Color(0xFF000000),
inverseSurface = Color(0xFFFFFFFF),
inverseOnSurface = Color(0xFF000000),
inversePrimary = Color(0xFF000000),
surfaceDim = Color(0xFF000000),
surfaceBright = Color(0xFFFFFFFF),
surfaceContainerLowest = Color(0xFF000000),
surfaceContainerLow = Color(0xFF000000),
surfaceContainer = Color(0xFF000000),
surfaceContainerHigh = Color(0xFF000000),
surfaceContainerHighest = Color(0xFF000000),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF000000),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFF000000),
onPrimaryContainer = Color(0xFFFFFFFF),
secondary = Color(0xFF000000),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFF888888),
onSecondaryContainer = Color(0xFFFFFFFF),
tertiary = Color(0xFF888888),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFF000000),
onTertiaryContainer = Color(0xFFFFFFFF),
error = Color(0xFF000000),
onError = Color(0xFFFFFFFF),
errorContainer = Color(0xFF000000),
onErrorContainer = Color(0xFFFFFFFF),
background = Color(0xFFFFFFFF),
onBackground = Color(0xFF000000),
surface = Color(0xFFFFFFFF),
onSurface = Color(0xFF000000),
surfaceVariant = Color(0xFFFFFFFF),
onSurfaceVariant = Color(0xFF000000),
outline = Color(0xFF000000),
outlineVariant = Color(0xFF000000),
scrim = Color(0xFF000000),
inverseSurface = Color(0xFF000000),
inverseOnSurface = Color(0xFFFFFFFF),
inversePrimary = Color(0xFFFFFFFF),
surfaceDim = Color(0xFFFFFFFF),
surfaceBright = Color(0xFFFFFFFF),
surfaceContainerLowest = Color(0xFFFFFFFF),
surfaceContainerLow = Color(0xFFFFFFFF),
surfaceContainer = Color(0xFFFFFFFF),
surfaceContainerHigh = Color(0xFFFFFFFF),
surfaceContainerHighest = Color(0xFFFFFFFF),
)
}

View File

@@ -10,12 +10,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
@@ -24,9 +22,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -75,7 +70,6 @@ fun TrackInfoDialogHome(
onOpenInBrowser: (TrackItem) -> Unit, onOpenInBrowser: (TrackItem) -> Unit,
onRemoved: (TrackItem) -> Unit, onRemoved: (TrackItem) -> Unit,
onCopyLink: (TrackItem) -> Unit, onCopyLink: (TrackItem) -> Unit,
onTogglePrivate: (TrackItem) -> Unit,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -90,7 +84,6 @@ fun TrackInfoDialogHome(
if (item.track != null) { if (item.track != null) {
val supportsScoring = item.tracker.getScoreList().isNotEmpty() val supportsScoring = item.tracker.getScoreList().isNotEmpty()
val supportsReadingDates = item.tracker.supportsReadingDates val supportsReadingDates = item.tracker.supportsReadingDates
val supportsPrivate = item.tracker.supportsPrivateTracking
TrackInfoItem( TrackInfoItem(
title = item.track.title, title = item.track.title,
tracker = item.tracker, tracker = item.tracker,
@@ -122,9 +115,6 @@ fun TrackInfoDialogHome(
onOpenInBrowser = { onOpenInBrowser(item) }, onOpenInBrowser = { onOpenInBrowser(item) },
onRemoved = { onRemoved(item) }, onRemoved = { onRemoved(item) },
onCopyLink = { onCopyLink(item) }, onCopyLink = { onCopyLink(item) },
private = item.track.private,
onTogglePrivate = { onTogglePrivate(item) }
.takeIf { supportsPrivate },
) )
} else { } else {
TrackInfoItemEmpty( TrackInfoItemEmpty(
@@ -154,37 +144,17 @@ private fun TrackInfoItem(
onOpenInBrowser: () -> Unit, onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit, onRemoved: () -> Unit,
onCopyLink: () -> Unit, onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) { ) {
val context = LocalContext.current val context = LocalContext.current
Column { Column {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) {
BadgedBox(
badge = {
if (private) {
Badge(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.absoluteOffset(x = (-5).dp),
) {
Icon(
imageVector = Icons.Filled.VisibilityOff,
contentDescription = stringResource(MR.strings.tracked_privately),
modifier = Modifier.size(14.dp),
)
}
}
},
) { ) {
TrackLogoIcon( TrackLogoIcon(
tracker = tracker, tracker = tracker,
onClick = onOpenInBrowser, onClick = onOpenInBrowser,
onLongClick = onCopyLink, onLongClick = onCopyLink,
) )
}
Box( Box(
modifier = Modifier modifier = Modifier
.height(48.dp) .height(48.dp)
@@ -211,8 +181,6 @@ private fun TrackInfoItem(
onOpenInBrowser = onOpenInBrowser, onOpenInBrowser = onOpenInBrowser,
onRemoved = onRemoved, onRemoved = onRemoved,
onCopyLink = onCopyLink, onCopyLink = onCopyLink,
private = private,
onTogglePrivate = onTogglePrivate,
) )
} }
@@ -323,8 +291,6 @@ private fun TrackInfoItemMenu(
onOpenInBrowser: () -> Unit, onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit, onRemoved: () -> Unit,
onCopyLink: () -> Unit, onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) { Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
@@ -352,25 +318,6 @@ private fun TrackInfoItemMenu(
expanded = false expanded = false
}, },
) )
if (onTogglePrivate != null) {
DropdownMenuItem(
text = {
Text(
stringResource(
if (private) {
MR.strings.action_toggle_private_off
} else {
MR.strings.action_toggle_private_on
},
),
)
},
onClick = {
onTogglePrivate()
expanded = false
},
)
}
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_remove)) }, text = { Text(stringResource(MR.strings.action_remove)) },
onClick = { onClick = {

View File

@@ -25,9 +25,7 @@ internal class TrackInfoDialogHomePreviewProvider :
remoteUrl = "https://example.com", remoteUrl = "https://example.com",
startDate = 0L, startDate = 0L,
finishDate = 0L, finishDate = 0L,
private = false,
) )
private val privateTrack = aTrack.copy(private = true)
private val trackItemWithoutTrack = TrackItem( private val trackItemWithoutTrack = TrackItem(
track = null, track = null,
tracker = DummyTracker( tracker = DummyTracker(
@@ -42,13 +40,6 @@ internal class TrackInfoDialogHomePreviewProvider :
name = "Example Tracker 2", name = "Example Tracker 2",
), ),
) )
private val trackItemWithPrivateTrack = TrackItem(
track = privateTrack,
tracker = DummyTracker(
id = 2L,
name = "Example Tracker 2",
),
)
private val trackersWithAndWithoutTrack = @Composable { private val trackersWithAndWithoutTrack = @Composable {
TrackInfoDialogHome( TrackInfoDialogHome(
@@ -66,7 +57,6 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {}, onOpenInBrowser = {},
onRemoved = {}, onRemoved = {},
onCopyLink = {}, onCopyLink = {},
onTogglePrivate = {},
) )
} }
@@ -83,24 +73,6 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {}, onOpenInBrowser = {},
onRemoved = {}, onRemoved = {},
onCopyLink = {}, onCopyLink = {},
onTogglePrivate = {},
)
}
private val trackerWithPrivateTracking = @Composable {
TrackInfoDialogHome(
trackItems = listOf(trackItemWithPrivateTrack),
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
onTogglePrivate = {},
) )
} }
@@ -108,6 +80,5 @@ internal class TrackInfoDialogHomePreviewProvider :
get() = sequenceOf( get() = sequenceOf(
trackersWithAndWithoutTrack, trackersWithAndWithoutTrack,
noTrackers, noTrackers,
trackerWithPrivateTracking,
) )
} }

View File

@@ -33,7 +33,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@@ -91,9 +90,8 @@ fun TrackerSearch(
queryResult: Result<List<TrackSearch>>?, queryResult: Result<List<TrackSearch>>?,
selected: TrackSearch?, selected: TrackSearch?,
onSelectedChange: (TrackSearch) -> Unit, onSelectedChange: (TrackSearch) -> Unit,
onConfirmSelection: (private: Boolean) -> Unit, onConfirmSelection: () -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
supportsPrivateTracking: Boolean,
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@@ -166,32 +164,16 @@ fun TrackerSearch(
enter = fadeIn() + slideInVertically { it / 2 }, enter = fadeIn() + slideInVertically { it / 2 },
exit = slideOutVertically { it / 2 } + fadeOut(), exit = slideOutVertically { it / 2 } + fadeOut(),
) { ) {
Row( Button(
onClick = { onConfirmSelection() },
modifier = Modifier modifier = Modifier
.padding(MaterialTheme.padding.small) .padding(12.dp)
.windowInsetsPadding(WindowInsets.navigationBars) .windowInsetsPadding(WindowInsets.navigationBars)
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Button(
onClick = { onConfirmSelection(false) },
modifier = Modifier.weight(1f),
elevation = ButtonDefaults.elevatedButtonElevation(), elevation = ButtonDefaults.elevatedButtonElevation(),
) { ) {
Text(text = stringResource(MR.strings.action_track)) Text(text = stringResource(MR.strings.action_track))
} }
if (supportsPrivateTracking) {
Button(
onClick = { onConfirmSelection(true) },
elevation = ButtonDefaults.elevatedButtonElevation(),
) {
Icon(
imageVector = Icons.Filled.VisibilityOff,
contentDescription = stringResource(MR.strings.action_toggle_private_on),
)
}
}
}
} }
}, },
) { innerPadding -> ) { innerPadding ->
@@ -304,15 +286,6 @@ private fun SearchResultItem(
} }
}, },
) )
if (trackSearch.authors.isNotEmpty() || trackSearch.artists.isNotEmpty()) {
Text(
text = (trackSearch.authors + trackSearch.artists).distinct().joinToString(),
modifier = Modifier.secondaryItemAlpha(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
)
}
if (type.isNotBlank()) { if (type.isNotBlank()) {
SearchResultItemDetails( SearchResultItemDetails(
title = stringResource(MR.strings.track_type), title = stringResource(MR.strings.track_type),

View File

@@ -5,11 +5,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import java.text.SimpleDateFormat
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Locale
import kotlin.random.Random import kotlin.random.Random
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> { internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
@@ -23,7 +20,6 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {}, onSelectedChange = {},
onConfirmSelection = {}, onConfirmSelection = {},
onDismissRequest = {}, onDismissRequest = {},
supportsPrivateTracking = false,
) )
} }
private val fullPageWithoutSelected = @Composable { private val fullPageWithoutSelected = @Composable {
@@ -35,7 +31,6 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {}, onSelectedChange = {},
onConfirmSelection = {}, onConfirmSelection = {},
onDismissRequest = {}, onDismissRequest = {},
supportsPrivateTracking = false,
) )
} }
private val loading = @Composable { private val loading = @Composable {
@@ -47,27 +42,12 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {}, onSelectedChange = {},
onConfirmSelection = {}, onConfirmSelection = {},
onDismissRequest = {}, onDismissRequest = {},
supportsPrivateTracking = false,
)
}
private val fullPageWithPrivateTracking = @Composable {
val items = someTrackSearches().take(30).toList()
TrackerSearch(
state = TextFieldState(initialText = "search text"),
onDispatchQuery = {},
queryResult = Result.success(items),
selected = items[1],
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = true,
) )
} }
override val values: Sequence<@Composable () -> Unit> = sequenceOf( override val values: Sequence<@Composable () -> Unit> = sequenceOf(
fullPageWithSecondSelected, fullPageWithSecondSelected,
fullPageWithoutSelected, fullPageWithoutSelected,
loading, loading,
fullPageWithPrivateTracking,
) )
private fun someTrackSearches(): Sequence<TrackSearch> = sequence { private fun someTrackSearches(): Sequence<TrackSearch> = sequence {
@@ -76,8 +56,6 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
} }
} }
private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private fun randTrackSearch() = TrackSearch().let { private fun randTrackSearch() = TrackSearch().let {
it.id = Random.nextLong() it.id = Random.nextLong()
it.manga_id = Random.nextLong() it.manga_id = Random.nextLong()
@@ -93,17 +71,11 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
it.finished_reading_date = 0L it.finished_reading_date = 0L
it.tracking_url = "https://example.com/tracker-example" it.tracking_url = "https://example.com/tracker-example"
it.cover_url = "https://example.com/cover.png" it.cover_url = "https://example.com/cover.png"
it.start_date = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS))) it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString()
it.summary = lorem((0..40).random()).joinToString() it.summary = lorem((0..40).random()).joinToString()
it.publishing_status = if (Random.nextBoolean()) "Finished" else ""
it.publishing_type = if (Random.nextBoolean()) "Oneshot" else ""
it.artists = randomNames()
it.authors = randomNames()
it it
} }
private fun randomNames(): List<String> = (0..(0..3).random()).map { lorem((3..5).random()).joinToString() }
private fun lorem(words: Int): Sequence<String> = private fun lorem(words: Int): Sequence<String> =
LoremIpsum(words).values LoremIpsum(words).values
} }

View File

@@ -3,7 +3,6 @@ package eu.kanade.presentation.webview
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -27,7 +26,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.kevinnzou.web.AccompanistWebViewClient import com.kevinnzou.web.AccompanistWebViewClient
@@ -39,18 +37,13 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.components.WarningBanner
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.getHtml import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Request
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun WebViewScreenContent( fun WebViewScreenContent(
@@ -65,11 +58,8 @@ fun WebViewScreenContent(
) { ) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers) val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val navigator = rememberWebViewNavigator() val navigator = rememberWebViewNavigator()
val context = LocalContext.current
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val network = remember { Injekt.get<NetworkHelper>() }
val spoofedPackageName = remember { WebViewUtil.spoofedPackageName(context) }
var currentUrl by remember { mutableStateOf(url) } var currentUrl by remember { mutableStateOf(url) }
var showCloudflareHelp by remember { mutableStateOf(false) } var showCloudflareHelp by remember { mutableStateOf(false) }
@@ -124,40 +114,6 @@ fun WebViewScreenContent(
} }
return super.shouldOverrideUrlLoading(view, request) return super.shouldOverrideUrlLoading(view, request)
} }
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?,
): WebResourceResponse? {
return try {
val internalRequest = Request.Builder().apply {
url(request!!.url.toString())
request.requestHeaders.forEach { (key, value) ->
if (key == "X-Requested-With" && value in setOf(context.packageName, spoofedPackageName)) {
return@forEach
}
addHeader(key, value)
}
method(request.method, null)
}.build()
val response = network.nonCloudflareClient.newCall(internalRequest).execute()
val contentType = response.body.contentType()?.let { "${it.type}/${it.subtype}" } ?: "text/html"
val contentEncoding = response.body.contentType()?.charset()?.name() ?: "utf-8"
WebResourceResponse(
contentType,
contentEncoding,
response.code,
response.message,
response.headers.associate { it.first to it.second },
response.body.byteStream(),
)
} catch (e: Throwable) {
super.shouldInterceptRequest(view, request)
}
}
} }
} }

View File

@@ -26,7 +26,6 @@ import eu.kanade.domain.DomainModule
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.crash.CrashActivity import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
@@ -41,7 +40,6 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
@@ -54,12 +52,10 @@ import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import mihon.core.migration.Migrator import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations import mihon.core.migration.migrations.migrations
import mihon.telemetry.TelemetryConfig
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager import tachiyomi.presentation.widget.WidgetManager
@@ -71,7 +67,6 @@ import java.security.Security
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory { class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
private val basePreferences: BasePreferences by injectLazy() private val basePreferences: BasePreferences by injectLazy()
private val privacyPreferences: PrivacyPreferences by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver() private val disableIncognitoReceiver = DisableIncognitoReceiver()
@@ -80,7 +75,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
override fun onCreate() { override fun onCreate() {
super<Application>.onCreate() super<Application>.onCreate()
patchInjekt() patchInjekt()
TelemetryConfig.init(applicationContext)
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java) GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
@@ -103,8 +97,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
val scope = ProcessLifecycleOwner.get().lifecycleScope
// Show notification to disable Incognito Mode when it's enabled // Show notification to disable Incognito Mode when it's enabled
basePreferences.incognitoMode().changes() basePreferences.incognitoMode().changes()
.onEach { enabled -> .onEach { enabled ->
@@ -132,30 +124,14 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
cancelNotification(Notifications.ID_INCOGNITO_MODE) cancelNotification(Notifications.ID_INCOGNITO_MODE)
} }
} }
.launchIn(scope) .launchIn(ProcessLifecycleOwner.get().lifecycleScope)
privacyPreferences.analytics()
.changes()
.onEach(TelemetryConfig::setAnalyticsEnabled)
.launchIn(scope)
privacyPreferences.crashlytics()
.changes()
.onEach(TelemetryConfig::setCrashlyticsEnabled)
.launchIn(scope)
basePreferences.hardwareBitmapThreshold().let { preference ->
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
}
basePreferences.hardwareBitmapThreshold().changes()
.onEach { ImageUtil.hardwareBitmapThreshold = it }
.launchIn(scope)
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get()) setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
// Updates widget update // Updates widget update
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) } with(WidgetManager(Injekt.get(), Injekt.get())) {
init(ProcessLifecycleOwner.get().lifecycleScope)
}
if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) { if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
@@ -219,15 +195,17 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
try { try {
// Override the value passed as X-Requested-With in WebView requests // Override the value passed as X-Requested-With in WebView requests
val stackTrace = Looper.getMainLooper().thread.stackTrace val stackTrace = Looper.getMainLooper().thread.stackTrace
val isChromiumCall = stackTrace.any { trace -> val chromiumElement = stackTrace.find {
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) && it.className.equals(
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) } "org.chromium.base.BuildInfo",
ignoreCase = true,
)
}
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
} }
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
} catch (_: Exception) { } catch (_: Exception) {
} }
return super.getPackageName() return super.getPackageName()
} }

View File

@@ -11,6 +11,7 @@ import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import kotlin.system.exitProcess
class GlobalExceptionHandler private constructor( class GlobalExceptionHandler private constructor(
private val applicationContext: Context, private val applicationContext: Context,
@@ -30,10 +31,14 @@ class GlobalExceptionHandler private constructor(
} }
override fun uncaughtException(thread: Thread, exception: Throwable) { override fun uncaughtException(thread: Thread, exception: Throwable) {
try {
logcat(priority = LogPriority.ERROR, throwable = exception) logcat(priority = LogPriority.ERROR, throwable = exception)
launchActivity(applicationContext, activityToBeLaunched, exception) launchActivity(applicationContext, activityToBeLaunched, exception)
exitProcess(0)
} catch (_: Exception) {
defaultHandler.uncaughtException(thread, exception) defaultHandler.uncaughtException(thread, exception)
} }
}
private fun launchActivity( private fun launchActivity(
applicationContext: Context, applicationContext: Context,

View File

@@ -27,7 +27,6 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -44,7 +43,6 @@ class BackupCreator(
private val parser: ProtoBuf = Injekt.get(), private val parser: ProtoBuf = Injekt.get(),
private val getFavorites: GetFavorites = Injekt.get(), private val getFavorites: GetFavorites = Injekt.get(),
private val backupPreferences: BackupPreferences = Injekt.get(), private val backupPreferences: BackupPreferences = Injekt.get(),
private val mangaRepository: MangaRepository = Injekt.get(),
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(), private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(), private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
@@ -77,9 +75,7 @@ class BackupCreator(
throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error)) throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error))
} }
val nonFavoriteManga = if (options.readEntries) mangaRepository.getReadMangaNotInLibrary() else emptyList() val backupManga = backupMangas(getFavorites.await(), options)
val backupManga = backupMangas(getFavorites.await() + nonFavoriteManga, options)
val backup = Backup( val backup = Backup(
backupManga = backupManga, backupManga = backupManga,
backupCategories = backupCategories(options), backupCategories = backupCategories(options),

View File

@@ -10,7 +10,6 @@ data class BackupOptions(
val chapters: Boolean = true, val chapters: Boolean = true,
val tracking: Boolean = true, val tracking: Boolean = true,
val history: Boolean = true, val history: Boolean = true,
val readEntries: Boolean = true,
val appSettings: Boolean = true, val appSettings: Boolean = true,
val extensionRepoSettings: Boolean = true, val extensionRepoSettings: Boolean = true,
val sourceSettings: Boolean = true, val sourceSettings: Boolean = true,
@@ -23,7 +22,6 @@ data class BackupOptions(
chapters, chapters,
tracking, tracking,
history, history,
readEntries,
appSettings, appSettings,
extensionRepoSettings, extensionRepoSettings,
sourceSettings, sourceSettings,
@@ -62,12 +60,6 @@ data class BackupOptions(
getter = BackupOptions::categories, getter = BackupOptions::categories,
setter = { options, enabled -> options.copy(categories = enabled) }, setter = { options, enabled -> options.copy(categories = enabled) },
), ),
Entry(
label = MR.strings.non_library_settings,
getter = BackupOptions::readEntries,
setter = { options, enabled -> options.copy(readEntries = enabled) },
enabled = { it.libraryEntries },
),
) )
val settingsOptions = persistentListOf( val settingsOptions = persistentListOf(
@@ -100,11 +92,10 @@ data class BackupOptions(
chapters = array[2], chapters = array[2],
tracking = array[3], tracking = array[3],
history = array[4], history = array[4],
readEntries = array[5], appSettings = array[5],
appSettings = array[6], extensionRepoSettings = array[6],
extensionRepoSettings = array[7], sourceSettings = array[7],
sourceSettings = array[8], privateSettings = array[8],
privateSettings = array[9],
) )
} }

View File

@@ -8,7 +8,6 @@ import tachiyomi.domain.category.model.Category
class BackupCategory( class BackupCategory(
@ProtoNumber(1) var name: String, @ProtoNumber(1) var name: String,
@ProtoNumber(2) var order: Long = 0, @ProtoNumber(2) var order: Long = 0,
@ProtoNumber(3) var id: Long = 0,
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x // @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
@ProtoNumber(100) var flags: Long = 0, @ProtoNumber(100) var flags: Long = 0,
) { ) {
@@ -22,7 +21,6 @@ class BackupCategory(
val backupCategoryMapper = { category: Category -> val backupCategoryMapper = { category: Category ->
BackupCategory( BackupCategory(
id = category.id,
name = category.name, name = category.name,
order = category.order, order = category.order,
flags = category.flags, flags = category.flags,

View File

@@ -25,7 +25,6 @@ data class BackupTracking(
@ProtoNumber(10) var startedReadingDate: Long = 0, @ProtoNumber(10) var startedReadingDate: Long = 0,
// finishedReadingDate is called endReadTime in 1.x // finishedReadingDate is called endReadTime in 1.x
@ProtoNumber(11) var finishedReadingDate: Long = 0, @ProtoNumber(11) var finishedReadingDate: Long = 0,
@ProtoNumber(12) var private: Boolean = false,
@ProtoNumber(100) var mediaId: Long = 0, @ProtoNumber(100) var mediaId: Long = 0,
) { ) {
@@ -49,7 +48,6 @@ data class BackupTracking(
startDate = this@BackupTracking.startedReadingDate, startDate = this@BackupTracking.startedReadingDate,
finishDate = this@BackupTracking.finishedReadingDate, finishDate = this@BackupTracking.finishedReadingDate,
remoteUrl = this@BackupTracking.trackingUrl, remoteUrl = this@BackupTracking.trackingUrl,
private = this@BackupTracking.private,
) )
} }
} }
@@ -68,7 +66,6 @@ val backupTrackMapper = {
remoteUrl: String, remoteUrl: String,
startDate: Long, startDate: Long,
finishDate: Long, finishDate: Long,
private: Boolean,
-> ->
BackupTracking( BackupTracking(
syncId = syncId.toInt(), syncId = syncId.toInt(),
@@ -83,6 +80,5 @@ val backupTrackMapper = {
startedReadingDate = startDate, startedReadingDate = startDate,
finishedReadingDate = finishDate, finishedReadingDate = finishDate,
trackingUrl = remoteUrl, trackingUrl = remoteUrl,
private = private,
) )
} }

View File

@@ -91,7 +91,7 @@ class BackupRestorer(
restoreCategories(backup.backupCategories) restoreCategories(backup.backupCategories)
} }
if (options.appSettings) { if (options.appSettings) {
restoreAppPreferences(backup.backupPreferences, backup.backupCategories.takeIf { options.categories }) restoreAppPreferences(backup.backupPreferences)
} }
if (options.sourceSettings) { if (options.sourceSettings) {
restoreSourcePreferences(backup.backupSourcePreferences) restoreSourcePreferences(backup.backupSourcePreferences)
@@ -140,15 +140,9 @@ class BackupRestorer(
} }
} }
private fun CoroutineScope.restoreAppPreferences( private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
preferences: List<BackupPreference>,
categories: List<BackupCategory>?,
) = launch {
ensureActive() ensureActive()
preferenceRestorer.restoreApp( preferenceRestorer.restoreApp(preferences)
preferences,
categories,
)
restoreProgress += 1 restoreProgress += 1
notifier.showRestoreProgress( notifier.showRestoreProgress(

View File

@@ -404,7 +404,6 @@ class MangaRestorer(
track.remoteUrl, track.remoteUrl,
track.startDate, track.startDate,
track.finishDate, track.finishDate,
track.private,
track.id, track.id,
) )
} }

View File

@@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers package eu.kanade.tachiyomi.data.backup.restore.restorers
import android.content.Context import android.content.Context
import android.util.Log
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupPreference import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
@@ -16,62 +14,38 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.source.sourcePreferences
import tachiyomi.core.common.preference.AndroidPreferenceStore import tachiyomi.core.common.preference.AndroidPreferenceStore
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.plusAssign
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class PreferenceRestorer( class PreferenceRestorer(
private val context: Context, private val context: Context,
private val getCategories: GetCategories = Injekt.get(),
private val preferenceStore: PreferenceStore = Injekt.get(), private val preferenceStore: PreferenceStore = Injekt.get(),
) { ) {
suspend fun restoreApp(
preferences: List<BackupPreference>, fun restoreApp(preferences: List<BackupPreference>) {
backupCategories: List<BackupCategory>?, restorePreferences(preferences, preferenceStore)
) {
restorePreferences(
preferences,
preferenceStore,
backupCategories,
)
LibraryUpdateJob.setupTask(context) LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context) BackupCreateJob.setupTask(context)
} }
suspend fun restoreSource(preferences: List<BackupSourcePreferences>) { fun restoreSource(preferences: List<BackupSourcePreferences>) {
preferences.forEach { preferences.forEach {
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey)) val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
restorePreferences(it.prefs, sourcePrefs) restorePreferences(it.prefs, sourcePrefs)
} }
} }
private suspend fun restorePreferences( private fun restorePreferences(
toRestore: List<BackupPreference>, toRestore: List<BackupPreference>,
preferenceStore: PreferenceStore, preferenceStore: PreferenceStore,
backupCategories: List<BackupCategory>? = null,
) { ) {
val allCategories = if (backupCategories != null) getCategories.await() else emptyList()
val categoriesByName = allCategories.associateBy { it.name }
val backupCategoriesById = backupCategories?.associateBy { it.id.toString() }.orEmpty()
val prefs = preferenceStore.getAll() val prefs = preferenceStore.getAll()
toRestore.forEach { (key, value) -> toRestore.forEach { (key, value) ->
try {
when (value) { when (value) {
is IntPreferenceValue -> { is IntPreferenceValue -> {
if (prefs[key] is Int?) { if (prefs[key] is Int?) {
val newValue = if (key == LibraryPreferences.DEFAULT_CATEGORY_PREF_KEY) { preferenceStore.getInt(key).set(value.value)
backupCategoriesById[value.value.toString()]
?.let { categoriesByName[it.name]?.id?.toInt() }
} else {
value.value
}
newValue?.let { preferenceStore.getInt(key).set(it) }
} }
} }
is LongPreferenceValue -> { is LongPreferenceValue -> {
@@ -96,42 +70,10 @@ class PreferenceRestorer(
} }
is StringSetPreferenceValue -> { is StringSetPreferenceValue -> {
if (prefs[key] is Set<*>?) { if (prefs[key] is Set<*>?) {
val restored = restoreCategoriesPreference( preferenceStore.getStringSet(key).set(value.value)
key,
value.value,
preferenceStore,
backupCategoriesById,
categoriesByName,
)
if (!restored) preferenceStore.getStringSet(key).set(value.value)
} }
} }
} }
} catch (e: Exception) {
Log.e("PreferenceRestorer", "Failed to restore preference <$key>", e)
} }
} }
} }
private fun restoreCategoriesPreference(
key: String,
value: Set<String>,
preferenceStore: PreferenceStore,
backupCategoriesById: Map<String, BackupCategory>,
categoriesByName: Map<String, Category>,
): Boolean {
val categoryPreferences = LibraryPreferences.categoryPreferenceKeys + DownloadPreferences.categoryPreferenceKeys
if (key !in categoryPreferences) return false
val ids = value.mapNotNull {
backupCategoriesById[it]?.name?.let { name ->
categoriesByName[name]?.id?.toString()
}
}
if (ids.isNotEmpty()) {
preferenceStore.getStringSet(key) += ids
}
return true
}
}

View File

@@ -10,6 +10,7 @@ import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult import coil3.fetch.SourceFetchResult
import coil3.request.Options import coil3.request.Options
import coil3.request.bitmapConfig import coil3.request.bitmapConfig
import eu.kanade.tachiyomi.util.system.GLUtil
import okio.BufferedSource import okio.BufferedSource
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
@@ -45,7 +46,10 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
check(bitmap != null) { "Failed to decode image" } check(bitmap != null) { "Failed to decode image" }
if (options.bitmapConfig == Bitmap.Config.HARDWARE && ImageUtil.canUseHardwareBitmap(bitmap)) { if (
options.bitmapConfig == Bitmap.Config.HARDWARE &&
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
) {
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false) val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
if (hwBitmap != null) { if (hwBitmap != null) {
bitmap.recycle() bitmap.recycle()

View File

@@ -1,4 +1,4 @@
@file:Suppress("PropertyName") @file:Suppress("PropertyName", "ktlint:standard:property-naming")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
@@ -27,9 +27,6 @@ interface Chapter : SChapter, Serializable {
var version: Long var version: Long
} }
val Chapter.isRecognizedNumber: Boolean
get() = chapter_number >= 0f
fun Chapter.toDomainChapter(): DomainChapter? { fun Chapter.toDomainChapter(): DomainChapter? {
if (id == null || manga_id == null) return null if (id == null || manga_id == null) return null
return DomainChapter( return DomainChapter(

View File

@@ -1,4 +1,4 @@
@file:Suppress("PropertyName") @file:Suppress("PropertyName", "ktlint:standard:property-naming")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models

View File

@@ -1,4 +1,4 @@
@file:Suppress("PropertyName") @file:Suppress("PropertyName", "ktlint:standard:property-naming")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
@@ -32,15 +32,12 @@ interface Track : Serializable {
var tracking_url: String var tracking_url: String
var private: Boolean fun copyPersonalFrom(other: Track) {
fun copyPersonalFrom(other: Track, copyRemotePrivate: Boolean = true) {
last_chapter_read = other.last_chapter_read last_chapter_read = other.last_chapter_read
score = other.score score = other.score
status = other.status status = other.status
started_reading_date = other.started_reading_date started_reading_date = other.started_reading_date
finished_reading_date = other.finished_reading_date finished_reading_date = other.finished_reading_date
if (copyRemotePrivate) private = other.private
} }
companion object { companion object {

View File

@@ -1,4 +1,4 @@
@file:Suppress("PropertyName") @file:Suppress("PropertyName", "ktlint:standard:property-naming")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
@@ -29,6 +29,4 @@ class TrackImpl : Track {
override var finished_reading_date: Long = 0 override var finished_reading_date: Long = 0
override var tracking_url: String = "" override var tracking_url: String = ""
override var private: Boolean = false
} }

View File

@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.download
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.core.net.toUri import android.net.Uri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@@ -96,13 +96,13 @@ class DownloadCache(
private val diskCacheFile: File private val diskCacheFile: File
get() = File(context.cacheDir, "dl_index_cache_v3") get() = File(context.cacheDir, "dl_index_cache_v3")
private val rootDownloadsDirMutex = Mutex() private val rootDownloadsDirLock = Mutex()
private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
init { init {
// Attempt to read cache file // Attempt to read cache file
scope.launch { scope.launch {
rootDownloadsDirMutex.withLock { rootDownloadsDirLock.withLock {
try { try {
if (diskCacheFile.exists()) { if (diskCacheFile.exists()) {
val diskCache = diskCacheFile.inputStream().use { val diskCache = diskCacheFile.inputStream().use {
@@ -112,7 +112,7 @@ class DownloadCache(
lastRenew = System.currentTimeMillis() lastRenew = System.currentTimeMillis()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to initialize from disk cache" } logcat(LogPriority.ERROR, e) { "Failed to initialize disk cache" }
diskCacheFile.delete() diskCacheFile.delete()
} }
} }
@@ -198,7 +198,7 @@ class DownloadCache(
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) { suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
rootDownloadsDirMutex.withLock { rootDownloadsDirLock.withLock {
// Retrieve the cached source directory or cache a new one // Retrieve the cached source directory or cache a new one
var sourceDir = rootDownloadsDir.sourceDirs[manga.source] var sourceDir = rootDownloadsDir.sourceDirs[manga.source]
if (sourceDir == null) { if (sourceDir == null) {
@@ -230,7 +230,7 @@ class DownloadCache(
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
suspend fun removeChapter(chapter: Chapter, manga: Manga) { suspend fun removeChapter(chapter: Chapter, manga: Manga) {
rootDownloadsDirMutex.withLock { rootDownloadsDirLock.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach { provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
@@ -250,7 +250,7 @@ class DownloadCache(
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
suspend fun removeChapters(chapters: List<Chapter>, manga: Manga) { suspend fun removeChapters(chapters: List<Chapter>, manga: Manga) {
rootDownloadsDirMutex.withLock { rootDownloadsDirLock.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
chapters.forEach { chapter -> chapters.forEach { chapter ->
@@ -271,7 +271,7 @@ class DownloadCache(
* @param manga the manga to remove. * @param manga the manga to remove.
*/ */
suspend fun removeManga(manga: Manga) { suspend fun removeManga(manga: Manga) {
rootDownloadsDirMutex.withLock { rootDownloadsDirLock.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDirName = provider.getMangaDirName(manga.title) val mangaDirName = provider.getMangaDirName(manga.title)
if (sourceDir.mangaDirs.containsKey(mangaDirName)) { if (sourceDir.mangaDirs.containsKey(mangaDirName)) {
@@ -283,7 +283,7 @@ class DownloadCache(
} }
suspend fun removeSource(source: Source) { suspend fun removeSource(source: Source) {
rootDownloadsDirMutex.withLock { rootDownloadsDirLock.withLock {
rootDownloadsDir.sourceDirs -= source.id rootDownloadsDir.sourceDirs -= source.id
} }
@@ -322,10 +322,10 @@ class DownloadCache(
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
rootDownloadsDirMutex.withLock { rootDownloadsDirLock.withLock {
val updatedRootDir = RootDirectory(storageManager.getDownloadsDirectory()) rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
updatedRootDir.sourceDirs = updatedRootDir.dir?.listFiles().orEmpty() val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() } .filter { it.isDirectory && !it.name.isNullOrBlank() }
.mapNotNull { dir -> .mapNotNull { dir ->
val sourceId = sourceMap[dir.name!!.lowercase()] val sourceId = sourceMap[dir.name!!.lowercase()]
@@ -333,7 +333,10 @@ class DownloadCache(
} }
.toMap() .toMap()
updatedRootDir.sourceDirs.values.map { sourceDir -> rootDownloadsDir.sourceDirs = sourceDirs
sourceDirs.values
.map { sourceDir ->
async { async {
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty() sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() } .filter { it.isDirectory && !it.name.isNullOrBlank() }
@@ -360,8 +363,6 @@ class DownloadCache(
} }
} }
.awaitAll() .awaitAll()
rootDownloadsDir = updatedRootDir
} }
_isInitializing.emit(false) _isInitializing.emit(false)
@@ -454,7 +455,7 @@ private object UniFileAsStringSerializer : KSerializer<UniFile?> {
override fun deserialize(decoder: Decoder): UniFile? { override fun deserialize(decoder: Decoder): UniFile? {
return if (decoder.decodeNotNullMark()) { return if (decoder.decodeNotNullMark()) {
UniFile.fromUri(Injekt.get<Application>(), decoder.decodeString().toUri()) UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
} else { } else {
decoder.decodeNull() decoder.decodeNull()
} }

View File

@@ -1,64 +0,0 @@
package eu.kanade.tachiyomi.data.export
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import tachiyomi.domain.manga.model.Manga
object LibraryExporter {
data class ExportOptions(
val includeTitle: Boolean,
val includeAuthor: Boolean,
val includeArtist: Boolean,
)
suspend fun exportToCsv(
context: Context,
uri: Uri,
favorites: List<Manga>,
options: ExportOptions,
onExportComplete: () -> Unit,
) {
withContext(Dispatchers.IO) {
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
val csvData = generateCsvData(favorites, options)
outputStream.write(csvData.toByteArray())
}
onExportComplete()
}
}
private val escapeRequired = listOf("\r", "\n", "\"", ",")
private fun generateCsvData(favorites: List<Manga>, options: ExportOptions): String {
val columnSize = listOf(
options.includeTitle,
options.includeAuthor,
options.includeArtist,
)
.count { it }
val rows = buildList(favorites.size) {
favorites.forEach { manga ->
buildList(columnSize) {
if (options.includeTitle) add(manga.title)
if (options.includeAuthor) add(manga.author)
if (options.includeArtist) add(manga.artist)
}
.let(::add)
}
}
return rows.joinToString("\r\n") { columns ->
columns.joinToString(",") columns@{ column ->
if (column.isNullOrBlank()) return@columns ""
if (escapeRequired.any { column.contains(it) }) {
column.replace("\"", "\"\"").let { "\"$it\"" }
} else {
column
}
}
}
}
}

View File

@@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.library
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build import android.os.Build
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.Constraints import androidx.work.Constraints
@@ -94,13 +92,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
if (tags.contains(WORK_NAME_AUTO)) { if (tags.contains(WORK_NAME_AUTO)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
val preferences = Injekt.get<LibraryPreferences>() val preferences = Injekt.get<LibraryPreferences>()
val restrictions = preferences.autoUpdateDeviceRestrictions().get() val restrictions = preferences.autoUpdateDeviceRestrictions().get()
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) { if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
return Result.retry() return Result.retry()
} }
}
// Find a running manual worker. If exists, try again later // Find a running manual worker. If exists, try again later
if (context.workManager.isRunning(WORK_NAME_MANUAL)) { if (context.workManager.isRunning(WORK_NAME_MANUAL)) {
@@ -436,24 +432,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val interval = prefInterval ?: preferences.autoUpdateInterval().get() val interval = prefInterval ?: preferences.autoUpdateInterval().get()
if (interval > 0) { if (interval > 0) {
val restrictions = preferences.autoUpdateDeviceRestrictions().get() val restrictions = preferences.autoUpdateDeviceRestrictions().get()
val networkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { val constraints = Constraints(
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
NetworkType.UNMETERED NetworkType.UNMETERED
} else { } else {
NetworkType.CONNECTED NetworkType.CONNECTED
} },
val networkRequestBuilder = NetworkRequest.Builder() requiresCharging = DEVICE_CHARGING in restrictions,
if (DEVICE_ONLY_ON_WIFI in restrictions) { requiresBatteryNotLow = true,
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) )
}
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
}
val constraints = Constraints.Builder()
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
.setRequiresCharging(DEVICE_CHARGING in restrictions)
.setRequiresBatteryNotLow(true)
.build()
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
interval.toLong(), interval.toLong(),

View File

@@ -71,7 +71,6 @@ object Notifications {
const val CHANNEL_APP_UPDATE = "app_apk_update_channel" const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
const val ID_APP_UPDATER = 1 const val ID_APP_UPDATER = 1
const val ID_APP_UPDATE_PROMPT = 2 const val ID_APP_UPDATE_PROMPT = 2
const val ID_APP_UPDATE_ERROR = 3
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel" const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
const val ID_UPDATES_TO_EXTS = -401 const val ID_UPDATES_TO_EXTS = -401
const val ID_EXTENSION_INSTALLER = -402 const val ID_EXTENSION_INSTALLER = -402

View File

@@ -175,7 +175,6 @@ sealed class Image(
} }
sealed interface Location { sealed interface Location {
@ConsistentCopyVisibility
data class Pictures private constructor(val relativePath: String) : Location { data class Pictures private constructor(val relativePath: String) : Location {
companion object { companion object {
fun create(relativePath: String = ""): Pictures { fun create(relativePath: String = ""): Pictures {

View File

@@ -37,8 +37,6 @@ abstract class BaseTracker(
// Application and remote support for reading dates // Application and remote support for reading dates
override val supportsReadingDates: Boolean = false override val supportsReadingDates: Boolean = false
override val supportsPrivateTracking: Boolean = false
// TODO: Store all scores as 10 point in the future maybe? // TODO: Store all scores as 10 point in the future maybe?
override fun get10PointScore(track: DomainTrack): Double { override fun get10PointScore(track: DomainTrack): Double {
return track.score return track.score
@@ -122,11 +120,6 @@ abstract class BaseTracker(
updateRemote(track) updateRemote(track)
} }
override suspend fun setRemotePrivate(track: Track, private: Boolean) {
track.private = private
updateRemote(track)
}
private suspend fun updateRemote(track: Track): Unit = withIOContext { private suspend fun updateRemote(track: Track): Unit = withIOContext {
try { try {
update(track) update(track)

View File

@@ -22,8 +22,6 @@ interface Tracker {
// Application and remote support for reading dates // Application and remote support for reading dates
val supportsReadingDates: Boolean val supportsReadingDates: Boolean
val supportsPrivateTracking: Boolean
@ColorInt @ColorInt
fun getLogoColor(): Int fun getLogoColor(): Int
@@ -84,6 +82,4 @@ interface Tracker {
suspend fun setRemoteStartDate(track: Track, epochMillis: Long) suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
suspend fun setRemotePrivate(track: Track, private: Boolean)
} }

View File

@@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -42,8 +43,6 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override val supportsReadingDates: Boolean = true override val supportsReadingDates: Boolean = true
override val supportsPrivateTracking: Boolean = true
private val scorePreference = trackPreferences.anilistScoreType() private val scorePreference = trackPreferences.anilistScoreType()
init { init {
@@ -184,7 +183,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val remoteTrack = api.findLibManga(track, getUsername().toInt()) val remoteTrack = api.findLibManga(track, getUsername().toInt())
return if (remoteTrack != null) { return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
if (track.status != COMPLETED) { if (track.status != COMPLETED) {

View File

@@ -42,8 +42,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val query = """ val query = """
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) { |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) { |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id | id
| status | status
|} |}
@@ -56,7 +56,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("mangaId", track.remote_id) put("mangaId", track.remote_id)
put("progress", track.last_chapter_read.toInt()) put("progress", track.last_chapter_read.toInt())
put("status", track.toApiStatus()) put("status", track.toApiStatus())
put("private", track.private)
} }
} }
with(json) { with(json) {
@@ -80,11 +79,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return withIOContext { return withIOContext {
val query = """ val query = """
|mutation UpdateManga( |mutation UpdateManga(
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean, |${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput |${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|) { |) {
|SaveMediaListEntry( |SaveMediaListEntry(
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private, |id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt |scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|) { |) {
|id |id
@@ -103,7 +102,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("score", track.score.toInt()) put("score", track.score.toInt())
put("startedAt", createDate(track.started_reading_date)) put("startedAt", createDate(track.started_reading_date))
put("completedAt", createDate(track.finished_reading_date)) put("completedAt", createDate(track.finished_reading_date))
put("private", track.private)
} }
} }
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
@@ -140,19 +138,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|Page (perPage: 50) { |Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|id |id
|staff {
|edges {
|role
|id
|node {
|name {
|full
|userPreferred
|native
|}
|}
|}
|}
|title { |title {
|userPreferred |userPreferred
|} |}
@@ -205,7 +190,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|status |status
|scoreRaw: score(format: POINT_100) |scoreRaw: score(format: POINT_100)
|progress |progress
|private
|startedAt { |startedAt {
|year |year
|month |month
@@ -233,19 +217,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|month |month
|day |day
|} |}
|staff {
|edges {
|role
|id
|node {
|name {
|full
|userPreferred
|native
|}
|}
|}
|}
|} |}
|} |}
|} |}

View File

@@ -19,7 +19,6 @@ data class ALManga(
val startDateFuzzy: Long, val startDateFuzzy: Long,
val totalChapters: Long, val totalChapters: Long,
val averageScore: Int, val averageScore: Int,
val staff: ALStaff,
) { ) {
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply { fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
remote_id = remoteId remote_id = remoteId
@@ -39,11 +38,6 @@ data class ALManga(
"" ""
} }
} }
staff.edges.forEach {
val name = it.node.name() ?: return@forEach
if ("Story" in it.role) authors += name
if ("Art" in it.role) artists += name
}
} }
} }
@@ -55,7 +49,6 @@ data class ALUserManga(
val startDateFuzzy: Long, val startDateFuzzy: Long,
val completedDateFuzzy: Long, val completedDateFuzzy: Long,
val manga: ALManga, val manga: ALManga,
val private: Boolean,
) { ) {
fun toTrack() = Track.create(TrackerManager.ANILIST).apply { fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
remote_id = manga.remoteId remote_id = manga.remoteId
@@ -67,7 +60,6 @@ data class ALUserManga(
last_chapter_read = chaptersRead.toDouble() last_chapter_read = chaptersRead.toDouble()
library_id = libraryId library_id = libraryId
total_chapters = manga.totalChapters total_chapters = manga.totalChapters
private = this@ALUserManga.private
} }
private fun toTrackStatus() = when (listStatus) { private fun toTrackStatus() = when (listStatus) {

View File

@@ -13,7 +13,6 @@ data class ALSearchItem(
val startDate: ALFuzzyDate, val startDate: ALFuzzyDate,
val chapters: Long?, val chapters: Long?,
val averageScore: Int?, val averageScore: Int?,
val staff: ALStaff,
) { ) {
fun toALManga(): ALManga = ALManga( fun toALManga(): ALManga = ALManga(
remoteId = id, remoteId = id,
@@ -25,7 +24,6 @@ data class ALSearchItem(
startDateFuzzy = startDate.toEpochMilli(), startDateFuzzy = startDate.toEpochMilli(),
totalChapters = chapters ?: 0, totalChapters = chapters ?: 0,
averageScore = averageScore ?: -1, averageScore = averageScore ?: -1,
staff = staff,
) )
} }
@@ -38,31 +36,3 @@ data class ALItemTitle(
data class ItemCover( data class ItemCover(
val large: String, val large: String,
) )
@Serializable
data class ALStaff(
val edges: List<ALEdge>,
)
@Serializable
data class ALEdge(
val role: String,
val id: Int,
val node: ALStaffNode,
)
@Serializable
data class ALStaffNode(
val name: ALStaffName,
)
@Serializable
data class ALStaffName(
val userPreferred: String?,
val native: String?,
val full: String?,
) {
operator fun invoke(): String? {
return userPreferred ?: full ?: native
}
}

View File

@@ -28,7 +28,6 @@ data class ALUserListItem(
val startedAt: ALFuzzyDate, val startedAt: ALFuzzyDate,
val completedAt: ALFuzzyDate, val completedAt: ALFuzzyDate,
val media: ALSearchItem, val media: ALSearchItem,
val private: Boolean,
) { ) {
fun toALUserManga(): ALUserManga { fun toALUserManga(): ALUserManga {
return ALUserManga( return ALUserManga(
@@ -39,7 +38,6 @@ data class ALUserListItem(
startDateFuzzy = startedAt.toEpochMilli(), startDateFuzzy = startedAt.toEpochMilli(),
completedDateFuzzy = completedAt.toEpochMilli(), completedDateFuzzy = completedAt.toEpochMilli(),
manga = media.toALManga(), manga = media.toALManga(),
private = private,
) )
} }
} }

View File

@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -22,8 +23,6 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
private val api by lazy { BangumiApi(id, client, interceptor) } private val api by lazy { BangumiApi(id, client, interceptor) }
override val supportsPrivateTracking: Boolean = true
override fun getScoreList(): ImmutableList<String> = SCORE_LIST override fun getScoreList(): ImmutableList<String> = SCORE_LIST
override fun displayScore(track: DomainTrack): String { override fun displayScore(track: DomainTrack): String {
@@ -49,23 +48,26 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
} }
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val statusTrack = api.statusLibManga(track, getUsername()) val statusTrack = api.statusLibManga(track)
return if (statusTrack != null) { val remoteTrack = api.findLibManga(track)
track.copyPersonalFrom(statusTrack, copyRemotePrivate = false) return if (remoteTrack != null && statusTrack != null) {
track.library_id = statusTrack.library_id track.copyPersonalFrom(remoteTrack)
track.score = statusTrack.score track.library_id = remoteTrack.library_id
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = statusTrack.total_chapters
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
track.status = if (hasReadChapters) READING else statusTrack.status track.status = if (hasReadChapters) READING else statusTrack.status
} }
update(track) track.score = statusTrack.score
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = remoteTrack.total_chapters
refresh(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.status = if (hasReadChapters) READING else PLAN_TO_READ track.status = if (hasReadChapters) READING else PLAN_TO_READ
track.score = 0.0 track.score = 0.0
add(track) add(track)
update(track)
} }
} }
@@ -74,8 +76,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
} }
override suspend fun refresh(track: Track): Track { override suspend fun refresh(track: Track): Track {
val remoteStatusTrack = api.statusLibManga(track, getUsername()) ?: throw Exception("Could not find manga") val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
track.copyPersonalFrom(remoteStatusTrack) track.copyPersonalFrom(remoteStatusTrack)
api.findLibManga(track)?.let { remoteTrack ->
track.total_chapters = remoteTrack.total_chapters
}
return track return track
} }
@@ -108,12 +113,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
try { try {
val oauth = api.accessToken(code) val oauth = api.accessToken(code)
interceptor.newAuth(oauth) interceptor.newAuth(oauth)
// Users can set a 'username' (not nickname) once which effectively saveCredentials(oauth.userId.toString(), oauth.accessToken)
// replaces the stringified ID in certain queries. } catch (e: Throwable) {
// If no username is set, the API returns the user ID as a strings
var username = api.getUsername()
saveCredentials(username, oauth.accessToken)
} catch (_: Throwable) {
logout() logout()
} }
} }
@@ -125,7 +126,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
fun restoreToken(): BGMOAuth? { fun restoreToken(): BGMOAuth? {
return try { return try {
json.decodeFromString<BGMOAuth>(trackPreferences.trackToken(this).get()) json.decodeFromString<BGMOAuth>(trackPreferences.trackToken(this).get())
} catch (_: Exception) { } catch (e: Exception) {
null null
} }
} }
@@ -137,11 +138,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
} }
companion object { companion object {
const val PLAN_TO_READ = 1L
const val COMPLETED = 2L
const val READING = 3L const val READING = 3L
const val COMPLETED = 2L
const val ON_HOLD = 4L const val ON_HOLD = 4L
const val DROPPED = 5L const val DROPPED = 5L
const val PLAN_TO_READ = 1L
private val SCORE_LIST = IntRange(0, 10) private val SCORE_LIST = IntRange(0, 10)
.map(Int::toString) .map(Int::toString)

View File

@@ -5,28 +5,22 @@ import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMUser
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers.Companion.headersOf
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class BangumiApi( class BangumiApi(
private val trackId: Long, private val trackId: Long,
@@ -40,17 +34,11 @@ class BangumiApi(
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val url = "$API_URL/v0/users/-/collections/${track.remote_id}" val body = FormBody.Builder()
val body = buildJsonObject { .add("rating", track.score.toInt().toString())
put("type", track.toApiStatus()) .add("status", track.toApiStatus())
put("rate", track.score.toInt().coerceIn(0, 10)) .build()
put("ep_status", track.last_chapter_read.toInt()) authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body))
put("private", track.private)
}
.toString()
.toRequestBody()
// Returns with 202 Accepted on success with no body
authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
.awaitSuccess() .awaitSuccess()
track track
} }
@@ -58,110 +46,106 @@ class BangumiApi(
suspend fun updateLibManga(track: Track): Track { suspend fun updateLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val url = "$API_URL/v0/users/-/collections/${track.remote_id}" // read status update
val body = buildJsonObject { val sbody = FormBody.Builder()
put("type", track.toApiStatus()) .add("rating", track.score.toInt().toString())
put("rate", track.score.toInt().coerceIn(0, 10)) .add("status", track.toApiStatus())
put("ep_status", track.last_chapter_read.toInt())
put("private", track.private)
}
.toString()
.toRequestBody()
val request = Request.Builder()
.url(url)
.patch(body)
.headers(headersOf("Content-Type", APP_JSON))
.build() .build()
// Returns with 204 No Content authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody))
authClient.newCall(request)
.awaitSuccess() .awaitSuccess()
// chapter update
val body = FormBody.Builder()
.add("watched_eps", track.last_chapter_read.toInt().toString())
.build()
authClient.newCall(
POST("$API_URL/subject/${track.remote_id}/update/watched_eps", body = body),
).awaitSuccess()
track track
} }
} }
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
// This API is marked as experimental in the documentation
// but that has been the case since 2022 with few significant
// changes to the schema for this endpoint since
// "实验性 API 本 schema 和实际的 API 行为都可能随时发生改动"
return withIOContext { return withIOContext {
val url = "$API_URL/v0/search/subjects?limit=20" val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
val body = buildJsonObject { .toUri()
put("keyword", search) .buildUpon()
put("sort", "match") .appendQueryParameter("max_results", "20")
putJsonObject("filter") { .build()
putJsonArray("type") {
add(1) // "Book" (书籍) type
}
}
}
.toString()
.toRequestBody()
with(json) { with(json) {
authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON))) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<BGMSearchResult>() .parseAs<BGMSearchResult>()
.data .let { result ->
.map { it.toTrackSearch(trackId) } if (result.code == 404) emptyList<TrackSearch>()
result.list
?.filter { it.type == 1 }
?.map { it.toTrackSearch(trackId) }
.orEmpty()
}
} }
} }
} }
suspend fun statusLibManga(track: Track, username: String): Track? { suspend fun findLibManga(track: Track): Track? {
return withIOContext { return withIOContext {
val url = "$API_URL/v0/users/$username/collections/${track.remote_id}"
with(json) { with(json) {
try { authClient.newCall(GET("$API_URL/subject/${track.remote_id}"))
authClient.newCall(GET(url, cache = CacheControl.FORCE_NETWORK)) .awaitSuccess()
.parseAs<BGMSearchItem>()
.toTrackSearch(trackId)
}
}
}
suspend fun statusLibManga(track: Track): Track? {
return withIOContext {
val urlUserRead = "$API_URL/collection/${track.remote_id}"
val requestUserRead = Request.Builder()
.url(urlUserRead)
.cacheControl(CacheControl.FORCE_NETWORK)
.get()
.build()
// TODO: get user readed chapter here
with(json) {
authClient.newCall(requestUserRead)
.awaitSuccess() .awaitSuccess()
.parseAs<BGMCollectionResponse>() .parseAs<BGMCollectionResponse>()
.let { .let {
track.status = it.getStatus() if (it.code == 400) return@let null
track.last_chapter_read = it.epStatus?.toDouble() ?: 0.0
track.score = it.rate?.toDouble() ?: 0.0 track.status = it.status?.id!!
track.total_chapters = it.subject?.eps?.toLong() ?: 0L track.last_chapter_read = it.epStatus!!.toDouble()
track.score = it.rating!!
track track
} }
} catch (e: HttpException) {
if (e.code == 404) { // "subject is not collected by user"
null
} else {
throw e
}
}
} }
} }
} }
suspend fun accessToken(code: String): BGMOAuth { suspend fun accessToken(code: String): BGMOAuth {
return withIOContext { return withIOContext {
val body = FormBody.Builder() with(json) {
client.newCall(accessTokenRequest(code))
.awaitSuccess()
.parseAs()
}
}
}
private fun accessTokenRequest(code: String) = POST(
OAUTH_URL,
body = FormBody.Builder()
.add("grant_type", "authorization_code") .add("grant_type", "authorization_code")
.add("client_id", CLIENT_ID) .add("client_id", CLIENT_ID)
.add("client_secret", CLIENT_SECRET) .add("client_secret", CLIENT_SECRET)
.add("code", code) .add("code", code)
.add("redirect_uri", REDIRECT_URL) .add("redirect_uri", REDIRECT_URL)
.build() .build(),
with(json) { )
client.newCall(POST(OAUTH_URL, body = body))
.awaitSuccess()
.parseAs<BGMOAuth>()
}
}
}
suspend fun getUsername(): String {
return withIOContext {
with(json) {
authClient.newCall(GET("$API_URL/v0/me"))
.awaitSuccess()
.parseAs<BGMUser>()
.username
}
}
}
companion object { companion object {
private const val CLIENT_ID = "bgm291665acbd06a4c28" private const val CLIENT_ID = "bgm291665acbd06a4c28"
@@ -173,8 +157,6 @@ class BangumiApi(
private const val REDIRECT_URL = "mihon://bangumi-auth" private const val REDIRECT_URL = "mihon://bangumi-auth"
private const val APP_JSON = "application/json"
fun authUrl(): Uri = fun authUrl(): Uri =
LOGIN_URL.toUri().buildUpon() LOGIN_URL.toUri().buildUpon()
.appendQueryParameter("client_id", CLIENT_ID) .appendQueryParameter("client_id", CLIENT_ID)

View File

@@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -20,13 +21,12 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
var currAuth: BGMOAuth = oauth ?: throw Exception("Not authenticated with Bangumi") val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
if (currAuth.isExpired()) { if (currAuth.isExpired()) {
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!)) val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!))
if (response.isSuccessful) { if (response.isSuccessful) {
currAuth = json.decodeFromString<BGMOAuth>(response.body.string()) newAuth(json.decodeFromString<BGMOAuth>(response.body.string()))
newAuth(currAuth)
} else { } else {
response.close() response.close()
} }
@@ -38,7 +38,14 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
"antsylich/Mihon/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/mihonapp/mihon)", "antsylich/Mihon/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/mihonapp/mihon)",
) )
.apply { .apply {
addHeader("Authorization", "Bearer ${currAuth.accessToken}") if (originalRequest.method == "GET") {
val newUrl = originalRequest.url.newBuilder()
.addQueryParameter("access_token", currAuth.accessToken)
.build()
url(newUrl)
} else {
post(addToken(currAuth.accessToken, originalRequest.body as FormBody))
}
} }
.build() .build()
.let(chain::proceed) .let(chain::proceed)
@@ -60,4 +67,13 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
bangumi.saveToken(oauth) bangumi.saveToken(oauth)
} }
private fun addToken(token: String, oidFormBody: FormBody): FormBody {
val newFormBody = FormBody.Builder()
for (i in 0..<oidFormBody.size) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", token)
return newFormBody.build()
}
} }

View File

@@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.data.track.bangumi
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
fun Track.toApiStatus() = when (status) { fun Track.toApiStatus() = when (status) {
Bangumi.PLAN_TO_READ -> 1 Bangumi.READING -> "do"
Bangumi.COMPLETED -> 2 Bangumi.COMPLETED -> "collect"
Bangumi.READING -> 3 Bangumi.ON_HOLD -> "on_hold"
Bangumi.ON_HOLD -> 4 Bangumi.DROPPED -> "dropped"
Bangumi.DROPPED -> 5 Bangumi.PLAN_TO_READ -> "wish"
else -> throw NotImplementedError("Unknown status: $status") else -> throw NotImplementedError("Unknown status: $status")
} }

View File

@@ -1,34 +1,28 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto package eu.kanade.tachiyomi.data.track.bangumi.dto
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
// Incomplete DTO with only our needed attributes
data class BGMCollectionResponse( data class BGMCollectionResponse(
val rate: Int?, val code: Int?,
val type: Int?, val `private`: Int? = 0,
val comment: String? = "",
@SerialName("ep_status") @SerialName("ep_status")
val epStatus: Int? = 0, val epStatus: Int? = 0,
@SerialName("lasttouch")
val lastTouch: Int? = 0,
val rating: Double? = 0.0,
val status: Status? = Status(),
val tag: List<String?>? = emptyList(),
val user: User? = User(),
@SerialName("vol_status") @SerialName("vol_status")
val volStatus: Int? = 0, val volStatus: Int? = 0,
val private: Boolean = false, )
val subject: BGMSlimSubject? = null,
) {
fun getStatus(): Long = when (type) {
1 -> Bangumi.PLAN_TO_READ
2 -> Bangumi.COMPLETED
3 -> Bangumi.READING
4 -> Bangumi.ON_HOLD
5 -> Bangumi.DROPPED
else -> throw NotImplementedError("Unknown status: $type")
}
}
@Serializable @Serializable
// Incomplete DTO with only our needed attributes data class Status(
data class BGMSlimSubject( val id: Long? = 0,
val volumes: Int?, val name: String? = "",
val eps: Int?, val type: String? = "",
) )

View File

@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto package eu.kanade.tachiyomi.data.track.bangumi.dto
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -11,7 +10,6 @@ data class BGMOAuth(
@SerialName("token_type") @SerialName("token_type")
val tokenType: String, val tokenType: String,
@SerialName("created_at") @SerialName("created_at")
@EncodeDefault
val createdAt: Long = System.currentTimeMillis() / 1000, val createdAt: Long = System.currentTimeMillis() / 1000,
@SerialName("expires_in") @SerialName("expires_in")
val expiresIn: Long, val expiresIn: Long,

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