Compare commits

..

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

380 changed files with 4714 additions and 8227 deletions

View file

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

@ -35,24 +35,9 @@ body:
required: true required: true
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help.
required: true required: true
- label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**. - label: I have updated the app to version **[1.9.7](https://github.com/null2264/yokai/releases/latest)**.
required: true required: true
- label: I have checked through the app settings for my feature. - label: I have checked through the app settings for my feature.
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
- type: "textarea"
id: "prioritisation"
attributes:
label: "Is this issue important to you?"
description: |
**Please do not modify this text area!**
This template let users to vote with a :+1: reaction if they find it important.
This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important.
value: |
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

View file

@ -94,30 +94,15 @@ body:
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true required: true
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help ([Browse → Extensions → Find the extension → Settings → Tap the `[<>]` icon](https://cdn.aap.my.id/extension-repo-link.png), it *should* redirect you to the maintainer). - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help.
required: true required: true
- label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon). - label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon)
required: true required: true
- label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/). - label: I have tried the [troubleshooting guide](https://mihon.app/help/).
required: true required: true
- label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**. - label: I have updated the app to version **[1.9.7](https://github.com/null2264/yokai/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true
- label: I have filled out all of the requested information in this form. - label: I have filled out all of the requested information in this form.
required: true required: true
- type: "textarea"
id: "prioritisation"
attributes:
label: "Is this issue important to you?"
description: |
**Please do not modify this text area!**
This template let users to vote with a :+1: reaction if they find it important.
This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important.
value: |
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

View file

@ -1,11 +0,0 @@
<!--
^ Please summarise the changes you have made here ^
-->
---
Add a :+1: [reaction] to [pull requests you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[pull requests you find important]: https://github.com/null2264/yokai/pulls?q=is%3Aopen+sort%3Areactions-%2B1-desc

View file

@ -4,15 +4,7 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
on: on: [pull_request]
pull_request:
paths:
- '**'
- '!**.md'
- '!i18n/src/commonMain/moko-resources/**/strings.xml'
- '!i18n/src/commonMain/moko-resources/**/plurals.xml'
- 'i18n/src/commonMain/moko-resources/base/strings.xml'
- 'i18n/src/commonMain/moko-resources/base/plurals.xml'
jobs: jobs:
build: build:
@ -30,21 +22,18 @@ jobs:
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0" ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
- name: Setup Gradle - name: Setup Gradle
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94 uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
with: with:
java: 17 java: 17
distro: temurin distro: adopt
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build the app - name: Build and run tests
run: ./gradlew assembleStandardRelease run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Run unit tests
run: ./gradlew testReleaseUnitTest testStandardReleaseUnitTest
- name: Publish test report - name: Publish test report
uses: mikepenz/action-junit-report@v5 uses: mikepenz/action-junit-report@v5

View file

@ -43,10 +43,10 @@ jobs:
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0" ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
- name: Setup Gradle - name: Setup Gradle
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94 uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
with: with:
java: 17 java: 17
distro: temurin distro: adopt
- name: Setup CHANGELOG parser - name: Setup CHANGELOG parser
uses: taiki-e/install-action@parse-changelog uses: taiki-e/install-action@parse-changelog
@ -80,7 +80,6 @@ jobs:
run: | run: |
set -x set -x
echo "VERSION_TAG=v${{github.event.inputs.version}}" >> $GITHUB_ENV echo "VERSION_TAG=v${{github.event.inputs.version}}" >> $GITHUB_ENV
echo "BUILD_TYPE=StandardRelease" >> $GITHUB_ENV
# BETA # BETA
- name: Prepare beta build - name: Prepare beta build
@ -89,14 +88,9 @@ jobs:
set -x set -x
BETA_COUNT=$(git tag -l --sort=refname "v${{github.event.inputs.version}}-b*" | tail -n1 | sed "s/^\S*-b//g") BETA_COUNT=$(git tag -l --sort=refname "v${{github.event.inputs.version}}-b*" | tail -n1 | sed "s/^\S*-b//g")
if [ -z "$BETA_COUNT" ]; then [ "$BETA_COUNT" = "" ] && BETA_COUNT="1" || BETA_COUNT=$((BETA_COUNT+1))
BETA_COUNT="1"
else
BETA_COUNT=$((BETA_COUNT+1))
fi
echo "VERSION_TAG=v${{github.event.inputs.version}}-b${BETA_COUNT}" >> $GITHUB_ENV echo "VERSION_TAG=v${{github.event.inputs.version}}-b${BETA_COUNT}" >> $GITHUB_ENV
echo "BUILD_TYPE=StandardBeta" >> $GITHUB_ENV
# NIGHTLY # NIGHTLY
- name: Prepare nightly build - name: Prepare nightly build
@ -104,17 +98,23 @@ jobs:
run: | run: |
set -x set -x
echo "VERSION_TAG=r$(git rev-list --count HEAD)" >> $GITHUB_ENV echo "VERSION_TAG=r$(git rev-list --count HEAD)" >> $GITHUB_ENV
echo "BUILD_TYPE=StandardNightly" >> $GITHUB_ENV
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
echo "COMMIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "COMMIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build the app # PROD
if: startsWith(env.BUILD_TYPE, 'Standard') - name: Build release build and run tests
run: ./gradlew assemble${{ env.BUILD_TYPE }} if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta != 'true'
run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Run unit tests # BETA
if: startsWith(env.BUILD_TYPE, 'Standard') - name: Build beta build and run tests
run: ./gradlew testReleaseUnitTest test${{ env.BUILD_TYPE }}UnitTest if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta == 'true'
run: ./gradlew assembleStandardBeta testReleaseUnitTest testStandardBetaUnitTest
# NIGHTLY
- name: Build nightly build and run tests
if: startsWith(env.VERSION_TAG, 'r')
run: ./gradlew assembleStandardNightly testReleaseUnitTest testStandardNightlyUnitTest
- name: Upload R8 APK to artifact - name: Upload R8 APK to artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -154,7 +154,7 @@ jobs:
echo "STAGE=${stage}" >> $GITHUB_OUTPUT echo "STAGE=${stage}" >> $GITHUB_OUTPUT
- name: Sign APK - name: Sign APK
uses: null2264/actions/android-signer@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94 uses: null2264/actions/android-signer@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
if: env.VERSION_TAG != '' if: env.VERSION_TAG != ''
with: with:
releaseDir: app/build/outputs/apk/standard/${{ steps.version_stage.outputs.STAGE }} releaseDir: app/build/outputs/apk/standard/${{ steps.version_stage.outputs.STAGE }}

1
.gitignore vendored
View file

@ -10,4 +10,3 @@
*/*/build */*/build
.kotlin/ .kotlin/
kls_database.db kls_database.db
weblate.conf

View file

@ -24,8 +24,6 @@
'com.github.tachiyomiorg:image-decoder', 'com.github.tachiyomiorg:image-decoder',
'com.github.tachiyomiorg:unifile', 'com.github.tachiyomiorg:unifile',
'com.github.tachiyomiorg:conductor-support-preference', 'com.github.tachiyomiorg:conductor-support-preference',
'com.github.chrisbanes:PhotoView',
'com.github.PhilJay:MPAndroidChart',
], ],
enabled: false, enabled: false,
}, },

View file

@ -6,101 +6,9 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co
- `Additions` - New features - `Additions` - New features
- `Changes` - Behaviour/visual changes - `Changes` - Behaviour/visual changes
- `Fixes` - Bugfixes - `Fixes` - Bugfixes
- `Translation` - Translation changes/updates
- `Other` - Technical changes/updates - `Other` - Technical changes/updates
## [Unreleased]
### Additions
- Add random library sort
- Add the ability to save search queries
- Add toggle to enable/disable hide source on swipe (@Hiirbaf)
- Add the ability to mark duplicate read chapters as read (@AntsyLich)
### Changes
- Temporarily disable log file
- Categories' header now show filtered count when you search the library when you have "Show number of items" enabled (@LeeSF03)
- Chapter progress now saved everything the page is changed
### Fixes
- Allow users to bypass onboarding's permission step if Shizuku is installed
- Fix Recents page shows "No recent chapters" instead of a loading screen
- Fix not fully loaded entries can't be selected on Library page
- Fix certain Infinix devices being unable to use any "Open link in browser" actions, including tracker setup (@MajorTanya)
- Fix source filter bottom sheet unable to be fully scrolled to the bottom
- Prevent potential "Comparison method violates its general contract!" crash
- Fix staggered grid cover being squashed for local source (@AwkwardPeak7)
### Translation
- Update translations from Weblate
### Other
- Refactor Library to utilize Flow even more
- Refactor EmptyView to use Compose
- Refactor Reader ChapterTransition to use Compose (@arkon)
- [Experimental] Add modified version of LargeTopAppBar that mimic J2K's ExpandedAppBarLayout
- Refactor About page to use Compose
- Adjust Compose-based pages' transition to match J2K's Conductor transition
- Resolve deprecation warnings
- Kotlin's context-receiver, schedule for removal on Kotlin v2.1.x and planned to be replaced by context-parameters on Kotlin v2.2
- Project.exec -> Providers.exec
- Remove internal API usage to retrieve Kotlin version for kotlin-stdlib
- Move :core module to :core:main
- Move archive related code to :core:archive (@AntsyLich)
- Refactor Library to store LibraryMap instead of flatten list of LibraryItem
- LibraryItem abstraction to make it easier to manage
- LibraryManga no longer extend MangaImpl
- Update dependency gradle to v8.12
- Update user agent (@Hiirbaf)
- Update serialization to v1.8.1
- Update dependency io.github.fornewid:material-motion-compose-core to v1.2.1
- Update lifecycle to v2.9.0
- Update dependency org.jsoup:jsoup to v1.20.1
- Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0
- Update dependency io.mockk:mockk to v1.14.2
- Update dependency io.coil-kt.coil3:coil-bom to v3.2.0
- Update dependency com.squareup.okio:okio to v3.12.0
- Update dependency com.google.firebase:firebase-bom to v33.14.0
- Update dependency com.google.accompanist:accompanist-themeadapter-material3 to v0.36.0
- Update dependency com.github.requery:sqlite-android to v3.49.0
- Update dependency com.getkeepsafe.taptargetview:taptargetview to v1.15.0
- Update dependency androidx.window:window to v1.4.0
- Update dependency androidx.webkit:webkit to v1.13.0
- Update dependency androidx.sqlite:sqlite-ktx to v2.5.1
- Update dependency androidx.sqlite:sqlite to v2.5.1
- Update dependency androidx.recyclerview:recyclerview to v1.4.0
- Update dependency androidx.core:core-ktx to v1.16.0
- Update dependency androidx.compose:compose-bom to v2025.05.01
- Update aboutlibraries to v11.6.3
- Update plugin kotlinter to v5.1.0
- Update plugin gradle-versions to v0.52.0
- Update okhttp monorepo to v5.0.0-alpha.16
- Update moko to v0.24.5
- Update kotlin monorepo to v2.1.21
- Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.2
- Update dependency me.zhanghai.android.libarchive:library to v1.1.5
- Update dependency io.insert-koin:koin-bom to v4.0.4
- Update dependency com.android.tools:desugar_jdk_libs to v2.1.5
- Update dependency androidx.work:work-runtime-ktx to v2.10.1
- Update dependency androidx.constraintlayout:constraintlayout to v2.2.1
- Update plugin firebase-crashlytics to v3.0.3
- Update null2264/actions digest to 363cb9c
- Update dependency io.github.pdvrieze.xmlutil:core-android to v0.91.1
## [1.9.7.3]
### Fixes
- More `Comparison method violates its general contract!` crash prevention
## [1.9.7.2]
### Fixes
- Fix MyAnimeList timeout issue
## [1.9.7.1]
### Fixes
- Prevent `Comparison method violates its general contract!` crashes
## [1.9.7] ## [1.9.7]
### Changes ### Changes

View file

@ -12,13 +12,10 @@
A free and open source manga reader A free and open source manga reader
[![CI](https://github.com/null2264/yokai/actions/workflows/build_push.yml/badge.svg)](https://github.com/null2264/yokai/actions/workflows/build_push.yml)
[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](/LICENSE)
[![Discord: Mihon](https://img.shields.io/discord/1195734228319617024.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF)](https://discord.gg/mihon) [![Discord: Mihon](https://img.shields.io/discord/1195734228319617024.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF)](https://discord.gg/mihon)
[![Mirror: GitLab](https://img.shields.io/badge/mirror-GitLab-orange.svg?labelColor=27303D)](https://gitlab.com/null2264/yokai) [![Mirror: GitLab](https://img.shields.io/badge/mirror-GitLab-orange.svg)](https://gitlab.com/null2264/yokai)
[![Mirror: git.aap](https://img.shields.io/badge/mirror-git.aap-red.svg?labelColor=27303D)](https://git.aap.my.id/null2264/yokai)
[![CI](https://github.com/null2264/yokai/actions/workflows/build_push.yml/badge.svg?labelColor=27303D)](https://github.com/null2264/yokai/actions/workflows/build_push.yml)
[![License: Apache-2.0](https://img.shields.io/github/license/null2264/yokai?labelColor=27303D&color=0877d2)](/LICENSE)
[![Translation status](https://img.shields.io/weblate/progress/yokai?labelColor=27303D&color=946300)](https://hosted.weblate.org/engage/yokai/)
<img src="./.github/readme-images/screens.gif" alt="Yokai screenshots" /> <img src="./.github/readme-images/screens.gif" alt="Yokai screenshots" />

View file

@ -1,13 +1,15 @@
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPlugin import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPlugin
import com.google.gms.googleservices.GoogleServicesPlugin import com.google.gms.googleservices.GoogleServicesPlugin
import java.io.ByteArrayOutputStream
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("yokai.android.application") alias(androidx.plugins.application)
id("yokai.android.application.compose") alias(kotlinx.plugins.android)
alias(kotlinx.plugins.compose.compiler)
alias(kotlinx.plugins.serialization) alias(kotlinx.plugins.serialization)
alias(kotlinx.plugins.parcelize) alias(kotlinx.plugins.parcelize)
alias(libs.plugins.aboutlibraries) alias(libs.plugins.aboutlibraries)
@ -21,12 +23,15 @@ if (gradle.startParameter.taskRequests.toString().contains("standard", true)) {
} }
fun runCommand(command: String): String { fun runCommand(command: String): String {
val result = providers.exec { commandLine(command.split(" ")) } val byteOut = ByteArrayOutputStream()
return result.standardOutput.asText.get().trim() project.exec {
commandLine = command.split(" ")
standardOutput = byteOut
}
return String(byteOut.toByteArray()).trim()
} }
@Suppress("PropertyName") val _versionName = "1.9.8"
val _versionName = "1.10.0"
val betaCount by lazy { val betaCount by lazy {
val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*")
@ -49,7 +54,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android { android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi" applicationId = "eu.kanade.tachiyomi"
versionCode = 158 versionCode = 157
versionName = _versionName versionName = _versionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled = true multiDexEnabled = true
@ -67,6 +72,11 @@ android {
//noinspection ChromeOsAbiSupport //noinspection ChromeOsAbiSupport
abiFilters += supportedAbis abiFilters += supportedAbis
} }
externalNativeBuild {
cmake {
this.arguments("-DHAVE_LIBJXL=FALSE")
}
}
} }
splits { splits {
@ -112,6 +122,7 @@ android {
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
compose = true
// If you're here because there's not BuildConfig, build the app first, it'll generate it for you // If you're here because there's not BuildConfig, build the app first, it'll generate it for you
buildConfig = true buildConfig = true
@ -145,8 +156,7 @@ android {
} }
dependencies { dependencies {
implementation(projects.core.archive) implementation(projects.core)
implementation(projects.core.main)
implementation(projects.data) implementation(projects.data)
implementation(projects.domain) implementation(projects.domain)
implementation(projects.i18n) implementation(projects.i18n)
@ -241,6 +251,8 @@ dependencies {
implementation(libs.shizuku.api) implementation(libs.shizuku.api)
implementation(libs.shizuku.provider) implementation(libs.shizuku.provider)
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION))
implementation(platform(kotlinx.coroutines.bom)) implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines) implementation(kotlinx.bundles.coroutines)
@ -268,15 +280,17 @@ tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> { withType<KotlinCompile> {
compilerOptions.freeCompilerArgs.addAll( compilerOptions.freeCompilerArgs.addAll(
"-Xcontext-receivers",
// "-opt-in=kotlin.Experimental", // "-opt-in=kotlin.Experimental",
"-opt-in=kotlin.RequiresOptIn", "-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.ExperimentalStdlibApi", "-opt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
// "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
// "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", // "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview",
@ -284,6 +298,19 @@ tasks {
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
) )
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
compilerOptions.freeCompilerArgs.addAll(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
(project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics",
)
compilerOptions.freeCompilerArgs.addAll(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
(project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics",
)
}
} }
// Duplicating Hebrew string assets due to some locale code issues on different devices // Duplicating Hebrew string assets due to some locale code issues on different devices

View file

@ -300,7 +300,7 @@ fun buildLogWritersToAdd(
) = buildList { ) = buildList {
if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter()) if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter())
// if (logPath != null && !BuildConfig.DEBUG) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose)) if (logPath != null) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose))
} }
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE" private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"

View file

@ -40,13 +40,9 @@ import eu.kanade.tachiyomi.util.system.launchIO
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToLong
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.models.cover import yokai.domain.manga.models.cover
import yokai.domain.recents.interactor.GetRecents
class UpdatesGridGlanceWidget : GlanceAppWidget() { class UpdatesGridGlanceWidget : GlanceAppWidget() {
private val app: Application by injectLazy() private val app: Application by injectLazy()
@ -68,33 +64,6 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
} }
} }
// FIXME: Don't depends on RecentsPresenter
private suspend fun getUpdates(customAmount: Int = 0, getRecents: GetRecents = Injekt.get()): List<Pair<Manga, Long>> {
return getRecents
.awaitUpdates(
limit = when {
customAmount > 0 -> (customAmount * 1.5).roundToLong()
else -> 25L
}
)
.mapNotNull {
when {
it.chapter.read || it.chapter.id == null -> RecentsPresenter.getNextChapter(it.manga)
it.history.id == null -> RecentsPresenter.getFirstUpdatedChapter(it.manga, it.chapter)
else -> it.chapter
} ?: return@mapNotNull null
it
}
.asSequence()
.distinctBy { it.manga.id }
.sortedByDescending { it.history.last_read }
// nChapterItems + nAdditionalItems + cReadingItems
.take((RecentsPresenter.UPDATES_CHAPTER_LIMIT * 2) + RecentsPresenter.UPDATES_READING_LIMIT_LOWER)
.filter { it.manga.id != null }
.map { it.manga to it.history.last_read }
.toList()
}
fun loadData(list: List<Pair<Manga, Long>>? = null) { fun loadData(list: List<Pair<Manga, Long>>? = null) {
coroutineScope.launchIO { coroutineScope.launchIO {
// Don't show anything when lock is active // Don't show anything when lock is active
@ -111,7 +80,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
.flatMap { manager.getAppWidgetSizes(it) } .flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value } .maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount() .calculateRowAndColumnCount()
val processList = list ?: getUpdates(customAmount = min(50, rowCount * columnCount)) val processList = list ?: RecentsPresenter.getRecentManga(customAmount = min(50, rowCount * columnCount))
data = prepareList(processList, rowCount * columnCount) data = prepareList(processList, rowCount * columnCount)
ids.forEach { update(app, it) } ids.forEach { update(app, it) }

View file

@ -17,15 +17,15 @@ import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider import androidx.glance.unit.ColorProvider
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import eu.kanade.tachiyomi.appwidget.ContainerModifier import eu.kanade.tachiyomi.appwidget.ContainerModifier
import eu.kanade.tachiyomi.appwidget.util.stringResource import eu.kanade.tachiyomi.appwidget.util.stringResource
import yokai.i18n.MR import eu.kanade.tachiyomi.ui.main.MainActivity
import yokai.presentation.core.Constants
@Composable @Composable
fun LockedWidget() { fun LockedWidget() {
val context = LocalContext.current val context = LocalContext.current
val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply { val intent = Intent(LocalContext.current, Class.forName(MainActivity.MAIN_ACTIVITY)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
Box( Box(

View file

@ -17,18 +17,19 @@ import androidx.glance.layout.Row
import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding import androidx.glance.layout.padding
import androidx.glance.text.Text import androidx.glance.text.Text
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import eu.kanade.tachiyomi.appwidget.ContainerModifier import eu.kanade.tachiyomi.appwidget.ContainerModifier
import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount
import eu.kanade.tachiyomi.appwidget.util.stringResource import eu.kanade.tachiyomi.appwidget.util.stringResource
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.SearchActivity import eu.kanade.tachiyomi.ui.main.SearchActivity
import yokai.i18n.MR
import yokai.presentation.core.Constants
@Composable @Composable
fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) { fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount() val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(Constants.SHORTCUT_RECENTS) val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(MainActivity.SHORTCUT_RECENTS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
Column( Column(
modifier = ContainerModifier.clickable(actionStartActivity(mainIntent)), modifier = ContainerModifier.clickable(actionStartActivity(mainIntent)),

View file

@ -5,17 +5,9 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import eu.kanade.tachiyomi.core.preference.Preference import eu.kanade.tachiyomi.core.preference.Preference
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
@Composable @Composable
fun <T> Preference<T>.collectAsState(): State<T> { fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() } val flow = remember(this) { changes() }
return flow.collectAsState(initial = get()) return flow.collectAsState(initial = get())
} }
fun String.asDateFormat(): DateFormat = when (this) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(this, Locale.getDefault())
}

View file

@ -57,10 +57,8 @@ data class BackupManga(
@ProtoNumber(805) var customGenre: List<String>? = null, @ProtoNumber(805) var customGenre: List<String>? = null,
) { ) {
fun getMangaImpl(): MangaImpl { fun getMangaImpl(): MangaImpl {
return MangaImpl( return MangaImpl().apply {
source = this.source, url = this@BackupManga.url
url = this.url,
).apply {
title = this@BackupManga.title title = this@BackupManga.title
artist = this@BackupManga.artist artist = this@BackupManga.artist
author = this@BackupManga.author author = this@BackupManga.author
@ -69,6 +67,7 @@ data class BackupManga(
status = this@BackupManga.status status = this@BackupManga.status
thumbnail_url = this@BackupManga.thumbnailUrl thumbnail_url = this@BackupManga.thumbnailUrl
favorite = this@BackupManga.favorite favorite = this@BackupManga.favorite
source = this@BackupManga.source
date_added = this@BackupManga.dateAdded date_added = this@BackupManga.dateAdded
viewer_flags = ( viewer_flags = (
this@BackupManga.viewer_flags this@BackupManga.viewer_flags

View file

@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
import android.content.Context import android.content.Context
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.ui.library.LibrarySort import eu.kanade.tachiyomi.ui.library.LibrarySort
import java.io.Serializable
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import java.io.Serializable
interface Category : Serializable { interface Category : Serializable {
@ -37,7 +37,8 @@ interface Category : Serializable {
return ((mangaSort?.minus('a') ?: 0) % 2) != 1 return ((mangaSort?.minus('a') ?: 0) % 2) != 1
} }
fun sortingMode(): LibrarySort? = LibrarySort.valueOf(mangaSort) fun sortingMode(nullAsDND: Boolean = false): LibrarySort? = LibrarySort.valueOf(mangaSort)
?: if (nullAsDND && !isDynamic) LibrarySort.DragAndDrop else null
val isDragAndDrop val isDragAndDrop
get() = ( get() = (
@ -55,21 +56,7 @@ interface Category : Serializable {
fun mangaOrderToString(): String = fun mangaOrderToString(): String =
if (mangaSort != null) mangaSort.toString() else mangaOrder.joinToString("/") if (mangaSort != null) mangaSort.toString() else mangaOrder.joinToString("/")
// For dynamic categories
fun dynamicHeaderKey(): String {
if (!isDynamic) throw IllegalStateException("This category is not a dynamic category")
return when {
sourceId != null -> "${name}$sourceSplitter${sourceId}"
langId != null -> "${langId}$langSplitter${name}"
else -> name
}
}
companion object { companion object {
const val sourceSplitter = "◘•◘"
const val langSplitter = "⨼⨦⨠"
var lastCategoriesAddedTo = emptySet<Int>() var lastCategoriesAddedTo = emptySet<Int>()
fun create(name: String): Category = CategoryImpl().apply { fun create(name: String): Category = CategoryImpl().apply {

View file

@ -32,14 +32,10 @@ class CategoryImpl : Category {
val category = other as Category val category = other as Category
if (isDynamic && category.isDynamic) return dynamicHeaderKey() == category.dynamicHeaderKey()
return name == category.name return name == category.name
} }
override fun hashCode(): Int { override fun hashCode(): Int {
if (isDynamic) return dynamicHeaderKey().hashCode()
return name.hashCode() return name.hashCode()
} }
} }

View file

@ -4,11 +4,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
fun SChapter.toChapter(): ChapterImpl { fun SChapter.toChapter(): ChapterImpl {
return ChapterImpl().apply { return ChapterImpl().apply {
name = this@toChapter.name name = this@SChapter.name
url = this@toChapter.url url = this@SChapter.url
date_upload = this@toChapter.date_upload date_upload = this@SChapter.date_upload
chapter_number = this@toChapter.chapter_number chapter_number = this@SChapter.chapter_number
scanlator = this@toChapter.scanlator scanlator = this@SChapter.scanlator
} }
} }

View file

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.ui.library.LibraryItem
import kotlin.math.roundToInt import kotlin.math.roundToInt
import yokai.data.updateStrategyAdapter
data class LibraryManga( data class LibraryManga(
val manga: Manga,
var unread: Int = 0, var unread: Int = 0,
var read: Int = 0, var read: Int = 0,
var category: Int = 0, var category: Int = 0,
@ -13,11 +13,41 @@ data class LibraryManga(
var latestUpdate: Long = 0, var latestUpdate: Long = 0,
var lastRead: Long = 0, var lastRead: Long = 0,
var lastFetch: Long = 0, var lastFetch: Long = 0,
) { ) : MangaImpl() {
var realMangaCount = 0
get() = if (isBlank()) field else throw IllegalStateException("realMangaCount is only accessible by placeholders")
set(value) {
if (!isBlank()) throw IllegalStateException("realMangaCount can only be set by placeholders")
field = value
}
val hasRead val hasRead
get() = read > 0 get() = read > 0
@Transient
var items: List<LibraryItem>? = null
get() = if (isHidden()) field else throw IllegalStateException("items only accessible by placeholders")
set(value) {
if (!isHidden()) throw IllegalStateException("items can only be set by placeholders")
field = value
}
companion object { companion object {
fun createBlank(categoryId: Int): LibraryManga = LibraryManga().apply {
title = ""
id = Long.MIN_VALUE
category = categoryId
}
fun createHide(categoryId: Int, title: String, hiddenItems: List<LibraryItem>): LibraryManga =
createBlank(categoryId).apply {
this.title = title
this.status = -1
this.read = hiddenItems.size
this.items = hiddenItems
}
fun mapper( fun mapper(
// manga // manga
id: Long, id: Long,
@ -48,37 +78,34 @@ data class LibraryManga(
latestUpdate: Long, latestUpdate: Long,
lastRead: Long, lastRead: Long,
lastFetch: Long, lastFetch: Long,
): LibraryManga = LibraryManga( ): LibraryManga = createBlank(categoryId.toInt()).apply {
manga = Manga.mapper( this.id = id
id = id, this.source = source
source = source, this.url = url
url = url, this.artist = artist
artist = artist, this.author = author
author = author, this.description = description
description = description, this.genre = genre
genre = genre, this.title = title
title = title, this.status = status.toInt()
status = status, this.thumbnail_url = thumbnailUrl
thumbnailUrl = thumbnailUrl, this.favorite = favorite
favorite = favorite, this.last_update = lastUpdate ?: 0L
lastUpdate = lastUpdate, this.initialized = initialized
initialized = initialized, this.viewer_flags = viewerFlags.toInt()
viewerFlags = viewerFlags, this.hide_title = hideTitle
hideTitle = hideTitle, this.chapter_flags = chapterFlags.toInt()
chapterFlags = chapterFlags, this.date_added = dateAdded ?: 0L
dateAdded = dateAdded, this.filtered_scanlators = filteredScanlators
filteredScanlators = filteredScanlators, this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode)
updateStrategy = updateStrategy, this.cover_last_modified = coverLastModified
coverLastModified = coverLastModified, this.read = readCount.roundToInt()
), this.unread = maxOf((total - readCount).roundToInt(), 0)
read = readCount.roundToInt(), this.totalChapters = total.toInt()
unread = maxOf((total - readCount).roundToInt(), 0), this.bookmarkCount = bookmarkCount.roundToInt()
totalChapters = total.toInt(), this.latestUpdate = latestUpdate
bookmarkCount = bookmarkCount.roundToInt(), this.lastRead = lastRead
category = categoryId.toInt(), this.lastFetch = lastFetch
latestUpdate = latestUpdate, }
lastRead = lastRead,
lastFetch = lastFetch,
)
} }
} }

View file

@ -182,12 +182,14 @@ var Manga.vibrantCoverColor: Int?
id?.let { MangaCoverMetadata.setVibrantColor(it, value) } id?.let { MangaCoverMetadata.setVibrantColor(it, value) }
} }
fun Manga.Companion.create(url: String, title: String, source: Long = 0) = fun Manga.Companion.create(source: Long) = MangaImpl().apply {
MangaImpl( this.source = source
source = source, }
url = url,
).apply { fun Manga.Companion.create(pathUrl: String, title: String, source: Long = 0) = MangaImpl().apply {
url = pathUrl
this.title = title this.title = title
this.source = source
} }
fun Manga.Companion.mapper( fun Manga.Companion.mapper(
@ -211,12 +213,14 @@ fun Manga.Companion.mapper(
filteredScanlators: String?, filteredScanlators: String?,
updateStrategy: Long, updateStrategy: Long,
coverLastModified: Long, coverLastModified: Long,
) = create(url, title, source).apply { ) = create(source).apply {
this.id = id this.id = id
this.url = url
this.artist = artist this.artist = artist
this.author = author this.author = author
this.description = description this.description = description
this.genre = genre this.genre = genre
this.title = title
this.status = status.toInt() this.status = status.toInt()
this.thumbnail_url = thumbnailUrl this.thumbnail_url = thumbnailUrl
this.favorite = favorite this.favorite = favorite

View file

@ -12,11 +12,7 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List<ChapterHistory> = emptyList()) { data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List<ChapterHistory> = emptyList()) {
companion object { companion object {
fun createBlank() = MangaChapterHistory( fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl())
MangaImpl(null, -1, ""),
ChapterImpl(),
HistoryImpl(),
)
fun mapper( fun mapper(
// manga // manga

View file

@ -8,11 +8,13 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
open class MangaImpl( open class MangaImpl : Manga {
override var id: Long? = null,
override var source: Long = -1, override var id: Long? = null
override var url: String = "",
) : Manga { override var source: Long = -1
override lateinit var url: String
private val customMangaManager: CustomMangaManager by injectLazy() private val customMangaManager: CustomMangaManager by injectLazy()
@ -105,7 +107,7 @@ open class MangaImpl(
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return if (url.isNotBlank()) { return if (::url.isInitialized) {
url.hashCode() url.hashCode()
} else { } else {
(id ?: 0L).hashCode() (id ?: 0L).hashCode()

View file

@ -21,8 +21,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
import yokai.domain.download.DownloadPreferences import yokai.domain.download.DownloadPreferences
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
@ -171,7 +171,7 @@ class DownloadManager(
return files.sortedBy { it.name } return files.sortedBy { it.name }
.mapIndexed { i, file -> .mapIndexed { i, file ->
Page(i, uri = file.uri).apply { status = Page.State.Ready } Page(i, uri = file.uri).apply { status = Page.State.READY }
} }
} }

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.os.Looper
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
@ -54,8 +55,8 @@ import kotlinx.coroutines.supervisorScope
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
import yokai.core.archive.ZipWriter import yokai.core.archive.ZipWriter
import yokai.core.metadata.COMIC_INFO_FILE import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo import yokai.core.metadata.ComicInfo
@ -364,11 +365,11 @@ class Downloader(
// Get all the URLs to the source images, fetch pages if necessary // Get all the URLs to the source images, fetch pages if necessary
pageList.filter { it.imageUrl.isNullOrEmpty() }.forEach { page -> pageList.filter { it.imageUrl.isNullOrEmpty() }.forEach { page ->
page.status = Page.State.LoadPage page.status = Page.State.LOAD_PAGE
try { try {
page.imageUrl = download.source.getImageUrl(page) page.imageUrl = download.source.getImageUrl(page)
} catch (e: Throwable) { } catch (e: Throwable) {
page.status = Page.State.Error page.status = Page.State.ERROR
} }
} }
@ -493,12 +494,12 @@ class Downloader(
page.uri = file.uri page.uri = file.uri
page.progress = 100 page.progress = 100
page.status = Page.State.Ready page.status = Page.State.READY
} catch (e: Throwable) { } catch (e: Throwable) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
// Mark this page as error and allow to download the remaining // Mark this page as error and allow to download the remaining
page.progress = 0 page.progress = 0
page.status = Page.State.Error page.status = Page.State.ERROR
notifier.onError(e.message, chapName, download.manga.title) notifier.onError(e.message, chapName, download.manga.title)
} }
} }
@ -517,7 +518,7 @@ class Downloader(
tmpDir: UniFile, tmpDir: UniFile,
filename: String, filename: String,
): UniFile { ): UniFile {
page.status = Page.State.DownloadImage page.status = Page.State.DOWNLOAD_IMAGE
page.progress = 0 page.progress = 0
return flow { return flow {
val response = source.getImage(page) val response = source.getImage(page)
@ -603,9 +604,8 @@ class Downloader(
dirname: String, dirname: String,
tmpDir: UniFile, tmpDir: UniFile,
) { ) {
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX") val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
if (zip?.isFile != true) throw Exception("Failed to create CBZ file for downloaded chapter") ZipWriter(context, zip).use { writer ->
ZipWriter(context, zip!!).use { writer ->
tmpDir.listFiles()?.forEach { file -> tmpDir.listFiles()?.forEach { file ->
writer.write(file) writer.write(file)
} }

View file

@ -22,7 +22,7 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
get() = pages?.sumOf(Page::progress) ?: 0 get() = pages?.sumOf(Page::progress) ?: 0
val downloadedImages: Int val downloadedImages: Int
get() = pages?.count { it.status is Page.State.Ready } ?: 0 get() = pages?.count { it.status == Page.State.READY } ?: 0
@Transient @Transient
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED) private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)

View file

@ -4,7 +4,6 @@ import android.content.Context
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.domain.manga.models.Manga
import java.nio.charset.StandardCharsets
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -27,6 +26,7 @@ import yokai.domain.library.custom.interactor.GetCustomManga
import yokai.domain.library.custom.interactor.RelinkCustomManga import yokai.domain.library.custom.interactor.RelinkCustomManga
import yokai.domain.library.custom.model.CustomMangaInfo import yokai.domain.library.custom.model.CustomMangaInfo
import yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo import yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo
import java.nio.charset.StandardCharsets
class CustomMangaManager(val context: Context) { class CustomMangaManager(val context: Context) {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
@ -176,7 +176,8 @@ class CustomMangaManager(val context: Context) {
val status: Int? = null, val status: Int? = null,
) { ) {
fun toManga() = MangaImpl(id = this.id).apply { fun toManga() = MangaImpl().apply {
id = this@MangaJson.id
title = this@MangaJson.title ?: "" title = this@MangaJson.title ?: ""
author = this@MangaJson.author author = this@MangaJson.author
artist = this@MangaJson.artist artist = this@MangaJson.artist
@ -271,6 +272,9 @@ class CustomMangaManager(val context: Context) {
} }
} }
private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) = private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) = MangaImpl().apply {
MangaImpl(id = id).apply { this.copyFromComicInfo(comicInfo) } this.id = id
this.copyFromComicInfo(comicInfo)
this.title = comicInfo.series?.value ?: ""
}
} }

View file

@ -46,7 +46,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
import eu.kanade.tachiyomi.util.manga.MangaShortcutManager import eu.kanade.tachiyomi.util.manga.MangaShortcutManager
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.createFileInCacheDir
@ -174,17 +173,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val mangaList = ( val mangaList = (
if (savedMangasList != null) { if (savedMangasList != null) {
val mangas = val mangas = getLibraryManga.await().filter {
getLibraryManga.await() it.id in savedMangasList
.filter { it.manga.id in savedMangasList } }.distinctBy { it.id }
.distinctBy { it.manga.id }
val categoryId = inputData.getInt(KEY_CATEGORY, -1) val categoryId = inputData.getInt(KEY_CATEGORY, -1)
if (categoryId > -1) categoryIds.add(categoryId) if (categoryId > -1) categoryIds.add(categoryId)
mangas mangas
} else { } else {
getMangaToUpdate() getMangaToUpdate()
} }
).sortedBy { it.manga.title } ).sortedBy { it.title }
return withIOContext { return withIOContext {
try { try {
@ -229,7 +227,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) { private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
mangaToUpdate.addAll(mangaToAdd) mangaToUpdate.addAll(mangaToAdd)
mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.manga.source }) mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.source })
checkIfMassiveUpdate() checkIfMassiveUpdate()
coroutineScope { coroutineScope {
val list = mangaToUpdateMap.keys.map { source -> val list = mangaToUpdateMap.keys.map { source ->
@ -259,42 +257,42 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateDetails(mangaToUpdate: List<LibraryManga>) = coroutineScope { private suspend fun updateDetails(mangaToUpdate: List<LibraryManga>) = coroutineScope {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0) val count = AtomicInteger(0)
val asyncList = mangaToUpdate.groupBy { it.manga.source }.values.map { list -> val asyncList = mangaToUpdate.groupBy { it.source }.values.map { list ->
async { async {
requestSemaphore.withPermit { requestSemaphore.withPermit {
list.forEach { manga -> list.forEach { manga ->
ensureActive() ensureActive()
val source = sourceManager.get(manga.manga.source) as? HttpSource ?: return@async val source = sourceManager.get(manga.source) as? HttpSource ?: return@async
notifier.showProgressNotification( notifier.showProgressNotification(
manga.manga, manga,
count.andIncrement, count.andIncrement,
mangaToUpdate.size, mangaToUpdate.size,
) )
ensureActive() ensureActive()
val networkManga = try { val networkManga = try {
source.getMangaDetails(manga.manga.copy()) source.getMangaDetails(manga.copy())
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
Logger.e(e) Logger.e(e)
null null
} }
if (networkManga != null) { if (networkManga != null) {
manga.manga.prepareCoverUpdate(coverCache, networkManga, false) manga.prepareCoverUpdate(coverCache, networkManga, false)
val thumbnailUrl = manga.manga.thumbnail_url val thumbnailUrl = manga.thumbnail_url
manga.manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.manga.initialized = true manga.initialized = true
val request: ImageRequest = val request: ImageRequest =
if (thumbnailUrl != manga.manga.thumbnail_url) { if (thumbnailUrl != manga.thumbnail_url) {
// load new covers in background // load new covers in background
ImageRequest.Builder(context).data(manga.manga.cover()) ImageRequest.Builder(context).data(manga.cover())
.memoryCachePolicy(CachePolicy.DISABLED).build() .memoryCachePolicy(CachePolicy.DISABLED).build()
} else { } else {
ImageRequest.Builder(context).data(manga.manga.cover()) ImageRequest.Builder(context).data(manga.cover())
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.WRITE_ONLY) .diskCachePolicy(CachePolicy.WRITE_ONLY)
.build() .build()
} }
context.imageLoader.execute(request) context.imageLoader.execute(request)
updateManga.await(manga.manga.toMangaUpdate()) updateManga.await(manga.toMangaUpdate())
} }
} }
} }
@ -315,9 +313,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val loggedServices = trackManager.services.filter { it.isLogged } val loggedServices = trackManager.services.filter { it.isLogged }
mangaToUpdate.forEach { manga -> mangaToUpdate.forEach { manga ->
notifier.showProgressNotification(manga.manga, count++, mangaToUpdate.size) notifier.showProgressNotification(manga, count++, mangaToUpdate.size)
val tracks = getTrack.awaitAllByMangaId(manga.manga.id!!) val tracks = getTrack.awaitAllByMangaId(manga.id!!)
tracks.forEach { track -> tracks.forEach { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
@ -326,7 +324,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val newTrack = service.refresh(track) val newTrack = service.refresh(track)
insertTrack.await(newTrack) insertTrack.await(newTrack)
syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.manga.id!!, false), track, service) syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.id!!, false), track, service)
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(e) Logger.e(e)
} }
@ -378,7 +376,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private fun checkIfMassiveUpdate() { private fun checkIfMassiveUpdate() {
val largestSourceSize = mangaToUpdate val largestSourceSize = mangaToUpdate
.groupBy { it.manga.source } .groupBy { it.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource } .filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0 .maxOfOrNull { it.value.size } ?: 0
if (largestSourceSize > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) { if (largestSourceSize > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
@ -393,7 +391,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val httpSource = sourceManager.get(source) as? HttpSource ?: return false val httpSource = sourceManager.get(source) as? HttpSource ?: return false
while (count < mangaToUpdateMap[source]!!.size) { while (count < mangaToUpdateMap[source]!!.size) {
val manga = mangaToUpdateMap[source]!![count] val manga = mangaToUpdateMap[source]!![count]
val shouldDownload = manga.manga.shouldDownloadNewChapters(preferences) val shouldDownload = manga.shouldDownloadNewChapters(preferences)
if (updateMangaChapters(manga, this.count.andIncrement, httpSource, shouldDownload)) { if (updateMangaChapters(manga, this.count.andIncrement, httpSource, shouldDownload)) {
hasDownloads = true hasDownloads = true
} }
@ -412,15 +410,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
try { try {
var hasDownloads = false var hasDownloads = false
ensureActive() ensureActive()
notifier.showProgressNotification(manga.manga, progress, mangaToUpdate.size) notifier.showProgressNotification(manga, progress, mangaToUpdate.size)
val fetchedChapters = source.getChapterList(manga.manga.copy()) val fetchedChapters = source.getChapterList(manga.copy())
if (fetchedChapters.isNotEmpty()) { if (fetchedChapters.isNotEmpty()) {
val newChapters = syncChaptersWithSource(fetchedChapters, manga.manga, source) val newChapters = syncChaptersWithSource(fetchedChapters, manga, source)
if (newChapters.first.isNotEmpty()) { if (newChapters.first.isNotEmpty()) {
if (shouldDownload) { if (shouldDownload) {
downloadChapters( downloadChapters(
manga.manga, manga,
newChapters.first.sortedBy { it.chapter_number }, newChapters.first.sortedBy { it.chapter_number },
) )
hasDownloads = true hasDownloads = true
@ -430,24 +428,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
} }
if (deleteRemoved && newChapters.second.isNotEmpty()) { if (deleteRemoved && newChapters.second.isNotEmpty()) {
val removedChapters = newChapters.second.filter { val removedChapters = newChapters.second.filter {
downloadManager.isChapterDownloaded(it, manga.manga) && downloadManager.isChapterDownloaded(it, manga) &&
newChapters.first.none { newChapter -> newChapters.first.none { newChapter ->
newChapter.chapter_number == it.chapter_number && it.scanlator.isNullOrBlank() newChapter.chapter_number == it.chapter_number && it.scanlator.isNullOrBlank()
} }
} }
if (removedChapters.isNotEmpty()) { if (removedChapters.isNotEmpty()) {
downloadManager.deleteChapters(removedChapters, manga.manga, source) downloadManager.deleteChapters(removedChapters, manga, source)
} }
} }
if (newChapters.first.size + newChapters.second.size > 0) { if (newChapters.first.size + newChapters.second.size > 0) {
sendUpdate(manga.manga.id) sendUpdate(manga.id)
} }
} }
return@coroutineScope hasDownloads return@coroutineScope hasDownloads
} catch (e: Exception) { } catch (e: Exception) {
if (e !is CancellationException) { if (e !is CancellationException) {
failedUpdates[manga.manga] = e.message failedUpdates[manga] = e.message
Logger.e { "Failed updating: ${manga.manga.title}: $e" } Logger.e { "Failed updating: ${manga.title}: $e" }
} }
return@coroutineScope false return@coroutineScope false
} }
@ -462,23 +460,18 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private fun filterMangaToUpdate(mangaToAdd: List<LibraryManga>): List<LibraryManga> { private fun filterMangaToUpdate(mangaToAdd: List<LibraryManga>): List<LibraryManga> {
val restrictions = preferences.libraryUpdateMangaRestriction().get() val restrictions = preferences.libraryUpdateMangaRestriction().get()
return mangaToAdd.filter { manga -> return mangaToAdd.filter { manga ->
// TODO: See if this is problematic
// Always update local source entries if it's a manual global/library update
if (!tags.contains(WORK_NAME_AUTO) && manga.manga.isLocal()) {
return@filter true
}
when { when {
MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED -> { MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_completed) skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_completed)
} }
MANGA_HAS_UNREAD in restrictions && manga.unread != 0 -> { MANGA_HAS_UNREAD in restrictions && manga.unread != 0 -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_caught_up) skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_caught_up)
} }
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead -> { MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_started) skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_started)
} }
manga.manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> { manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_always_update) skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_always_update)
} }
else -> { else -> {
return@filter true return@filter true
@ -510,10 +503,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
preferences.libraryUpdateCategories().get().map(String::toInt) preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) { if (categoriesToUpdate.isNotEmpty()) {
categoryIds.addAll(categoriesToUpdate) categoryIds.addAll(categoriesToUpdate)
libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.manga.id } libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.id }
} else { } else {
categoryIds.addAll(getCategories.await().mapNotNull { it.id } + 0) categoryIds.addAll(getCategories.await().mapNotNull { it.id } + 0)
libraryManga.distinctBy { it.manga.id } libraryManga.distinctBy { it.id }
} }
} }
@ -571,13 +564,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
} }
private fun addMangaToQueue(categoryId: Int, manga: List<LibraryManga>) { private fun addMangaToQueue(categoryId: Int, manga: List<LibraryManga>) {
val mangas = filterMangaToUpdate(manga).sortedBy { it.manga.title } val mangas = filterMangaToUpdate(manga).sortedBy { it.title }
categoryIds.add(categoryId) categoryIds.add(categoryId)
addManga(mangas) addManga(mangas)
} }
private fun addCategory(categoryId: Int) { private fun addCategory(categoryId: Int) {
val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.manga.title } val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.title }
categoryIds.add(categoryId) categoryIds.add(categoryId)
addManga(mangas) addManga(mangas)
} }
@ -586,7 +579,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val distinctManga = mangaToAdd.filter { it !in mangaToUpdate } val distinctManga = mangaToAdd.filter { it !in mangaToUpdate }
mangaToUpdate.addAll(distinctManga) mangaToUpdate.addAll(distinctManga)
checkIfMassiveUpdate() checkIfMassiveUpdate()
distinctManga.groupBy { it.manga.source }.forEach { distinctManga.groupBy { it.source }.forEach {
// if added queue items is a new source not in the async list or an async list has // if added queue items is a new source not in the async list or an async list has
// finished running // finished running
if (mangaToUpdateMap[it.key].isNullOrEmpty()) { if (mangaToUpdateMap[it.key].isNullOrEmpty()) {
@ -734,9 +727,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (mangaToUse != null) { if (mangaToUse != null) {
builder.putLongArray( builder.putLongArray(
KEY_MANGAS, KEY_MANGAS,
mangaToUse.firstOrNull()?.manga?.id?.let { longArrayOf(it) } ?: longArrayOf(), mangaToUse.firstOrNull()?.id?.let { longArrayOf(it) } ?: longArrayOf(),
) )
extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.manga.id } extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.id }
} }
} }
val inputData = builder.build() val inputData = builder.build()

View file

@ -185,14 +185,14 @@ class LibraryUpdateNotifier(private val context: Context) {
val manga = it.key val manga = it.key
val chapters = it.value val chapters = it.value
val chapterNames = chapters.map { chapter -> val chapterNames = chapters.map { chapter ->
chapter.preferredChapterName(context, manga.manga, preferences) chapter.preferredChapterName(context, manga, preferences)
} }
notifications.add( notifications.add(
Pair( Pair(
context.notification(Notifications.CHANNEL_NEW_CHAPTERS) { context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_yokai) setSmallIcon(R.drawable.ic_yokai)
try { try {
val request = ImageRequest.Builder(context).data(manga.manga.cover()) val request = ImageRequest.Builder(context).data(manga.cover())
.networkCachePolicy(CachePolicy.DISABLED) .networkCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.ENABLED) .diskCachePolicy(CachePolicy.ENABLED)
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
@ -205,7 +205,7 @@ class LibraryUpdateNotifier(private val context: Context) {
} catch (_: Exception) { } catch (_: Exception) {
} }
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
setContentTitle(manga.manga.title) setContentTitle(manga.title)
color = ContextCompat.getColor(context, R.color.secondaryTachiyomi) color = ContextCompat.getColor(context, R.color.secondaryTachiyomi)
val chaptersNames = if (chapterNames.size > MAX_CHAPTERS) { val chaptersNames = if (chapterNames.size > MAX_CHAPTERS) {
"${chapterNames.take(MAX_CHAPTERS - 1).joinToString(", ")}, " + "${chapterNames.take(MAX_CHAPTERS - 1).joinToString(", ")}, " +
@ -224,7 +224,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent( setContentIntent(
NotificationReceiver.openChapterPendingActivity( NotificationReceiver.openChapterPendingActivity(
context, context,
manga.manga, manga,
chapters.first(), chapters.first(),
), ),
) )
@ -233,7 +233,7 @@ class LibraryUpdateNotifier(private val context: Context) {
context.getString(MR.strings.mark_as_read), context.getString(MR.strings.mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast( NotificationReceiver.markAsReadPendingBroadcast(
context, context,
manga.manga, manga,
chapters, chapters,
Notifications.ID_NEW_CHAPTERS, Notifications.ID_NEW_CHAPTERS,
), ),
@ -243,13 +243,13 @@ class LibraryUpdateNotifier(private val context: Context) {
context.getString(MR.strings.view_chapters), context.getString(MR.strings.view_chapters),
NotificationReceiver.openChapterPendingActivity( NotificationReceiver.openChapterPendingActivity(
context, context,
manga.manga, manga,
Notifications.ID_NEW_CHAPTERS, Notifications.ID_NEW_CHAPTERS,
), ),
) )
setAutoCancel(true) setAutoCancel(true)
}, },
manga.manga.id.hashCode(), manga.id.hashCode(),
), ),
) )
} }
@ -281,13 +281,13 @@ class LibraryUpdateNotifier(private val context: Context) {
NotificationCompat.BigTextStyle() NotificationCompat.BigTextStyle()
.bigText( .bigText(
updates.keys.joinToString("\n") { updates.keys.joinToString("\n") {
it.manga.title.chop(45) it.title.chop(45)
}, },
), ),
) )
} }
} else if (!preferences.hideNotificationContent().get()) { } else if (!preferences.hideNotificationContent().get()) {
setContentText(updates.keys.first().manga.title.chop(45)) setContentText(updates.keys.first().title.chop(45))
} }
priority = NotificationCompat.PRIORITY_HIGH priority = NotificationCompat.PRIORITY_HIGH
setGroup(Notifications.GROUP_NEW_CHAPTERS) setGroup(Notifications.GROUP_NEW_CHAPTERS)

View file

@ -487,7 +487,7 @@ class NotificationReceiver : BroadcastReceiver() {
*/ */
internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent { internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent {
val newIntent = val newIntent =
Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_MANGA) Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
.putExtra(Constants.MANGA_EXTRA, manga.id) .putExtra(Constants.MANGA_EXTRA, manga.id)
.putExtra("notificationId", manga.id.hashCode()) .putExtra("notificationId", manga.id.hashCode())

View file

@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.core.preference.Preference import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.core.preference.getEnum import eu.kanade.tachiyomi.core.preference.getEnum
import eu.kanade.tachiyomi.core.storage.preference.asDateFormat
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
@ -20,12 +19,13 @@ import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.system.Themes import eu.kanade.tachiyomi.util.system.Themes
import java.text.DateFormat
import java.util.Locale
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
@ -187,10 +187,10 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10") fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10")
fun dateFormatRaw() = preferenceStore.getString(Keys.dateFormat, "") fun dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
@Deprecated("Use dateFormatRaw().get().asDateFormat() instead") else -> SimpleDateFormat(format, Locale.getDefault())
fun dateFormat(format: String = dateFormatRaw().get()): DateFormat = format.asDateFormat() }
fun appLanguage() = preferenceStore.getString("app_language", "") fun appLanguage() = preferenceStore.getString("app_language", "")

View file

@ -49,6 +49,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("completedAt", createDate(track.finished_reading_date)) put("completedAt", createDate(track.finished_reading_date))
} }
} }
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess() .awaitSuccess()
.parseAs<ALAddMangaResult>() .parseAs<ALAddMangaResult>()
@ -58,6 +59,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
} }
} }
}
suspend fun updateLibraryManga(track: Track): Track { suspend fun updateLibraryManga(track: Track): Track {
return withIOContext { return withIOContext {
@ -72,11 +74,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("completedAt", createDate(track.finished_reading_date)) put("completedAt", createDate(track.finished_reading_date))
} }
} }
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess() .awaitSuccess()
track track
} }
} }
}
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
return withIOContext { return withIOContext {
@ -86,6 +90,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("query", search) put("query", search)
} }
} }
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess() .awaitSuccess()
.parseAs<ALSearchResult>() .parseAs<ALSearchResult>()
@ -93,6 +98,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
.map { it.toALManga().toTrack() } .map { it.toALManga().toTrack() }
} }
} }
}
suspend fun findLibManga(track: Track, userid: Int): Track? { suspend fun findLibManga(track: Track, userid: Int): Track? {
return withIOContext { return withIOContext {
@ -103,6 +109,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("manga_id", track.media_id) put("manga_id", track.media_id)
} }
} }
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess() .awaitSuccess()
.parseAs<ALUserListMangaQueryResult>() .parseAs<ALUserListMangaQueryResult>()
@ -112,6 +119,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
?.toTrack() ?.toTrack()
} }
} }
}
suspend fun getLibManga(track: Track, userid: Int): Track { suspend fun getLibManga(track: Track, userid: Int): Track {
return findLibManga(track, userid) ?: throw Exception("Could not find manga") return findLibManga(track, userid) ?: throw Exception("Could not find manga")

View file

@ -75,6 +75,7 @@ class BangumiApi(
.appendQueryParameter("responseGroup", "large") .appendQueryParameter("responseGroup", "large")
.appendQueryParameter("max_results", "20") .appendQueryParameter("max_results", "20")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<BGMSearchResult>() .parseAs<BGMSearchResult>()
@ -87,15 +88,18 @@ class BangumiApi(
} }
} }
} }
}
suspend fun findLibManga(track: Track): Track? { suspend fun findLibManga(track: Track): Track? {
return withIOContext { return withIOContext {
with(json) {
authClient.newCall(GET("$API_URL/subject/${track.media_id}")) authClient.newCall(GET("$API_URL/subject/${track.media_id}"))
.awaitSuccess() .awaitSuccess()
.parseAs<BGMSearchItem>() .parseAs<BGMSearchItem>()
.toTrackSearch(trackId) .toTrackSearch(trackId)
} }
} }
}
suspend fun statusLibManga(track: Track): Track? { suspend fun statusLibManga(track: Track): Track? {
return withIOContext { return withIOContext {
@ -107,6 +111,7 @@ class BangumiApi(
.build() .build()
// TODO: get user readed chapter here // TODO: get user readed chapter here
with(json) {
authClient.newCall(requestUserRead) authClient.newCall(requestUserRead)
.awaitSuccess() .awaitSuccess()
.parseAs<BGMCollectionResponse>() .parseAs<BGMCollectionResponse>()
@ -120,14 +125,17 @@ class BangumiApi(
} }
} }
} }
}
suspend fun accessToken(code: String): BGMOAuth { suspend fun accessToken(code: String): BGMOAuth {
return withIOContext { return withIOContext {
with(json) {
client.newCall(accessTokenRequest(code)) client.newCall(accessTokenRequest(code))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
private fun accessTokenRequest(code: String) = POST( private fun accessTokenRequest(code: String) = POST(
OAUTH_URL, OAUTH_URL,

View file

@ -44,7 +44,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
try { try {
client.newCall(request).execute().use { client.newCall(request).execute().use {
when (it.code) { when (it.code) {
200 -> return it.parseAs<AuthenticationDto>().token 200 -> return with(json) { it.parseAs<AuthenticationDto>().token }
401 -> { 401 -> {
Logger.w { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" } Logger.w { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
throw IOException("Unauthorized / api key not valid") throw IOException("Unauthorized / api key not valid")
@ -89,10 +89,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
private fun getTotalChapters(url: String): Long { private fun getTotalChapters(url: String): Long {
val requestUrl = getApiVolumesUrl(url) val requestUrl = getApiVolumesUrl(url)
try { try {
val listVolumeDto = val listVolumeDto = with(json) {
authClient.newCall(GET(requestUrl)) authClient.newCall(GET(requestUrl))
.execute() .execute()
.parseAs<List<VolumeDto>>() .parseAs<List<VolumeDto>>()
}
var volumeNumber = 0L var volumeNumber = 0L
var maxChapterNumber = 0L var maxChapterNumber = 0L
for (volume in listVolumeDto) { for (volume in listVolumeDto) {
@ -116,7 +117,9 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
try { try {
authClient.newCall(GET(requestUrl)).execute().use { authClient.newCall(GET(requestUrl)).execute().use {
if (it.code == 200) { if (it.code == 200) {
return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat() return with(json) {
it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
}
} }
if (it.code == 204) { if (it.code == 204) {
return 0F return 0F
@ -131,10 +134,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { suspend fun getTrackSearch(url: String): TrackSearch = withIOContext {
try { try {
val serieDto: SeriesDto = val serieDto: SeriesDto = with(json) {
authClient.newCall(GET(url)) authClient.newCall(GET(url))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
}
val track = serieDto.toTrack() val track = serieDto.toTrack()
track.apply { track.apply {

View file

@ -131,6 +131,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
suspend fun search(query: String): List<TrackSearch> { suspend fun search(query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
with(json) {
authClient.newCall(GET(ALGOLIA_KEY_URL)) authClient.newCall(GET(ALGOLIA_KEY_URL))
.awaitSuccess() .awaitSuccess()
.parseAs<KitsuSearchResult>() .parseAs<KitsuSearchResult>()
@ -139,12 +140,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> { private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
val jsonObject = buildJsonObject { val jsonObject = buildJsonObject {
put("params", "query=$query$ALGOLIA_FILTER") put("params", "query=$query$ALGOLIA_FILTER")
} }
with(json) {
client.newCall( client.newCall(
POST( POST(
ALGOLIA_URL, ALGOLIA_URL,
@ -164,6 +167,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.map { it.toTrack() } .map { it.toTrack() }
} }
} }
}
suspend fun findLibManga(track: Track, userId: String): Track? { suspend fun findLibManga(track: Track, userId: String): Track? {
return withIOContext { return withIOContext {
@ -171,6 +175,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId") .encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
.appendQueryParameter("include", "manga") .appendQueryParameter("include", "manga")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<KitsuListSearchResult>() .parseAs<KitsuListSearchResult>()
@ -183,6 +188,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
suspend fun getLibManga(track: Track): Track { suspend fun getLibManga(track: Track): Track {
return withIOContext { return withIOContext {
@ -190,6 +196,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[id]=${track.media_id}") .encodedQuery("filter[id]=${track.media_id}")
.appendQueryParameter("include", "manga") .appendQueryParameter("include", "manga")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<KitsuListSearchResult>() .parseAs<KitsuListSearchResult>()
@ -202,6 +209,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
suspend fun login(username: String, password: String): KitsuOAuth { suspend fun login(username: String, password: String): KitsuOAuth {
return withIOContext { return withIOContext {
@ -212,17 +220,20 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.add("client_id", CLIENT_ID) .add("client_id", CLIENT_ID)
.add("client_secret", CLIENT_SECRET) .add("client_secret", CLIENT_SECRET)
.build() .build()
with(json) {
client.newCall(POST(LOGIN_URL, body = formBody)) client.newCall(POST(LOGIN_URL, body = formBody))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
suspend fun getCurrentUser(): String { suspend fun getCurrentUser(): String {
return withIOContext { return withIOContext {
val url = "${BASE_URL}users".toUri().buildUpon() val url = "${BASE_URL}users".toUri().buildUpon()
.encodedQuery("filter[self]=true") .encodedQuery("filter[self]=true")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<KitsuCurrentUserResult>() .parseAs<KitsuCurrentUserResult>()
@ -230,6 +241,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.id .id
} }
} }
}
companion object { companion object {
private const val CLIENT_ID = private const val CLIENT_ID =

View file

@ -26,6 +26,7 @@ class KomgaApi(private val client: OkHttpClient) {
withIOContext { withIOContext {
try { try {
val track = val track =
with(json) {
if (url.contains(READLIST_API)) { if (url.contains(READLIST_API)) {
client.newCall(GET(url)) client.newCall(GET(url))
.awaitSuccess() .awaitSuccess()
@ -37,8 +38,9 @@ class KomgaApi(private val client: OkHttpClient) {
.parseAs<SeriesDto>() .parseAs<SeriesDto>()
.toTrack() .toTrack()
} }
}
val progress = val progress = with(json) {
client client
.newCall( .newCall(
GET( GET(
@ -57,6 +59,7 @@ class KomgaApi(private val client: OkHttpClient) {
it.parseAs<ReadProgressDto>().toV2() it.parseAs<ReadProgressDto>().toV2()
} }
} }
}
track.apply { track.apply {
cover_url = "$url/thumbnail" cover_url = "$url/thumbnail"
tracking_url = url tracking_url = url

View file

@ -37,6 +37,7 @@ class MangaUpdatesApi(
suspend fun getSeriesListItem(track: Track): Pair<MUListItem, MURating?> { suspend fun getSeriesListItem(track: Track): Pair<MUListItem, MURating?> {
val listItem = val listItem =
with(json) {
authClient.newCall( authClient.newCall(
GET( GET(
url = "$BASE_URL/v1/lists/series/${track.media_id}", url = "$BASE_URL/v1/lists/series/${track.media_id}",
@ -44,6 +45,7 @@ class MangaUpdatesApi(
) )
.awaitSuccess() .awaitSuccess()
.parseAs<MUListItem>() .parseAs<MUListItem>()
}
val rating = getSeriesRating(track) val rating = getSeriesRating(track)
@ -102,6 +104,7 @@ class MangaUpdatesApi(
private suspend fun getSeriesRating(track: Track): MURating? { private suspend fun getSeriesRating(track: Track): MURating? {
return try { return try {
with(json) {
authClient.newCall( authClient.newCall(
GET( GET(
url = "$BASE_URL/v1/series/${track.media_id}/rating", url = "$BASE_URL/v1/series/${track.media_id}/rating",
@ -109,6 +112,7 @@ class MangaUpdatesApi(
) )
.awaitSuccess() .awaitSuccess()
.parseAs<MURating>() .parseAs<MURating>()
}
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@ -147,7 +151,8 @@ class MangaUpdatesApi(
}, },
) )
} }
return client.newCall( return with(json) {
client.newCall(
POST( POST(
url = "$BASE_URL/v1/series/search", url = "$BASE_URL/v1/series/search",
body = body.toString().toRequestBody(CONTENT_TYPE), body = body.toString().toRequestBody(CONTENT_TYPE),
@ -158,13 +163,15 @@ class MangaUpdatesApi(
.results .results
.map { it.record } .map { it.record }
} }
}
suspend fun authenticate(username: String, password: String): MUContext? { suspend fun authenticate(username: String, password: String): MUContext? {
val body = buildJsonObject { val body = buildJsonObject {
put("username", username) put("username", username)
put("password", password) put("password", password)
} }
return client.newCall( return with(json) {
client.newCall(
PUT( PUT(
url = "$BASE_URL/v1/account/login", url = "$BASE_URL/v1/account/login",
body = body.toString().toRequestBody(CONTENT_TYPE), body = body.toString().toRequestBody(CONTENT_TYPE),
@ -174,6 +181,7 @@ class MangaUpdatesApi(
.parseAs<MULoginResponse>() .parseAs<MULoginResponse>()
.context .context
} }
}
companion object { companion object {
private const val BASE_URL = "https://api.mangaupdates.com" private const val BASE_URL = "https://api.mangaupdates.com"

View file

@ -45,11 +45,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.add("code_verifier", codeVerifier) .add("code_verifier", codeVerifier)
.add("grant_type", "authorization_code") .add("grant_type", "authorization_code")
.build() .build()
with(json) {
client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody)) client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
suspend fun getCurrentUser(): String { suspend fun getCurrentUser(): String {
return withIOContext { return withIOContext {
@ -57,12 +59,14 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url("$BASE_API_URL/users/@me") .url("$BASE_API_URL/users/@me")
.get() .get()
.build() .build()
with(json) {
authClient.newCall(request) authClient.newCall(request)
.awaitSuccess() .awaitSuccess()
.parseAs<MALUser>() .parseAs<MALUser>()
.name .name
} }
} }
}
suspend fun search(query: String): List<TrackSearch> { suspend fun search(query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
@ -71,6 +75,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendQueryParameter("q", query.take(64)) .appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true") .appendQueryParameter("nsfw", "true")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<MALSearchResult>() .parseAs<MALSearchResult>()
@ -80,6 +85,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.filter { !it.publishing_type.contains("novel") } .filter { !it.publishing_type.contains("novel") }
} }
} }
}
suspend fun getMangaDetails(id: Int): TrackSearch { suspend fun getMangaDetails(id: Int): TrackSearch {
return withIOContext { return withIOContext {
@ -87,6 +93,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(id.toString()) .appendPath(id.toString())
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date") .appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<MALManga>() .parseAs<MALManga>()
@ -96,7 +103,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
title = it.title title = it.title
summary = it.synopsis summary = it.synopsis
total_chapters = it.numChapters total_chapters = it.numChapters
cover_url = (it.covers?.large ?: it.covers?.medium).orEmpty() cover_url = it.covers.large
tracking_url = "https://myanimelist.net/manga/$media_id" tracking_url = "https://myanimelist.net/manga/$media_id"
publishing_status = it.status.replace("_", " ") publishing_status = it.status.replace("_", " ")
publishing_type = it.mediaType.replace("_", " ") publishing_type = it.mediaType.replace("_", " ")
@ -105,6 +112,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
} }
} }
}
suspend fun updateItem(track: Track): Track { suspend fun updateItem(track: Track): Track {
return withIOContext { return withIOContext {
@ -124,12 +132,14 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(mangaUrl(track.media_id).toString()) .url(mangaUrl(track.media_id).toString())
.put(formBodyBuilder.build()) .put(formBodyBuilder.build())
.build() .build()
with(json) {
authClient.newCall(request) authClient.newCall(request)
.awaitSuccess() .awaitSuccess()
.parseAs<MALListItemStatus>() .parseAs<MALListItemStatus>()
.let { parseMangaItem(it, track) } .let { parseMangaItem(it, track) }
} }
} }
}
suspend fun findListItem(track: Track): Track? { suspend fun findListItem(track: Track): Track? {
return withIOContext { return withIOContext {
@ -137,6 +147,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build() .build()
with(json) {
authClient.newCall(GET(uri.toString())) authClient.newCall(GET(uri.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<MALListItem>() .parseAs<MALListItem>()
@ -146,6 +157,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
} }
} }
}
suspend fun findListItems(query: String, offset: Int = 0): List<TrackSearch> { suspend fun findListItems(query: String, offset: Int = 0): List<TrackSearch> {
return withIOContext { return withIOContext {
@ -178,11 +190,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(urlBuilder.build().toString()) .url(urlBuilder.build().toString())
.get() .get()
.build() .build()
with(json) {
authClient.newCall(request) authClient.newCall(request)
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
private fun parseMangaItem(listStatus: MALListItemStatus, track: Track): Track { private fun parseMangaItem(listStatus: MALListItemStatus, track: Track): Track {
return track.apply { return track.apply {

View file

@ -34,7 +34,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
// Add the authorization header to the original request // Add the authorization header to the original request
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}") .addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
// .header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)
@ -66,7 +66,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
return runCatching { return runCatching {
if (response.isSuccessful) { if (response.isSuccessful) {
response.parseAs<MALOAuth>() with(json) { response.parseAs<MALOAuth>() }
} else { } else {
response.close() response.close()
null null

View file

@ -12,7 +12,7 @@ data class MALManga(
val numChapters: Long, val numChapters: Long,
val mean: Double = -1.0, val mean: Double = -1.0,
@SerialName("main_picture") @SerialName("main_picture")
val covers: MALMangaCovers?, val covers: MALMangaCovers,
val status: String, val status: String,
@SerialName("media_type") @SerialName("media_type")
val mediaType: String, val mediaType: String,
@ -22,6 +22,5 @@ data class MALManga(
@Serializable @Serializable
data class MALMangaCovers( data class MALMangaCovers(
val large: String?, val large: String = "",
val medium: String,
) )

View file

@ -74,12 +74,14 @@ class ShikimoriApi(
.appendQueryParameter("search", search) .appendQueryParameter("search", search)
.appendQueryParameter("limit", "20") .appendQueryParameter("limit", "20")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<List<SMManga>>() .parseAs<List<SMManga>>()
.map { it.toTrack(trackId) } .map { it.toTrack(trackId) }
} }
} }
}
suspend fun remove(track: Track): Boolean { suspend fun remove(track: Track): Boolean {
return try { return try {
@ -100,16 +102,18 @@ class ShikimoriApi(
val urlMangas = "$API_URL/mangas".toUri().buildUpon() val urlMangas = "$API_URL/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.build() .build()
val manga = val manga = with(json) {
authClient.newCall(GET(urlMangas.toString())) authClient.newCall(GET(urlMangas.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<SMManga>() .parseAs<SMManga>()
}
val url = "$API_URL/v2/user_rates".toUri().buildUpon() val url = "$API_URL/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id) .appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga") .appendQueryParameter("target_type", "Manga")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.execute() .execute()
.parseAs<List<SMUserListEntry>>() .parseAs<List<SMUserListEntry>>()
@ -123,23 +127,28 @@ class ShikimoriApi(
} }
} }
} }
}
suspend fun getCurrentUser(): Int { suspend fun getCurrentUser(): Int {
return withIOContext { return withIOContext {
with(json) {
authClient.newCall(GET("$API_URL/users/whoami")) authClient.newCall(GET("$API_URL/users/whoami"))
.awaitSuccess() .awaitSuccess()
.parseAs<SMUser>() .parseAs<SMUser>()
.id .id
} }
} }
}
suspend fun accessToken(code: String): SMOAuth { suspend fun accessToken(code: String): SMOAuth {
return withIOContext { return withIOContext {
with(json) {
client.newCall(accessTokenRequest(code)) client.newCall(accessTokenRequest(code))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
private fun accessTokenRequest(code: String) = POST( private fun accessTokenRequest(code: String) = POST(
OAUTH_URL, OAUTH_URL,

View file

@ -52,7 +52,9 @@ class TachideskApi {
trackUrl trackUrl
} }
val manga = client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs<MangaDataClass>() val manga = with(json) {
client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs<MangaDataClass>()
}
TrackSearch.create(TrackManager.SUWAYOMI).apply { TrackSearch.create(TrackManager.SUWAYOMI).apply {
title = manga.title title = manga.title
@ -72,7 +74,9 @@ class TachideskApi {
suspend fun updateProgress(track: Track): Track { suspend fun updateProgress(track: Track): Track {
val url = track.tracking_url val url = track.tracking_url
val chapters = client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs<List<ChapterDataClass>>() val chapters = with(json) {
client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs<List<ChapterDataClass>>()
}
val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index
client.newCall( client.newCall(

View file

@ -11,12 +11,12 @@ import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.localeContext import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withIOContext
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import yokai.domain.base.models.Version import yokai.domain.base.models.Version
import java.util.*
import java.util.concurrent.*
class AppUpdateChecker( class AppUpdateChecker(
private val json: Json = Injekt.get(), private val json: Json = Injekt.get(),
@ -31,7 +31,8 @@ class AppUpdateChecker(
} }
return withIOContext { return withIOContext {
val result = if (preferences.checkForBetas().get()) { val result = with(json) {
if (preferences.checkForBetas().get()) {
networkService.client networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases")) .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases"))
.await() .await()
@ -72,6 +73,7 @@ class AppUpdateChecker(
} }
} }
} }
}
if (doExtrasAfterNewUpdate && result is AppUpdateResult.NewUpdate) { if (doExtrasAfterNewUpdate && result is AppUpdateResult.NewUpdate) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
preferences.appShouldAutoUpdate().get() != AppDownloadInstallJob.NEVER preferences.appShouldAutoUpdate().get() != AppDownloadInstallJob.NEVER

View file

@ -7,7 +7,6 @@ import android.os.Parcelable
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionApi import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
@ -18,9 +17,6 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.launchNow
import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withIOContext
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -31,6 +27,8 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import yokai.domain.base.BasePreferences import yokai.domain.base.BasePreferences
import yokai.domain.extension.interactor.TrustExtension import yokai.domain.extension.interactor.TrustExtension
import java.util.*
import java.util.concurrent.*
/** /**
* The manager of extensions installed as another apk which extend the available sources. It handles * The manager of extensions installed as another apk which extend the available sources. It handles

View file

@ -1,16 +1,21 @@
package eu.kanade.tachiyomi.extension.installer package eu.kanade.tachiyomi.extension
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
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.Process import android.os.Process
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import eu.kanade.tachiyomi.util.system.getUriSize import eu.kanade.tachiyomi.util.system.getUriSize
import eu.kanade.tachiyomi.util.system.isShizukuInstalled import eu.kanade.tachiyomi.util.system.isPackageInstalled
import java.io.BufferedReader
import java.io.InputStream
import java.lang.reflect.Method
import java.util.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -18,21 +23,38 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import rikka.shizuku.Shizuku import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuRemoteProcess import rikka.shizuku.ShizukuRemoteProcess
import yokai.i18n.MR import rikka.sui.Sui
import yokai.util.lang.getString import uy.kohesive.injekt.injectLazy
import java.io.BufferedReader
import java.io.InputStream
import java.lang.reflect.Method
import java.util.*
import java.util.concurrent.atomic.AtomicReference
class ShizukuInstaller( class ShizukuInstaller(private val context: Context, val finishedQueue: (ShizukuInstaller) -> Unit) {
context: Context,
finishedQueue: (Installer) -> Unit,
) : Installer(context, finishedQueue) {
private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
data class Entry(val downloadId: Long, val pkgName: String, val uri: Uri)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val shizukuDeadListener = Shizuku.OnBinderDeadListener { private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
Logger.d { "Shizuku was killed prematurely" } Logger.d { "Shizuku was killed prematurely" }
finishedQueue(this) finishedQueue(this)
} }
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener { private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener {
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) { if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
@ -47,13 +69,13 @@ class ShizukuInstaller(
} }
} }
override var ready = false var ready = false
private val newProcess: Method private val newProcess: Method
init { init {
Shizuku.addBinderDeadListener(shizukuDeadListener) Shizuku.addBinderDeadListener(shizukuDeadListener)
require(Shizuku.pingBinder() && context.isShizukuInstalled) { require(Shizuku.pingBinder() && (context.isPackageInstalled(shizukuPkgName) || Sui.isSui())) {
finishedQueue(this) finishedQueue(this)
context.getString(MR.strings.ext_installer_shizuku_stopped) context.getString(MR.strings.ext_installer_shizuku_stopped)
} }
@ -69,8 +91,9 @@ class ShizukuInstaller(
newProcess.isAccessible = true newProcess.isAccessible = true
} }
override fun processEntry(entry: Entry) { @Suppress("BlockingMethodInNonBlockingContext")
super.processEntry(entry) fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
ioScope.launch { ioScope.launch {
var sessionId: String? = null var sessionId: String? = null
try { try {
@ -108,14 +131,85 @@ class ShizukuInstaller(
} }
} }
// Don't cancel if entry is already started installing /**
override fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry * Checks the queue. The provided service will be stopped if the queue is empty.
* Will not be run when not ready.
*
* @see ready
*/
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
finishedQueue(this)
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeAt(0)
processEntry(nextEntry)
}
}
override fun onDestroy() { /**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [ExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(succeeded: Boolean) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.setInstallationResult(completedEntry.downloadId, succeeded)
checkQueue()
}
}
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) {
queue.add(Entry(downloadId, pkgName, uri))
checkQueue()
}
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
private fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) {
queue.remove(toCancel)
if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue
this.waitingInstall.set(null)
checkQueue()
}
queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) }
// extensionManager.up(downloadId, InstallStep.Idle)
}
}
// Don't cancel if entry is already started installing
fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
fun getActiveEntry(): Entry? = waitingInstall.get()
fun onDestroy() {
Shizuku.removeBinderDeadListener(shizukuDeadListener) Shizuku.removeBinderDeadListener(shizukuDeadListener)
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener) Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
ioScope.cancel() ioScope.cancel()
super.onDestroy() LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) }
queue.clear()
waitingInstall.set(null)
} }
private fun exec(command: String, stdin: InputStream? = null): ShellResult { private fun exec(command: String, stdin: InputStream? = null): ShellResult {

View file

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -23,6 +24,7 @@ import yokai.domain.extension.repo.model.ExtensionRepo
internal class ExtensionApi { internal class ExtensionApi {
private val json: Json by injectLazy()
private val networkService: NetworkHelper by injectLazy() private val networkService: NetworkHelper by injectLazy()
private val getExtensionRepo: GetExtensionRepo by injectLazy() private val getExtensionRepo: GetExtensionRepo by injectLazy()
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy() private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
@ -45,9 +47,11 @@ internal class ExtensionApi {
.newCall(GET("$repoBaseUrl/index.min.json")) .newCall(GET("$repoBaseUrl/index.min.json"))
.awaitSuccess() .awaitSuccess()
with(json) {
response response
.parseAs<List<ExtensionJsonObject>>() .parseAs<List<ExtensionJsonObject>>()
.toExtensions(repoBaseUrl) .toExtensions(repoBaseUrl)
}
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(e) { "Failed to get extensions from $repoBaseUrl" } Logger.e(e) { "Failed to get extensions from $repoBaseUrl" }
emptyList() emptyList()

View file

@ -1,122 +0,0 @@
package eu.kanade.tachiyomi.extension.installer
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.annotation.CallSuper
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
import uy.kohesive.injekt.injectLazy
abstract class Installer(
internal val context: Context,
// TODO: Remove finishedQueue
internal val finishedQueue: (Installer) -> Unit,
) {
private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
abstract var ready: Boolean
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) {
queue.add(Entry(downloadId, pkgName, uri))
checkQueue()
}
@CallSuper
open fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
}
open fun cancelEntry(entry: Entry): Boolean {
return true
}
/**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [ExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(succeeded: Boolean) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.setInstallationResult(completedEntry.downloadId, succeeded)
checkQueue()
}
}
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
finishedQueue(this)
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeAt(0)
processEntry(nextEntry)
}
}
@CallSuper
open fun onDestroy() {
LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) }
queue.clear()
waitingInstall.set(null)
}
protected fun getActiveEntry(): Entry? = waitingInstall.get()
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) {
queue.remove(toCancel)
if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue
this.waitingInstall.set(null)
checkQueue()
}
queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) }
// extensionManager.up(downloadId, InstallStep.Idle)
}
}
data class Entry(
val downloadId: Long,
val pkgName: String,
val uri: Uri,
)
}

View file

@ -14,7 +14,7 @@ import androidx.core.net.toUri
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.extension.ExtensionInstallerJob import eu.kanade.tachiyomi.extension.ExtensionInstallerJob
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller import eu.kanade.tachiyomi.extension.ShizukuInstaller
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.isPackageInstalled import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import java.io.File
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -48,6 +47,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences import yokai.domain.base.BasePreferences
import java.io.File
/** /**
* The installer which installs, updates and uninstalls the extensions. * The installer which installs, updates and uninstalls the extensions.

View file

@ -12,6 +12,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.storage.fillMetadata
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.extension import eu.kanade.tachiyomi.util.system.extension
import eu.kanade.tachiyomi.util.system.nameWithoutExtension import eu.kanade.tachiyomi.util.system.nameWithoutExtension
@ -31,8 +33,7 @@ import nl.adaptivity.xmlutil.serialization.XML
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.core.archive.util.archiveReader import yokai.core.archive.archiveReader
import yokai.core.archive.util.epubReader
import yokai.core.metadata.COMIC_INFO_FILE import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo import yokai.core.metadata.ComicInfo
import yokai.core.metadata.copyFromComicInfo import yokai.core.metadata.copyFromComicInfo
@ -41,7 +42,6 @@ import yokai.domain.chapter.services.ChapterRecognition
import yokai.domain.source.SourcePreferences import yokai.domain.source.SourcePreferences
import yokai.domain.storage.StorageManager import yokai.domain.storage.StorageManager
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.fillMetadata
import yokai.util.lang.getString import yokai.util.lang.getString
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource { class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
@ -410,7 +410,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
} }
} }
is Format.Epub -> { is Format.Epub -> {
format.file.epubReader(context).use { epub -> EpubFile(format.file.archiveReader(context)).use { epub ->
val entry = epub.getImagesFromPages().firstOrNull() val entry = epub.getImagesFromPages().firstOrNull()
entry?.let { updateCover(manga, epub.getInputStream(it)!!, context) } entry?.let { updateCover(manga, epub.getInputStream(it)!!, context) }
@ -433,7 +433,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
true true
} }
is Format.Epub -> { is Format.Epub -> {
format.file.epubReader(context).use { epub -> EpubFile(format.file.archiveReader(context)).use { epub ->
epub.fillMetadata(chapter, manga) epub.fillMetadata(chapter, manga)
} }
true true

View file

@ -1,13 +1,20 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import java.util.concurrent.ConcurrentHashMap import eu.kanade.tachiyomi.source.online.all.Cubari
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.source.online.english.KireiCake
import eu.kanade.tachiyomi.source.online.english.MangaPlus
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -17,8 +24,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import yokai.i18n.MR import java.util.concurrent.ConcurrentHashMap
import yokai.util.lang.getString
class SourceManager( class SourceManager(
private val context: Context, private val context: Context,
@ -34,8 +40,28 @@ class SourceManager(
val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() } val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
val onlineSources: Flow<List<HttpSource>> = catalogueSources.map { it.filterIsInstance<HttpSource>() } val onlineSources: Flow<List<HttpSource>> = catalogueSources.map { it.filterIsInstance<HttpSource>() }
// FIXME: Delegated source, unused at the moment, J2K only delegate deep links private val delegatedSources = listOf(
private val delegatedSources = emptyList<DelegatedSource>().associateBy { it.sourceId } DelegatedSource(
"reader.kireicake.com",
5509224355268673176,
KireiCake(),
),
DelegatedSource(
"mangadex.org",
2499283573021220255,
MangaDex(),
),
DelegatedSource(
"mangaplus.shueisha.co.jp",
1998944621602463790,
MangaPlus(),
),
DelegatedSource(
"cubari.moe",
6338219619148105941,
Cubari(),
),
).associateBy { it.sourceId }
init { init {
scope.launch { scope.launch {
@ -45,7 +71,7 @@ class SourceManager(
extensions.forEach { extension -> extensions.forEach { extension ->
extension.sources.forEach { extension.sources.forEach {
mutableMap[it.id] = it mutableMap[it.id] = it
//delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource
// registerStubSource(it) // registerStubSource(it)
} }
} }

View file

@ -0,0 +1,47 @@
package eu.kanade.tachiyomi.source.online
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.create
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.model.SChapter
import uy.kohesive.injekt.injectLazy
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetManga
abstract class DelegatedHttpSource {
var delegate: HttpSource? = null
abstract val domainName: String
protected val getChapter: GetChapter by injectLazy()
protected val getManga: GetManga by injectLazy()
protected val network: NetworkHelper by injectLazy()
abstract fun canOpenUrl(uri: Uri): Boolean
abstract fun chapterUrl(uri: Uri): String?
open fun pageNumber(uri: Uri): Int? = uri.pathSegments.lastOrNull()?.toIntOrNull()
abstract suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>?
protected open suspend fun getMangaInfo(url: String): Manga? {
val id = delegate?.id ?: return null
val manga = Manga.create(url, "", id)
val networkManga = delegate?.getMangaDetails(manga.copy()) ?: return null
val newManga = MangaImpl().apply {
this.url = url
title = try { networkManga.title } catch (e: Exception) { "" }
source = id
}
newManga.copyFrom(networkManga)
return newManga
}
suspend fun getChapters(url: String): List<SChapter>? {
val id = delegate?.id ?: return null
val manga = Manga.create(url, "", id)
return delegate?.getChapterList(manga)
}
}

View file

@ -1,31 +1,22 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.toChapter import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource
import java.util.Locale import java.util.Locale
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
class Cubari(delegate: HttpSource) : class Cubari : DelegatedHttpSource() {
DelegatedHttpSource(delegate) {
private val getManga: GetManga = Injekt.get()
private val getChapter: GetChapter = Injekt.get()
override val lang = "all"
override val domainName: String = "cubari" override val domainName: String = "cubari"
override fun canOpenUrl(uri: Uri): Boolean = true override fun canOpenUrl(uri: Uri): Boolean = true
@ -33,24 +24,24 @@ class Cubari(delegate: HttpSource) :
override fun pageNumber(uri: Uri): Int? = uri.pathSegments.getOrNull(4)?.toIntOrNull() override fun pageNumber(uri: Uri): Int? = uri.pathSegments.getOrNull(4)?.toIntOrNull()
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? { override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
val cubariType = uri.pathSegments.getOrNull(1)?.lowercase(Locale.ROOT) ?: return null val cubariType = uri.pathSegments.getOrNull(1)?.lowercase(Locale.ROOT) ?: return null
val cubariPath = uri.pathSegments.getOrNull(2) ?: return null val cubariPath = uri.pathSegments.getOrNull(2) ?: return null
val chapterNumber = uri.pathSegments.getOrNull(3)?.replace("-", ".")?.toFloatOrNull() ?: return null val chapterNumber = uri.pathSegments.getOrNull(3)?.replace("-", ".")?.toFloatOrNull() ?: return null
val mangaUrl = "/read/$cubariType/$cubariPath" val mangaUrl = "/read/$cubariType/$cubariPath"
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val deferredManga = async { val deferredManga = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl) getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
} }
val deferredChapters = async { val deferredChapters = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate.id)?.let { manga -> getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!)?.let { manga ->
val chapters = getChapter.awaitAll(manga, false) val chapters = getChapter.awaitAll(manga, false)
val chapter = findChapter(chapters, cubariType, chapterNumber) val chapter = findChapter(chapters, cubariType, chapterNumber)
if (chapter != null) { if (chapter != null) {
return@async chapters return@async chapters
} }
} }
getChapterListByUrl(mangaUrl) getChapters(mangaUrl)
} }
val manga = deferredManga.await() val manga = deferredManga.await()
val chapters = deferredChapters.await() val chapters = deferredChapters.await()
@ -59,7 +50,11 @@ class Cubari(delegate: HttpSource) :
?: error( ?: error(
context.getString(MR.strings.chapter_not_found), context.getString(MR.strings.chapter_not_found),
) )
Triple(trueChapter, manga, chapters) if (manga != null) {
Triple(trueChapter, manga, chapters.orEmpty())
} else {
null
}
} }
} }

View file

@ -1,15 +1,15 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.toChapter import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource
import java.util.Locale import java.util.Locale
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -20,15 +20,10 @@ import okhttp3.CacheControl
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) { class MangaDex : DelegatedHttpSource() {
private val getManga: GetManga = Injekt.get()
override val lang: String = "all"
override val domainName: String = "mangadex" override val domainName: String = "mangadex"
@ -47,13 +42,13 @@ class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) {
return uri.pathSegments.getOrNull(2)?.toIntOrNull() return uri.pathSegments.getOrNull(2)?.toIntOrNull()
} }
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? { override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
val url = chapterUrl(uri) ?: return null val url = chapterUrl(uri) ?: return null
val request = val request =
GET("https:///api.mangadex.org/v2$url", delegate!!.headers, CacheControl.FORCE_NETWORK) GET("https:///api.mangadex.org/v2$url", delegate!!.headers, CacheControl.FORCE_NETWORK)
val response = network.client.newCall(request).await() val response = network.client.newCall(request).await()
if (response.code != 200) throw Exception("HTTP error ${response.code}") if (response.code != 200) throw Exception("HTTP error ${response.code}")
val body = response.body.string() val body = response.body.string().orEmpty()
if (body.isEmpty()) { if (body.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
} }
@ -61,19 +56,28 @@ class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) {
val jsonObject = Json.decodeFromString<MangaDexChapterData>(body) val jsonObject = Json.decodeFromString<MangaDexChapterData>(body)
val dataObject = jsonObject.data ?: throw Exception("Chapter not found") val dataObject = jsonObject.data ?: throw Exception("Chapter not found")
val mangaId = dataObject.mangaId ?: throw Exception("No manga associated with chapter") val mangaId = dataObject.mangaId ?: throw Exception("No manga associated with chapter")
val langCode = getRealLangCode(dataObject.language ?: "en").uppercase(Locale.getDefault())
// Use the correct MangaDex source based on the language code, or the api will not return
// the correct chapter list
delegate = sourceManager.getOnlineSources().find { it.toString() == "MangaDex ($langCode)" }
?: error("Source not found")
val mangaUrl = "/manga/$mangaId/" val mangaUrl = "/manga/$mangaId/"
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val deferredManga = async { val deferredManga = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl) getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
} }
val deferredChapters = async { getChapterListByUrl(mangaUrl) } val deferredChapters = async { getChapters(mangaUrl) }
val manga = deferredManga.await() val manga = deferredManga.await()
val chapters = deferredChapters.await() val chapters = deferredChapters.await()
val context = Injekt.get<PreferencesHelper>().context val context = Injekt.get<PreferencesHelper>().context
val trueChapter = chapters.find { it.url == "/api$url" }?.toChapter() ?: error( val trueChapter = chapters?.find { it.url == "/api$url" }?.toChapter() ?: error(
context.getString(MR.strings.chapter_not_found), context.getString(MR.strings.chapter_not_found),
) )
Triple(trueChapter, manga, chapters) if (manga != null) {
Triple(trueChapter, manga, chapters.orEmpty())
} else {
null
}
} }
} }

View file

@ -0,0 +1,110 @@
package eu.kanade.tachiyomi.source.online.english
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import okhttp3.FormBody
import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.i18n.MR
import yokai.util.lang.getString
open class FoolSlide(override val domainName: String, private val urlModifier: String = "") :
DelegatedHttpSource
() {
override fun canOpenUrl(uri: Uri): Boolean = true
override fun chapterUrl(uri: Uri): String? {
val offset = if (urlModifier.isEmpty()) 0 else 1
val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null
val lang = uri.pathSegments.getOrNull(2 + offset) ?: return null
val volume = uri.pathSegments.getOrNull(3 + offset) ?: return null
val chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null
val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()?.toString()
return "$urlModifier/read/" + listOfNotNull(
mangaName,
lang,
volume,
chapterNumber,
subChapterNumber,
).joinToString("/") + "/"
}
override fun pageNumber(uri: Uri): Int? {
val count = uri.pathSegments.count()
if (count > 2 && uri.pathSegments[count - 2] == "page") {
return super.pageNumber(uri)
}
return null
}
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
val offset = if (urlModifier.isEmpty()) 0 else 1
val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null
var chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null
val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()
if (subChapterNumber != null) {
chapterNumber += ".$subChapterNumber"
}
return withContext(Dispatchers.IO) {
val mangaUrl = "$urlModifier/series/$mangaName/"
val sourceId = delegate?.id ?: return@withContext null
val deferredManga = async {
getManga.awaitByUrlAndSource(mangaUrl, sourceId) ?: getManga(mangaUrl)
}
val chapterUrl = chapterUrl(uri)
val deferredChapters = async { getChapters(mangaUrl) }
val manga = deferredManga.await()
val chapters = deferredChapters.await()
val context = Injekt.get<PreferencesHelper>().context
val trueChapter = chapters?.find { it.url == chapterUrl }?.toChapter() ?: error(
context.getString(MR.strings.chapter_not_found),
)
if (manga != null) Triple(trueChapter, manga, chapters) else null
}
}
open suspend fun getManga(url: String): Manga? {
val request = GET("${delegate!!.baseUrl}$url")
val document = network.client.newCall(allowAdult(request)).await().asJsoup()
val mangaDetailsInfoSelector = "div.info"
val infoElement = document.select(mangaDetailsInfoSelector).first()?.text() ?: return null
return MangaImpl().apply {
this.url = url
source = delegate?.id ?: -1
title = infoElement.substringAfter("Title:").substringBefore("Author:").trim()
author = infoElement.substringAfter("Author:").substringBefore("Artist:").trim()
artist = infoElement.substringAfter("Artist:").substringBefore("Synopsis:").trim()
description = infoElement.substringAfter("Synopsis:").trim()
thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")?.trim()
}
}
/**
* Transform a GET request into a POST request that automatically authorizes all adult content
*/
private fun allowAdult(request: Request) = allowAdult(request.url.toString())
private fun allowAdult(url: String): Request {
return POST(
url,
body = FormBody.Builder()
.add("adult", "true")
.build(),
)
}
}

View file

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.source.online.english
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.lang.capitalizeWords
class KireiCake : FoolSlide("kireicake") {
override suspend fun getManga(url: String): Manga? {
val request = GET("${delegate!!.baseUrl}$url")
val document = network.client.newCall(request).await().asJsoup()
val mangaDetailsInfoSelector = "div.info"
return MangaImpl().apply {
this.url = url
source = delegate?.id ?: -1
title = document.select("$mangaDetailsInfoSelector li:has(b:contains(title))").first()
?.ownText()?.substringAfter(":")?.trim()
?: url.split("/").last().replace("_", " " + "").capitalizeWords()
description =
document.select("$mangaDetailsInfoSelector li:has(b:contains(description))").first()
?.ownText()?.substringAfter(":")
thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")
}
}
}

View file

@ -1,31 +1,24 @@
package eu.kanade.tachiyomi.source.online.english package eu.kanade.tachiyomi.source.online.english
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.toChapter import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.CacheControl import okhttp3.CacheControl
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
class MangaPlus(delegate: HttpSource) : class MangaPlus : DelegatedHttpSource() {
DelegatedHttpSource(delegate) {
private val getManga: GetManga = Injekt.get()
override val lang: String get() = delegate.lang
override val domainName: String = "jumpg-webapi.tokyo-cdn" override val domainName: String = "jumpg-webapi.tokyo-cdn"
private val titleIdRegex = private val titleIdRegex =
@ -41,11 +34,11 @@ class MangaPlus(delegate: HttpSource) :
override fun pageNumber(uri: Uri): Int? = null override fun pageNumber(uri: Uri): Int? = null
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? { override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
val url = chapterUrl(uri) ?: return null val url = chapterUrl(uri) ?: return null
val request = GET( val request = GET(
chapterUrlTemplate.replace("##", uri.pathSegments[1]), chapterUrlTemplate.replace("##", uri.pathSegments[1]),
delegate.headers, delegate!!.headers,
CacheControl.FORCE_NETWORK, CacheControl.FORCE_NETWORK,
) )
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
@ -60,15 +53,16 @@ class MangaPlus(delegate: HttpSource) :
val trimmedTitle = title.substring(0, title.length - 1) val trimmedTitle = title.substring(0, title.length - 1)
val mangaUrl = "#/titles/$titleId" val mangaUrl = "#/titles/$titleId"
val deferredManga = async { val deferredManga = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl) getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
} }
val deferredChapters = async { getChapterListByUrl(mangaUrl) } val deferredChapters = async { getChapters(mangaUrl) }
val manga = deferredManga.await() val manga = deferredManga.await()
val chapters = deferredChapters.await() val chapters = deferredChapters.await()
val context = Injekt.get<PreferencesHelper>().context val context = Injekt.get<PreferencesHelper>().context
val trueChapter = chapters.find { it.url == url }?.toChapter() ?: error( val trueChapter = chapters?.find { it.url == url }?.toChapter() ?: error(
context.getString(MR.strings.chapter_not_found), context.getString(MR.strings.chapter_not_found),
) )
if (manga != null) {
Triple( Triple(
trueChapter, trueChapter,
manga.apply { manga.apply {
@ -76,6 +70,9 @@ class MangaPlus(delegate: HttpSource) :
}, },
chapters, chapters,
) )
} else {
null
}
} }
} }
} }

View file

@ -5,26 +5,19 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.ViewCompositionStrategy
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import eu.kanade.tachiyomi.util.compose.LocalDialogHostState
import yokai.domain.DialogHostState
import yokai.presentation.theme.YokaiTheme import yokai.presentation.theme.YokaiTheme
abstract class BaseComposeController(bundle: Bundle? = null) : abstract class BaseComposeController(bundle: Bundle? = null) :
BaseController(bundle) { BaseController(bundle) {
override val shouldHideLegacyAppBar = true
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup, container: ViewGroup,
savedViewState: Bundle? savedViewState: Bundle?
): View { ): View {
setAppBarVisibility() hideLegacyAppBar()
return ComposeView(container.context).apply { return ComposeView(container.context).apply {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@ -32,18 +25,12 @@ abstract class BaseComposeController(bundle: Bundle? = null) :
) )
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { setContent {
val dialogHostState = remember { DialogHostState() }
YokaiTheme { YokaiTheme {
CompositionLocalProvider(
LocalDialogHostState provides dialogHostState,
LocalBackPress provides router::handleBack,
) {
ScreenContent() ScreenContent()
} }
} }
} }
} }
}
@Composable @Composable
abstract fun ScreenContent() abstract fun ScreenContent()

View file

@ -25,8 +25,6 @@ import kotlinx.coroutines.cancel
abstract class BaseController(bundle: Bundle? = null) : abstract class BaseController(bundle: Bundle? = null) :
Controller(bundle), BackHandlerControllerInterface, BaseControllerPreferenceControllerCommonInterface { Controller(bundle), BackHandlerControllerInterface, BaseControllerPreferenceControllerCommonInterface {
abstract val shouldHideLegacyAppBar: Boolean
lateinit var viewScope: CoroutineScope lateinit var viewScope: CoroutineScope
var isDragging = false var isDragging = false
@ -60,10 +58,6 @@ abstract class BaseController(bundle: Bundle? = null) :
open fun onViewCreated(view: View) { } open fun onViewCreated(view: View) { }
internal fun setAppBarVisibility() {
if (shouldHideLegacyAppBar) hideLegacyAppBar() else showLegacyAppBar()
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter && !isControllerVisible) { if (type.isEnter && !isControllerVisible) {
view?.alpha = 0f view?.alpha = 0f

View file

@ -20,13 +20,11 @@ import eu.kanade.tachiyomi.util.view.isControllerVisible
abstract class BaseLegacyController<VB : ViewBinding>(bundle: Bundle? = null) : abstract class BaseLegacyController<VB : ViewBinding>(bundle: Bundle? = null) :
BaseController(bundle) { BaseController(bundle) {
override val shouldHideLegacyAppBar = false
lateinit var binding: VB lateinit var binding: VB
val isBindingInitialized get() = this::binding.isInitialized val isBindingInitialized get() = this::binding.isInitialized
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
setAppBarVisibility() showLegacyAppBar()
binding = createBinding(inflater) binding = createBinding(inflater)
binding.root.backgroundColor = binding.root.context.getResourceColor(R.attr.background) binding.root.backgroundColor = binding.root.context.getResourceColor(R.attr.background)
return binding.root return binding.root

View file

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.ui.base.presenter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Presenter that mimic [cafe.adriel.voyager.core.model.StateScreenModel] for easier migration.
* Temporary class while we're migrating to Compose.
*/
abstract class StateCoroutinePresenter<S, C>(initialState: S) : BaseCoroutinePresenter<C>() {
protected val mutableState: MutableStateFlow<S> = MutableStateFlow(initialState)
val state: StateFlow<S> = mutableState.asStateFlow()
}

View file

@ -5,8 +5,6 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MenuItem import android.view.MenuItem
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileDownloadOff
import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
@ -214,7 +212,7 @@ class DownloadBottomSheet @JvmOverloads constructor(
setBottomSheet() setBottomSheet()
if (presenter.downloadQueueState.value.isEmpty()) { if (presenter.downloadQueueState.value.isEmpty()) {
binding.emptyView.show( binding.emptyView.show(
Icons.Filled.FileDownloadOff, R.drawable.ic_download_off_24dp,
MR.strings.nothing_is_downloading, MR.strings.nothing_is_downloading,
) )
} else { } else {

View file

@ -13,15 +13,15 @@ import eu.kanade.tachiyomi.util.lang.removeArticles
import eu.kanade.tachiyomi.util.system.isLTR import eu.kanade.tachiyomi.util.system.isLTR
import eu.kanade.tachiyomi.util.system.timeSpanFromNow import eu.kanade.tachiyomi.util.system.timeSpanFromNow
import eu.kanade.tachiyomi.util.system.withDefContext import eu.kanade.tachiyomi.util.system.withDefContext
import java.util.*
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.category.interactor.GetCategories
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.history.interactor.GetHistory
import yokai.domain.ui.UiPreferences import yokai.domain.ui.UiPreferences
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import java.util.*
import yokai.domain.category.interactor.GetCategories
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.history.interactor.GetHistory
/** /**
* Adapter storing a list of manga in a certain category. * Adapter storing a list of manga in a certain category.
@ -117,8 +117,8 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/ */
fun indexOf(manga: Manga): Int { fun indexOf(manga: Manga): Int {
return currentItems.indexOfFirst { return currentItems.indexOfFirst {
if (it is LibraryMangaItem) { if (it is LibraryItem) {
it.manga.manga.id == manga.id it.manga.id == manga.id
} else { } else {
false false
} }
@ -142,7 +142,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/ */
fun allIndexOf(manga: Manga): List<Int> { fun allIndexOf(manga: Manga): List<Int> {
return currentItems.mapIndexedNotNull { index, it -> return currentItems.mapIndexedNotNull { index, it ->
if (it is LibraryMangaItem && it.manga.manga.id == manga.id) { if (it is LibraryItem && it.manga.id == manga.id) {
index index
} else { } else {
null null
@ -164,7 +164,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} else { } else {
val filteredManga = withDefContext { mangas.filter { it.filter(s) } } val filteredManga = withDefContext { mangas.filter { it.filter(s) } }
if (filteredManga.isEmpty() && controller?.presenter?.showAllCategories == false) { if (filteredManga.isEmpty() && controller?.presenter?.showAllCategories == false) {
val catId = (mangas.firstOrNull() as? LibraryMangaItem)?.let { it.header?.catId ?: it.manga.category } val catId = mangas.firstOrNull()?.let { it.header?.catId ?: it.manga.category }
val blankItem = catId?.let { controller.presenter.blankItem(it) } val blankItem = catId?.let { controller.presenter.blankItem(it) }
updateDataSet(blankItem ?: emptyList()) updateDataSet(blankItem ?: emptyList())
} else { } else {
@ -173,7 +173,6 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} }
isLongPressDragEnabled = libraryListener?.canDrag() == true && s.isNullOrBlank() isLongPressDragEnabled = libraryListener?.canDrag() == true && s.isNullOrBlank()
setItemsPerCategoryMap() setItemsPerCategoryMap()
notifyDataSetChanged()
} }
private fun getFirstLetter(name: String): String { private fun getFirstLetter(name: String): String {
@ -203,19 +202,18 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
vibrateOnCategoryChange(item.category.name) vibrateOnCategoryChange(item.category.name)
item.category.name item.category.name
} }
is LibraryPlaceholderItem -> { is LibraryItem -> {
item.header?.category?.name.orEmpty() val text = if (item.manga.isBlank()) {
} return item.header?.category?.name.orEmpty()
is LibraryMangaItem -> { } else {
val text =
when (getSort(position)) { when (getSort(position)) {
LibrarySort.DragAndDrop -> { LibrarySort.DragAndDrop -> {
if (item.header.category.isDynamic && item.manga.manga.id != null) { if (item.header.category.isDynamic && item.manga.id != null) {
// FIXME: Don't do blocking // FIXME: Don't do blocking
val category = runBlocking { getCategories.awaitByMangaId(item.manga.manga.id!!) }.firstOrNull()?.name val category = runBlocking { getCategories.awaitByMangaId(item.manga.id!!) }.firstOrNull()?.name
category ?: context.getString(MR.strings.default_value) category ?: context.getString(MR.strings.default_value)
} else { } else {
val title = item.manga.manga.title val title = item.manga.title
if (preferences.removeArticles().get()) { if (preferences.removeArticles().get()) {
title.removeArticles().chop(15) title.removeArticles().chop(15)
} else { } else {
@ -224,14 +222,14 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} }
} }
LibrarySort.DateFetched -> { LibrarySort.DateFetched -> {
val id = item.manga.manga.id ?: return "" val id = item.manga.id ?: return ""
// FIXME: Don't do blocking // FIXME: Don't do blocking
val history = runBlocking { getChapter.awaitAll(id, false) } val history = runBlocking { getChapter.awaitAll(id, false) }
val last = history.maxOfOrNull { it.date_fetch } val last = history.maxOfOrNull { it.date_fetch }
context.timeSpanFromNow(MR.strings.fetched_, last ?: 0) context.timeSpanFromNow(MR.strings.fetched_, last ?: 0)
} }
LibrarySort.LastRead -> { LibrarySort.LastRead -> {
val id = item.manga.manga.id ?: return "" val id = item.manga.id ?: return ""
// FIXME: Don't do blocking // FIXME: Don't do blocking
val history = runBlocking { getHistory.awaitAllByMangaId(id) } val history = runBlocking { getHistory.awaitAllByMangaId(id) }
val last = history.maxOfOrNull { it.last_read } val last = history.maxOfOrNull { it.last_read }
@ -258,21 +256,19 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} }
} }
LibrarySort.LatestChapter -> { LibrarySort.LatestChapter -> {
context.timeSpanFromNow(MR.strings.updated_, item.manga.manga.last_update) context.timeSpanFromNow(MR.strings.updated_, item.manga.last_update)
} }
LibrarySort.DateAdded -> { LibrarySort.DateAdded -> {
context.timeSpanFromNow(MR.strings.added_, item.manga.manga.date_added) context.timeSpanFromNow(MR.strings.added_, item.manga.date_added)
} }
LibrarySort.Title -> { LibrarySort.Title -> {
val title = if (preferences.removeArticles().get()) { val title = if (preferences.removeArticles().get()) {
item.manga.manga.title.removeArticles() item.manga.title.removeArticles()
} else { } else {
item.manga.manga.title item.manga.title
} }
getFirstLetter(title) getFirstLetter(title)
} }
LibrarySort.Random -> {
context.getString(MR.strings.random)
} }
} }
if (!isSingleCategory) { if (!isSingleCategory) {

View file

@ -28,8 +28,6 @@ import android.widget.ImageView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HeartBroken
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@ -451,7 +449,7 @@ open class LibraryController(
private fun setActiveCategory() { private fun setActiveCategory() {
val currentCategory = presenter.categories.indexOfFirst { val currentCategory = presenter.categories.indexOfFirst {
if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategoryId == it.id if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategory == it.id
} }
if (currentCategory > -1) { if (currentCategory > -1) {
binding.categoryRecycler.setCategories(currentCategory) binding.categoryRecycler.setCategories(currentCategory)
@ -521,13 +519,14 @@ open class LibraryController(
} }
private fun openRandomManga(global: Boolean) { private fun openRandomManga(global: Boolean) {
val items = val items = if (global) {
if (global) { presenter.currentLibraryItems } else { adapter.currentItems } presenter.allLibraryItems
.filterIsInstance<LibraryMangaItem>() } else {
.filter { !it.manga.manga.initialized || it.manga.unread > 0 } adapter.currentItems
}.filter { (it is LibraryItem && !it.manga.isBlank() && !it.manga.isHidden() && (!it.manga.initialized || it.manga.unread > 0)) }
if (items.isNotEmpty()) { if (items.isNotEmpty()) {
val item = items.random() as LibraryMangaItem val item = items.random() as LibraryItem
openManga(item.manga.manga) openManga(item.manga)
} }
} }
@ -558,7 +557,7 @@ open class LibraryController(
} }
presenter.groupType = item presenter.groupType = item
shouldScrollToTop = true shouldScrollToTop = true
presenter.updateLibrary() presenter.getLibrary()
true true
}.show() }.show()
} }
@ -661,7 +660,7 @@ open class LibraryController(
createActionModeIfNeeded() createActionModeIfNeeded()
} }
if (presenter.libraryItemsToDisplay.isNotEmpty() && !isSubClass) { if (presenter.libraryItems.isNotEmpty() && !isSubClass) {
presenter.restoreLibrary() presenter.restoreLibrary()
if (justStarted) { if (justStarted) {
val activityBinding = activityBinding ?: return val activityBinding = activityBinding ?: return
@ -705,7 +704,7 @@ open class LibraryController(
if (!LibraryUpdateJob.isRunning(context)) { if (!LibraryUpdateJob.isRunning(context)) {
when { when {
!presenter.showAllCategories && presenter.groupType == BY_DEFAULT -> { !presenter.showAllCategories && presenter.groupType == BY_DEFAULT -> {
presenter.currentCategory?.let { presenter.findCurrentCategory()?.let {
updateLibrary(it) updateLibrary(it)
} }
} }
@ -903,7 +902,7 @@ open class LibraryController(
} }
} else { } else {
val newOffset = val newOffset =
presenter.categories.indexOfFirst { presenter.currentCategoryId == it.id } + presenter.categories.indexOfFirst { presenter.currentCategory == it.id } +
(if (next) 1 else -1) (if (next) 1 else -1)
if (if (!next) { if (if (!next) {
newOffset > -1 newOffset > -1
@ -1012,7 +1011,7 @@ open class LibraryController(
override fun getSpanSize(position: Int): Int { override fun getSpanSize(position: Int): Int {
if (libraryLayout == LibraryItem.LAYOUT_LIST) return managerSpanCount if (libraryLayout == LibraryItem.LAYOUT_LIST) return managerSpanCount
val item = this@LibraryController.mAdapter?.getItem(position) val item = this@LibraryController.mAdapter?.getItem(position)
return if (item is LibraryHeaderItem || item is SearchGlobalItem || item is LibraryPlaceholderItem) { return if (item is LibraryHeaderItem || item is SearchGlobalItem || (item is LibraryItem && item.manga.isBlank())) {
managerSpanCount managerSpanCount
} else { } else {
1 1
@ -1057,7 +1056,7 @@ open class LibraryController(
if (type.isEnter) { if (type.isEnter) {
binding.filterBottomSheet.filterBottomSheet.isVisible = true binding.filterBottomSheet.filterBottomSheet.isVisible = true
if (type == ControllerChangeType.POP_ENTER) { if (type == ControllerChangeType.POP_ENTER) {
presenter.updateLibrary() presenter.getLibrary()
isPoppingIn = true isPoppingIn = true
} }
binding.recyclerCover.isClickable = false binding.recyclerCover.isClickable = false
@ -1096,7 +1095,7 @@ open class LibraryController(
if (!isBindingInitialized) return if (!isBindingInitialized) return
updateFilterSheetY() updateFilterSheetY()
if (observeLater) { if (observeLater) {
presenter.updateLibrary() presenter.getLibrary()
} }
} }
@ -1136,7 +1135,7 @@ open class LibraryController(
binding.emptyView.hide() binding.emptyView.hide()
} else { } else {
binding.emptyView.show( binding.emptyView.show(
Icons.Filled.HeartBroken, R.drawable.ic_heart_off_24dp,
if (hasActiveFilters) { if (hasActiveFilters) {
MR.strings.no_matches_for_filters MR.strings.no_matches_for_filters
} else { } else {
@ -1375,7 +1374,7 @@ open class LibraryController(
setActiveCategory() setActiveCategory()
return return
} }
val headerPosition = mAdapter?.indexOf(pos) ?: return val headerPosition = adapter.indexOf(pos)
if (headerPosition > -1) { if (headerPosition > -1) {
val activityBinding = activityBinding ?: return val activityBinding = activityBinding ?: return
val index = adapter.headerItems.indexOf(adapter.getItem(headerPosition)) val index = adapter.headerItems.indexOf(adapter.getItem(headerPosition))
@ -1409,7 +1408,7 @@ open class LibraryController(
private fun onRefresh() { private fun onRefresh() {
showCategories(false) showCategories(false)
presenter.updateLibrary() presenter.getLibrary()
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
} }
@ -1433,14 +1432,14 @@ open class LibraryController(
val isShowAllCategoriesSet = preferences.showAllCategories().get() val isShowAllCategoriesSet = preferences.showAllCategories().get()
if (!query.isNullOrBlank() && this.query.isBlank() && !isShowAllCategoriesSet) { if (!query.isNullOrBlank() && this.query.isBlank() && !isShowAllCategoriesSet) {
presenter.forceShowAllCategories = preferences.showAllCategoriesWhenSearchingSingleCategory().get() presenter.forceShowAllCategories = preferences.showAllCategoriesWhenSearchingSingleCategory().get()
presenter.updateLibrary() presenter.getLibrary()
} else if (query.isNullOrBlank() && this.query.isNotBlank() && !isShowAllCategoriesSet) { } else if (query.isNullOrBlank() && this.query.isNotBlank() && !isShowAllCategoriesSet) {
if (!isSubClass) { if (!isSubClass) {
preferences.showAllCategoriesWhenSearchingSingleCategory() preferences.showAllCategoriesWhenSearchingSingleCategory()
.set(presenter.forceShowAllCategories) .set(presenter.forceShowAllCategories)
} }
presenter.forceShowAllCategories = false presenter.forceShowAllCategories = false
presenter.updateLibrary() presenter.getLibrary()
} }
if (query != this.query && !query.isNullOrBlank()) { if (query != this.query && !query.isNullOrBlank()) {
@ -1458,7 +1457,7 @@ open class LibraryController(
adapter.removeAllScrollableHeaders() adapter.removeAllScrollableHeaders()
} }
adapter.setFilter(query) adapter.setFilter(query)
if (presenter.currentLibraryItems.isEmpty()) return true if (presenter.allLibraryItems.isEmpty()) return true
viewScope.launchUI { viewScope.launchUI {
adapter.performFilterAsync() adapter.performFilterAsync()
} }
@ -1477,6 +1476,7 @@ open class LibraryController(
} }
private fun setSelection(manga: Manga, selected: Boolean) { private fun setSelection(manga: Manga, selected: Boolean) {
if (manga.isBlank()) return
val currentMode = adapter.mode val currentMode = adapter.mode
if (selected) { if (selected) {
if (selectedMangas.add(manga)) { if (selectedMangas.add(manga)) {
@ -1526,7 +1526,7 @@ open class LibraryController(
toggleSelection(position) toggleSelection(position)
return return
} }
val manga = (adapter.getItem(position) as? LibraryMangaItem)?.manga?.manga ?: return val manga = (adapter.getItem(position) as? LibraryItem)?.manga ?: return
val activity = activity ?: return val activity = activity ?: return
val chapter = presenter.getFirstUnread(manga) ?: return val chapter = presenter.getFirstUnread(manga) ?: return
activity.apply { activity.apply {
@ -1542,8 +1542,9 @@ open class LibraryController(
} }
private fun toggleSelection(position: Int) { private fun toggleSelection(position: Int) {
val item = adapter.getItem(position) as? LibraryMangaItem ?: return val item = adapter.getItem(position) as? LibraryItem ?: return
setSelection(item.manga.manga, !adapter.isSelected(position)) if (item.manga.isBlank()) return
setSelection(item.manga, !adapter.isSelected(position))
invalidateActionMode() invalidateActionMode()
} }
@ -1559,14 +1560,14 @@ open class LibraryController(
* @return true if the item should be selected, false otherwise. * @return true if the item should be selected, false otherwise.
*/ */
override fun onItemClick(view: View?, position: Int): Boolean { override fun onItemClick(view: View?, position: Int): Boolean {
val item = adapter.getItem(position) as? LibraryMangaItem ?: return false val item = adapter.getItem(position) as? LibraryItem ?: return false
return if (adapter.mode == SelectableAdapter.Mode.MULTI) { return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
snack?.dismiss() snack?.dismiss()
lastClickPosition = position lastClickPosition = position
toggleSelection(position) toggleSelection(position)
false false
} else { } else {
openManga(item.manga.manga) openManga(item.manga)
false false
} }
} }
@ -1588,10 +1589,10 @@ open class LibraryController(
*/ */
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
val item = adapter.getItem(position) val item = adapter.getItem(position)
if (item !is LibraryMangaItem) return if (item !is LibraryItem) return
snack?.dismiss() snack?.dismiss()
if (libraryLayout == LibraryItem.LAYOUT_COVER_ONLY_GRID && actionMode == null) { if (libraryLayout == LibraryItem.LAYOUT_COVER_ONLY_GRID && actionMode == null) {
snack = view?.snack(item.manga.manga.title) { snack = view?.snack(item.manga.title) {
anchorView = activityBinding?.bottomNav anchorView = activityBinding?.bottomNav
view.elevation = 15f.dpToPx view.elevation = 15f.dpToPx
} }
@ -1640,14 +1641,14 @@ open class LibraryController(
if (mangaId == null) { if (mangaId == null) {
adapter.getHeaderPositions().forEach { adapter.notifyItemChanged(it) } adapter.getHeaderPositions().forEach { adapter.notifyItemChanged(it) }
} else { } else {
presenter.updateLibrary() presenter.updateManga()
} }
} }
private fun setSelection(position: Int, selected: Boolean = true) { private fun setSelection(position: Int, selected: Boolean = true) {
val item = adapter.getItem(position) as? LibraryMangaItem ?: return val item = adapter.getItem(position) as? LibraryItem ?: return
setSelection(item.manga.manga, selected) setSelection(item.manga, selected)
invalidateActionMode() invalidateActionMode()
} }
@ -1671,7 +1672,7 @@ open class LibraryController(
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean { override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition) if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition)
val item = adapter.getItem(fromPosition) as? LibraryMangaItem ?: return false val item = adapter.getItem(fromPosition) as? LibraryItem ?: return false
val newHeader = adapter.getSectionHeader(toPosition) as? LibraryHeaderItem val newHeader = adapter.getSectionHeader(toPosition) as? LibraryHeaderItem
if (toPosition < 1) return false if (toPosition < 1) return false
return (adapter.getItem(toPosition) !is LibraryHeaderItem) && ( return (adapter.getItem(toPosition) !is LibraryHeaderItem) && (
@ -1686,18 +1687,18 @@ open class LibraryController(
lastItem = null lastItem = null
isDragging = false isDragging = false
binding.swipeRefresh.isEnabled = true binding.swipeRefresh.isEnabled = true
if (mAdapter == null || adapter.selectedItemCount > 0) { if (adapter.selectedItemCount > 0) {
lastItemPosition = null lastItemPosition = null
return return
} }
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
// if nothing moved // if nothing moved
if (lastItemPosition == null) return if (lastItemPosition == null) return
val item = adapter.getItem(position) as? LibraryMangaItem ?: return val item = adapter.getItem(position) as? LibraryItem ?: return
val newHeader = adapter.getSectionHeader(position) as? LibraryHeaderItem val newHeader = adapter.getSectionHeader(position) as? LibraryHeaderItem
val libraryItems = getSectionItems(adapter.getSectionHeader(position), item) val libraryItems = getSectionItems(adapter.getSectionHeader(position), item)
.filterIsInstance<LibraryMangaItem>() .filterIsInstance<LibraryItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id } val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
if (newHeader?.category?.id == item.manga.category) { if (newHeader?.category?.id == item.manga.category) {
presenter.rearrangeCategory(item.manga.category, mangaIds) presenter.rearrangeCategory(item.manga.category, mangaIds)
} else { } else {
@ -1819,7 +1820,7 @@ open class LibraryController(
val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return
if (!category.isDynamic) { if (!category.isDynamic) {
ManageCategoryDialog(category) { ManageCategoryDialog(category) {
presenter.updateLibrary() presenter.getLibrary()
}.showDialog(router) }.showDialog(router)
} }
} }
@ -1829,8 +1830,8 @@ open class LibraryController(
if (category?.isDynamic == false && sortBy == LibrarySort.DragAndDrop.categoryValue) { if (category?.isDynamic == false && sortBy == LibrarySort.DragAndDrop.categoryValue) {
val item = adapter.findCategoryHeader(catId) ?: return val item = adapter.findCategoryHeader(catId) ?: return
val libraryItems = adapter.getSectionItems(item) val libraryItems = adapter.getSectionItems(item)
.filterIsInstance<LibraryMangaItem>() .filterIsInstance<LibraryItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id } val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
presenter.rearrangeCategory(catId, mangaIds) presenter.rearrangeCategory(catId, mangaIds)
} else { } else {
presenter.sortCategory(catId, sortBy) presenter.sortCategory(catId, sortBy)
@ -1912,7 +1913,7 @@ open class LibraryController(
isGone = true isGone = true
setOnClickListener { setOnClickListener {
presenter.forceShowAllCategories = !presenter.forceShowAllCategories presenter.forceShowAllCategories = !presenter.forceShowAllCategories
presenter.updateLibrary() presenter.getLibrary()
isSelected = presenter.forceShowAllCategories isSelected = presenter.forceShowAllCategories
} }
val pad = 12.dpToPx val pad = 12.dpToPx
@ -2192,7 +2193,7 @@ open class LibraryController(
val activity = activity ?: return val activity = activity ?: return
viewScope.launchIO { viewScope.launchIO {
selectedMangas.toList().moveCategories(activity) { selectedMangas.toList().moveCategories(activity) {
presenter.updateLibrary() presenter.getLibrary()
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
} }
} }

View file

@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.backgroundColor import eu.kanade.tachiyomi.util.view.backgroundColor
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import yokai.util.coil.loadManga import yokai.presentation.core.util.coil.loadManga
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -64,24 +64,23 @@ class LibraryGridHolder(
* @param item the manga item to bind. * @param item the manga item to bind.
*/ */
override fun onSetValues(item: LibraryItem) { override fun onSetValues(item: LibraryItem) {
if (item !is LibraryMangaItem) throw IllegalStateException("Only LibraryMangaItem can use grid holder")
// Update the title and subtitle of the manga. // Update the title and subtitle of the manga.
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root) setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.playButton.transitionName = "library chapter $bindingAdapterPosition transition" binding.playButton.transitionName = "library chapter $bindingAdapterPosition transition"
binding.constraintLayout.isVisible = item.manga.manga.id != Long.MIN_VALUE binding.constraintLayout.isVisible = !item.manga.isBlank()
binding.title.text = item.manga.manga.title.highlightText(item.filter, color) binding.title.text = item.manga.title.highlightText(item.filter, color)
binding.behindTitle.text = item.manga.manga.title binding.behindTitle.text = item.manga.title
val mangaColor = item.manga.manga.dominantCoverColors val mangaColor = item.manga.dominantCoverColors
binding.coverConstraint.backgroundColor = mangaColor?.first ?: itemView.context.getResourceColor(R.attr.background) binding.coverConstraint.backgroundColor = mangaColor?.first ?: itemView.context.getResourceColor(R.attr.background)
binding.behindTitle.setTextColor( binding.behindTitle.setTextColor(
mangaColor?.second ?: itemView.context.getResourceColor(R.attr.colorOnBackground), mangaColor?.second ?: itemView.context.getResourceColor(R.attr.colorOnBackground),
) )
val authorArtist = if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) { val authorArtist = if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
item.manga.manga.author?.trim() ?: "" item.manga.author?.trim() ?: ""
} else { } else {
listOfNotNull( listOfNotNull(
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() }, item.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() }, item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
).joinToString(", ") ).joinToString(", ")
} }
binding.subtitle.text = authorArtist.highlightText(item.filter, color) binding.subtitle.text = authorArtist.highlightText(item.filter, color)
@ -102,7 +101,7 @@ class LibraryGridHolder(
// Update the cover. // Update the cover.
binding.coverThumbnail.dispose() binding.coverThumbnail.dispose()
setCover(item.manga.manga) setCover(item.manga)
} }
override fun toggleActivation() { override fun toggleActivation() {

View file

@ -32,17 +32,14 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.compatToolTipText import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.util.view.setText import eu.kanade.tachiyomi.util.view.setText
import eu.kanade.tachiyomi.util.view.text import eu.kanade.tachiyomi.util.view.text
import kotlin.random.Random
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import yokai.domain.library.LibraryPreferences
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) : class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
BaseFlexibleViewHolder(view, adapter, true) { BaseFlexibleViewHolder(view, adapter, true) {
private val libraryPreferences: LibraryPreferences = Injekt.get()
private val binding = LibraryCategoryHeaderItemBinding.bind(view) private val binding = LibraryCategoryHeaderItemBinding.bind(view)
val progressDrawableStart = CircularProgressDrawable(itemView.context) val progressDrawableStart = CircularProgressDrawable(itemView.context)
val progressDrawableEnd = CircularProgressDrawable(itemView.context) val progressDrawableEnd = CircularProgressDrawable(itemView.context)
@ -186,17 +183,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
binding.categoryTitle.text = categoryName + binding.categoryTitle.text = categoryName +
if (adapter.showNumber) { if (adapter.showNumber) {
val filteredCount = adapter.currentItems.count { " (${adapter.itemsPerCategory[item.catId]})"
it is LibraryMangaItem && it.header?.catId == item.catId
}
val totalCount = adapter.itemsPerCategory[item.catId] ?: 0
val searchText = adapter.getFilter(String::class.java)
var countText = if (searchText.isNullOrBlank()) {
" ($totalCount)"
} else {
" ($filteredCount/$totalCount)"
}
countText
} else { "" } } else { "" }
if (category.sourceId != null) { if (category.sourceId != null) {
val icon = adapter.sourceManager.get(category.sourceId!!)?.icon() val icon = adapter.sourceManager.get(category.sourceId!!)?.icon()
@ -211,7 +198,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
val isAscending = category.isAscending() val isAscending = category.isAscending()
val sortingMode = category.sortingMode() val sortingMode = category.sortingMode()
val sortDrawable = getSortRes(sortingMode, isAscending, category.isDynamic, false) val sortDrawable = getSortRes(sortingMode, isAscending, R.drawable.ic_sort_24dp)
binding.categorySort.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, sortDrawable, 0) binding.categorySort.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, sortDrawable, 0)
binding.categorySort.setText(category.sortRes()) binding.categorySort.setText(category.sortRes())
@ -276,7 +263,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
val cat = category ?: return val cat = category ?: return
adapter.controller?.activity?.let { activity -> adapter.controller?.activity?.let { activity ->
val items = LibrarySort.entries.map { it.menuSheetItem(cat.isDynamic) } val items = LibrarySort.entries.map { it.menuSheetItem(cat.isDynamic) }
val sortingMode = cat.sortingMode() ?: if (!cat.isDynamic) LibrarySort.DragAndDrop else null val sortingMode = cat.sortingMode(true)
val sheet = MaterialMenuSheet( val sheet = MaterialMenuSheet(
activity, activity,
items, items,
@ -285,54 +272,58 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
) { sheet, item -> ) { sheet, item ->
onCatSortClicked(cat, item) onCatSortClicked(cat, item)
val nCategory = (adapter.getItem(flexibleAdapterPosition) as? LibraryHeaderItem)?.category val nCategory = (adapter.getItem(flexibleAdapterPosition) as? LibraryHeaderItem)?.category
sheet.updateSortIcon(nCategory, LibrarySort.valueOf(item)) val isAscending = nCategory?.isAscending() ?: false
val drawableRes = getSortRes(item, isAscending)
sheet.setDrawable(item, drawableRes)
false false
} }
sheet.updateSortIcon(cat, sortingMode) val isAscending = cat.isAscending()
val drawableRes = getSortRes(sortingMode, isAscending)
sheet.setDrawable(sortingMode?.mainValue ?: -1, drawableRes)
sheet.show() sheet.show()
} }
} }
private fun MaterialMenuSheet.updateSortIcon(category: Category?, sortingMode: LibrarySort?) {
val isAscending = category?.isAscending() ?: false
val drawableRes = getSortRes(sortingMode, isAscending, category?.isDynamic ?: false, true)
this.setDrawable(sortingMode?.mainValue ?: -1, drawableRes)
}
private fun getSortRes( private fun getSortRes(
sortMode: LibrarySort?, sortMode: LibrarySort?,
isAscending: Boolean, isAscending: Boolean,
isDynamic: Boolean, @DrawableRes defaultDrawableRes: Int = R.drawable.ic_check_24dp,
onSelection: Boolean,
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_sort_24dp,
@DrawableRes defaultSelectedDrawableRes: Int = R.drawable.ic_check_24dp,
): Int { ): Int {
sortMode ?: return if (onSelection) defaultSelectedDrawableRes else defaultDrawableRes sortMode ?: return defaultDrawableRes
return when (sortMode) {
if (sortMode.isDirectional) { LibrarySort.DragAndDrop -> defaultDrawableRes
return if (if (sortMode.hasInvertedSort) !isAscending else isAscending) { else -> {
if (if (sortMode.hasInvertedSort) !isAscending else isAscending) {
R.drawable.ic_arrow_downward_24dp R.drawable.ic_arrow_downward_24dp
} else { } else {
R.drawable.ic_arrow_upward_24dp R.drawable.ic_arrow_upward_24dp
} }
} }
if (onSelection) {
return when(sortMode) {
LibrarySort.DragAndDrop -> R.drawable.ic_check_24dp
LibrarySort.Random -> R.drawable.ic_refresh_24dp
else -> defaultSelectedDrawableRes
} }
} }
return sortMode.iconRes(isDynamic) private fun getSortRes(
sortingMode: Int?,
isAscending: Boolean,
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_check_24dp,
): Int {
sortingMode ?: return defaultDrawableRes
return when (val sortMode = LibrarySort.valueOf(sortingMode)) {
LibrarySort.DragAndDrop -> defaultDrawableRes
else -> {
if (if (sortMode?.hasInvertedSort == true) !isAscending else isAscending) {
R.drawable.ic_arrow_downward_24dp
} else {
R.drawable.ic_arrow_upward_24dp
}
}
}
} }
private fun onCatSortClicked(category: Category, menuId: Int?) { private fun onCatSortClicked(category: Category, menuId: Int?) {
val (mode, modType) = if (menuId == null) { val modType = if (menuId == null) {
val sortingMode = category.sortingMode() ?: LibrarySort.Title val sortingMode = category.sortingMode() ?: LibrarySort.Title
sortingMode to if (category.isAscending()) {
if (sortingMode != LibrarySort.Random && category.isAscending()) {
sortingMode.categoryValueDescending sortingMode.categoryValueDescending
} else { } else {
sortingMode.categoryValue sortingMode.categoryValue
@ -343,10 +334,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
onCatSortClicked(category, null) onCatSortClicked(category, null)
return return
} }
sortingMode to sortingMode.categoryValue sortingMode.categoryValue
}
if (mode == LibrarySort.Random) {
libraryPreferences.randomSortSeed().set(Random.nextInt())
} }
adapter.libraryListener?.sortCategory(category.id!!, modType) adapter.libraryListener?.sortCategory(category.id!!, modType)
} }

View file

@ -5,6 +5,9 @@ import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
@ -14,7 +17,9 @@ import eu.kanade.tachiyomi.util.view.setCards
* Generic class used to hold the displayed data of a manga in the library. * Generic class used to hold the displayed data of a manga in the library.
* @param view the inflated view for this holder. * @param view the inflated view for this holder.
* @param adapter the adapter handling this holder. * @param adapter the adapter handling this holder.
* @param listener a listener to react to the single tap and long tap events.
*/ */
abstract class LibraryHolder( abstract class LibraryHolder(
view: View, view: View,
val adapter: LibraryCategoryAdapter, val adapter: LibraryCategoryAdapter,
@ -38,7 +43,7 @@ abstract class LibraryHolder(
*/ */
abstract fun onSetValues(item: LibraryItem) abstract fun onSetValues(item: LibraryItem)
fun setUnreadBadge(badge: LibraryBadge, item: LibraryMangaItem) { fun setUnreadBadge(badge: LibraryBadge, item: LibraryItem) {
val showTotal = item.header.category.sortingMode() == LibrarySort.TotalChapters val showTotal = item.header.category.sortingMode() == LibrarySort.TotalChapters
badge.setUnreadDownload( badge.setUnreadDownload(
when { when {
@ -49,7 +54,7 @@ abstract class LibraryHolder(
}, },
when { when {
item.downloadCount == -1 -> -1 item.downloadCount == -1 -> -1
item.manga.manga.isLocal() -> -2 item.manga.isLocal() -> -2
else -> item.downloadCount else -> item.downloadCount
}, },
showTotal, showTotal,
@ -58,7 +63,7 @@ abstract class LibraryHolder(
) )
} }
fun setReadingButton(item: LibraryMangaItem) { fun setReadingButton(item: LibraryItem) {
itemView.findViewById<View>(R.id.play_layout)?.isVisible = itemView.findViewById<View>(R.id.play_layout)?.isVisible =
item.manga.unread > 0 && !item.hideReadingButton item.manga.unread > 0 && !item.hideReadingButton
} }
@ -75,8 +80,8 @@ abstract class LibraryHolder(
override fun onLongClick(view: View?): Boolean { override fun onLongClick(view: View?): Boolean {
return if (adapter.isLongPressDragEnabled) { return if (adapter.isLongPressDragEnabled) {
val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryMangaItem)?.manga val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryItem)?.manga
if (manga != null && !isDraggable) { if (manga != null && !isDraggable && !manga.isBlank() && !manga.isHidden()) {
adapter.mItemLongClickListener.onItemLongClick(flexibleAdapterPosition) adapter.mItemLongClickListener.onItemLongClick(flexibleAdapterPosition)
toggleActivation() toggleActivation()
true true

View file

@ -1,48 +1,205 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.content.Context import android.content.Context
import androidx.annotation.CallSuper import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFilterable import eu.davidea.flexibleadapter.items.IFilterable
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.seriesType
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.ui.UiPreferences import yokai.domain.ui.UiPreferences
abstract class LibraryItem( class LibraryItem(
val manga: LibraryManga,
header: LibraryHeaderItem, header: LibraryHeaderItem,
internal val context: Context?, private val context: Context?,
) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> { ) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
var downloadCount = -1
var unreadType = 2
var sourceLanguage: String? = null
var filter = "" var filter = ""
internal val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val uiPreferences: UiPreferences by injectLazy() private val uiPreferences: UiPreferences by injectLazy()
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
internal val uniformSize: Boolean private val uniformSize: Boolean
get() = uiPreferences.uniformGrid().get() get() = uiPreferences.uniformGrid().get()
internal val libraryLayout: Int private val libraryLayout: Int
get() = preferences.libraryLayout().get() get() = preferences.libraryLayout().get()
val hideReadingButton: Boolean val hideReadingButton: Boolean
get() = preferences.hideStartReadingButton().get() get() = preferences.hideStartReadingButton().get()
@CallSuper override fun getLayoutRes(): Int {
return if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
R.layout.manga_list_item
} else {
R.layout.manga_grid_item
}
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
val parent = adapter.recyclerView
return if (parent is AutofitRecyclerView) {
val libraryLayout = libraryLayout
val isFixedSize = uniformSize
if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
LibraryListHolder(view, adapter as LibraryCategoryAdapter)
} else {
view.apply {
val isStaggered = parent.layoutManager is StaggeredGridLayoutManager
val binding = MangaGridItemBinding.bind(this)
binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID
if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) {
binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID
binding.card.setCardForegroundColor(
ContextCompat.getColorStateList(
context,
R.color.library_comfortable_grid_foreground,
),
)
}
if (isFixedSize) {
binding.constraintLayout.layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
binding.coverThumbnail.maxHeight = Int.MAX_VALUE
binding.coverThumbnail.minimumHeight = 0
binding.constraintLayout.minHeight = 0
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
binding.coverThumbnail.adjustViewBounds = false
binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
dimensionRatio = "15:22"
}
}
if (libraryLayout != LAYOUT_COMFORTABLE_GRID) {
binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> {
bottomMargin = (if (isStaggered) 2 else 6).dpToPx
}
}
binding.setBGAndFG(libraryLayout)
}
val gridHolder = LibraryGridHolder(
view,
adapter as LibraryCategoryAdapter,
libraryLayout == LAYOUT_COMPACT_GRID,
isFixedSize,
)
if (!isFixedSize) {
gridHolder.setFreeformCoverRatio(manga, parent)
}
gridHolder
}
} else {
LibraryListHolder(view, adapter as LibraryCategoryAdapter)
}
}
override fun bindViewHolder( override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LibraryHolder, holder: LibraryHolder,
position: Int, position: Int,
payloads: MutableList<Any?>?, payloads: MutableList<Any?>?,
) { ) {
if (holder is LibraryGridHolder && !holder.fixedSize) {
holder.setFreeformCoverRatio(manga, adapter.recyclerView as? AutofitRecyclerView)
}
holder.onSetValues(this) holder.onSetValues(this)
(holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position)) (holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position))
(holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)?.isFullSpan = this is LibraryPlaceholderItem val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams
layoutParams?.isFullSpan = manga.isBlank()
if (libraryLayout == LAYOUT_COVER_ONLY_GRID) {
holder.itemView.compatToolTipText = manga.title
}
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return !manga.isBlank() && header.category.isDragAndDrop
}
override fun isEnabled(): Boolean {
return !manga.isBlank()
}
override fun isSelectable(): Boolean {
return !manga.isBlank()
}
/**
* Filters a manga depending on a query.
*
* @param constraint the query to apply.
* @return true if the manga should be included, false otherwise.
*/
override fun filter(constraint: String): Boolean {
filter = constraint
if (manga.isBlank() && manga.title.isBlank()) {
return constraint.isEmpty()
}
val sourceName by lazy { sourceManager.getOrStub(manga.source).name }
return manga.title.contains(constraint, true) ||
(manga.author?.contains(constraint, true) ?: false) ||
(manga.artist?.contains(constraint, true) ?: false) ||
sourceName.contains(constraint, true) ||
if (constraint.contains(",")) {
val genres = manga.genre?.split(", ")
constraint.split(",").all { containsGenre(it.trim(), genres) }
} else {
containsGenre(constraint, manga.genre?.split(", "))
}
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
if (tag.trim().isEmpty()) return true
context ?: return false
val seriesType by lazy { manga.seriesType(context, sourceManager) }
return if (tag.startsWith("-")) {
val realTag = tag.substringAfter("-")
genres?.find {
it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true)
} == null
} else {
genres?.find {
it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true)
} != null
}
}
override fun equals(other: Any?): Boolean {
if (other is LibraryItem) {
return manga.id == other.manga.id && manga.category == other.manga.category
}
return false
}
override fun hashCode(): Int {
return 31 * manga.id!!.hashCode() + header!!.hashCode()
} }
companion object { companion object {

View file

@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.util.lang.highlightText
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.coil.loadManga import yokai.presentation.core.util.coil.loadManga
import yokai.util.lang.getString import yokai.util.lang.getString
/** /**
@ -39,26 +39,23 @@ class LibraryListHolder(
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root) setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.title.isVisible = true binding.title.isVisible = true
binding.constraintLayout.minHeight = 56.dpToPx binding.constraintLayout.minHeight = 56.dpToPx
if (item is LibraryPlaceholderItem) { if (item.manga.isBlank()) {
binding.constraintLayout.minHeight = 0 binding.constraintLayout.minHeight = 0
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = ViewGroup.MarginLayoutParams.WRAP_CONTENT height = ViewGroup.MarginLayoutParams.WRAP_CONTENT
} }
when (item.type) { if (item.manga.status == -1) {
is LibraryPlaceholderItem.Type.Blank -> { binding.title.text = null
binding.title.isVisible = false
} else {
binding.title.text = itemView.context.getString( binding.title.text = itemView.context.getString(
if (adapter.hasActiveFilters && item.type.mangaCount >= 1) { if (adapter.hasActiveFilters && item.manga.realMangaCount >= 1) {
MR.strings.no_matches_for_filters_short MR.strings.no_matches_for_filters_short
} else { } else {
MR.strings.category_is_empty MR.strings.category_is_empty
}, },
) )
} }
is LibraryPlaceholderItem.Type.Hidden -> {
binding.title.text = null
binding.title.isVisible = false
}
}
binding.title.textAlignment = View.TEXT_ALIGNMENT_CENTER binding.title.textAlignment = View.TEXT_ALIGNMENT_CENTER
binding.card.isVisible = false binding.card.isVisible = false
binding.unreadDownloadBadge.badgeView.isVisible = false binding.unreadDownloadBadge.badgeView.isVisible = false
@ -66,9 +63,6 @@ class LibraryListHolder(
binding.subtitle.isVisible = false binding.subtitle.isVisible = false
return return
} }
if (item !is LibraryMangaItem) error("${item::class.qualifiedName} is not a valid item")
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = 52.dpToPx height = 52.dpToPx
} }
@ -77,16 +71,16 @@ class LibraryListHolder(
binding.title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START binding.title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
// Update the binding.title of the manga. // Update the binding.title of the manga.
binding.title.text = item.manga.manga.title.highlightText(item.filter, color) binding.title.text = item.manga.title.highlightText(item.filter, color)
setUnreadBadge(binding.unreadDownloadBadge.badgeView, item) setUnreadBadge(binding.unreadDownloadBadge.badgeView, item)
val authorArtist = val authorArtist =
if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) { if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
item.manga.manga.author?.trim() ?: "" item.manga.author?.trim() ?: ""
} else { } else {
listOfNotNull( listOfNotNull(
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() }, item.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() }, item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
).joinToString(", ") ).joinToString(", ")
} }
@ -101,7 +95,7 @@ class LibraryListHolder(
// Update the cover. // Update the cover.
binding.coverThumbnail.dispose() binding.coverThumbnail.dispose()
binding.coverThumbnail.loadManga(item.manga.manga) binding.coverThumbnail.loadManga(item.manga)
} }
override fun onActionStateChanged(position: Int, actionState: Int) { override fun onActionStateChanged(position: Int, actionState: Int) {

View file

@ -1,180 +0,0 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.seriesType
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
class LibraryMangaItem(
val manga: LibraryManga,
header: LibraryHeaderItem,
context: Context?,
) : LibraryItem(header, context) {
var downloadCount = -1
var unreadType = 2
var sourceLanguage: String? = null
override fun getLayoutRes(): Int {
return if (libraryLayout == LAYOUT_LIST) {
R.layout.manga_list_item
} else {
R.layout.manga_grid_item
}
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
val listHolder by lazy { LibraryListHolder(view, adapter as LibraryCategoryAdapter) }
val parent = adapter.recyclerView
if (parent !is AutofitRecyclerView) return listHolder
val libraryLayout = libraryLayout
val isFixedSize = uniformSize
if (libraryLayout == LAYOUT_LIST) { return listHolder }
view.apply {
val isStaggered = parent.layoutManager is StaggeredGridLayoutManager
val binding = MangaGridItemBinding.bind(this)
binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID
if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) {
binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID
binding.card.setCardForegroundColor(
ContextCompat.getColorStateList(
context,
R.color.library_comfortable_grid_foreground,
),
)
}
if (isFixedSize) {
binding.constraintLayout.layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
binding.coverThumbnail.maxHeight = Int.MAX_VALUE
binding.coverThumbnail.minimumHeight = 0
binding.constraintLayout.minHeight = 0
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
binding.coverThumbnail.adjustViewBounds = false
binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
dimensionRatio = "2:3"
}
}
if (libraryLayout != LAYOUT_COMFORTABLE_GRID) {
binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> {
bottomMargin = (if (isStaggered) 2 else 6).dpToPx
}
}
binding.setBGAndFG(libraryLayout)
}
val gridHolder = LibraryGridHolder(
view,
adapter as LibraryCategoryAdapter,
libraryLayout == LAYOUT_COMPACT_GRID,
isFixedSize,
)
if (!isFixedSize) {
gridHolder.setFreeformCoverRatio(manga.manga, parent)
}
return gridHolder
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LibraryHolder,
position: Int,
payloads: MutableList<Any?>?,
) {
if (holder is LibraryGridHolder && !holder.fixedSize) {
holder.setFreeformCoverRatio(manga.manga, adapter.recyclerView as? AutofitRecyclerView)
}
super.bindViewHolder(adapter, holder, position, payloads)
if (libraryLayout == LAYOUT_COVER_ONLY_GRID) {
holder.itemView.compatToolTipText = manga.manga.title
}
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return header.category.isDragAndDrop
}
override fun isEnabled(): Boolean {
return true
}
override fun isSelectable(): Boolean {
return true
}
/**
* Filters a manga depending on a query.
*
* @param constraint the query to apply.
* @return true if the manga should be included, false otherwise.
*/
override fun filter(constraint: String): Boolean {
filter = constraint
if (manga.manga.title.isBlank()) {
return constraint.isEmpty()
}
val sourceName by lazy { sourceManager.getOrStub(manga.manga.source).name }
return manga.manga.title.contains(constraint, true) ||
(manga.manga.author?.contains(constraint, true) ?: false) ||
(manga.manga.artist?.contains(constraint, true) ?: false) ||
sourceName.contains(constraint, true) ||
if (constraint.contains(",")) {
val genres = manga.manga.genre?.split(", ")
constraint.split(",").all { containsGenre(it.trim(), genres) }
} else {
containsGenre(constraint, manga.manga.genre?.split(", "))
}
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
if (tag.trim().isEmpty()) return true
context ?: return false
val seriesType by lazy { manga.manga.seriesType(context, sourceManager) }
return if (tag.startsWith("-")) {
val realTag = tag.substringAfter("-")
genres?.find {
it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true)
} == null
} else {
genres?.find {
it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true)
} != null
}
}
override fun equals(other: Any?): Boolean {
if (other is LibraryMangaItem) {
return manga.manga.id == other.manga.manga.id && manga.category == other.manga.category
}
return false
}
override fun hashCode(): Int {
return 31 * manga.manga.id.hashCode() + header!!.hashCode()
}
}

View file

@ -1,57 +0,0 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
/**
* Placeholder item to indicate if the category is hidden or empty/filtered out.
*/
class LibraryPlaceholderItem (
val category: Int,
val type: Type,
header: LibraryHeaderItem,
context: Context?,
) : LibraryItem(header, context) {
override fun getLayoutRes(): Int = R.layout.manga_list_item
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
return LibraryListHolder(view, adapter as LibraryCategoryAdapter)
}
override fun filter(constraint: String): Boolean {
filter = constraint
if (type !is Type.Hidden || type.title.isBlank()) return constraint.isEmpty()
return type.title.contains(constraint, true)
}
override fun equals(other: Any?): Boolean {
if (other is LibraryPlaceholderItem) {
return category == other.category
}
return false
}
override fun hashCode(): Int {
return 31 * Long.MIN_VALUE.hashCode() + header!!.hashCode()
}
sealed class Type {
data class Hidden(val title: String, val hiddenItems: List<LibraryMangaItem>) : Type()
data class Blank(val mangaCount: Int) : Type()
}
companion object {
fun hidden(category: Int, header: LibraryHeaderItem, context: Context?, title: String, hiddenItems: List<LibraryMangaItem>) =
LibraryPlaceholderItem(category, Type.Hidden(title, hiddenItems), header, context)
fun blank(category: Int, header: LibraryHeaderItem, context: Context?, mangaCount: Int = 0) =
LibraryPlaceholderItem(category, Type.Blank(mangaCount), header, context)
}
}

View file

@ -1,10 +1,13 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
enum class LibrarySort( enum class LibrarySort(
val mainValue: Int, val mainValue: Int,
@ -30,11 +33,7 @@ enum class LibrarySort(
MR.strings.category, MR.strings.category,
R.drawable.ic_label_outline_24dp, R.drawable.ic_label_outline_24dp,
), ),
Random(
8,
MR.strings.random,
R.drawable.ic_shuffle_24dp,
),
; ;
val categoryValue: Char val categoryValue: Char
@ -51,7 +50,6 @@ enum class LibrarySort(
LatestChapter -> "LATEST_CHAPTER" LatestChapter -> "LATEST_CHAPTER"
DateFetched -> "CHAPTER_FETCH_DATE" DateFetched -> "CHAPTER_FETCH_DATE"
DateAdded -> "DATE_ADDED" DateAdded -> "DATE_ADDED"
Random -> "RANDOM"
else -> "ALPHABETICAL" else -> "ALPHABETICAL"
} }
return "$type,ASCENDING" return "$type,ASCENDING"
@ -65,9 +63,6 @@ enum class LibrarySort(
val hasInvertedSort: Boolean val hasInvertedSort: Boolean
get() = this in listOf(LastRead, DateAdded, LatestChapter, DateFetched) get() = this in listOf(LastRead, DateAdded, LatestChapter, DateFetched)
val isDirectional: Boolean
get() = this !in listOf(DragAndDrop, Random)
fun menuSheetItem(isDynamic: Boolean): MaterialMenuSheet.MenuSheetItem { fun menuSheetItem(isDynamic: Boolean): MaterialMenuSheet.MenuSheetItem {
return MaterialMenuSheet.MenuSheetItem( return MaterialMenuSheet.MenuSheetItem(
mainValue, mainValue,
@ -90,7 +85,6 @@ enum class LibrarySort(
"LATEST_CHAPTER" -> LatestChapter "LATEST_CHAPTER" -> LatestChapter
"CHAPTER_FETCH_DATE" -> DateFetched "CHAPTER_FETCH_DATE" -> DateFetched
"DATE_ADDED" -> DateAdded "DATE_ADDED" -> DateAdded
"RANDOM" -> Random
else -> Title else -> Title
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -1,93 +0,0 @@
package eu.kanade.tachiyomi.ui.library.compose
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.core.view.isGone
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
import eu.kanade.tachiyomi.ui.library.models.LibraryItem
import eu.kanade.tachiyomi.ui.main.BottomSheetController
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
import java.util.Locale
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.ui.UiPreferences
import yokai.i18n.MR
import yokai.presentation.library.LibraryContent
import yokai.presentation.theme.YokaiTheme
import yokai.util.lang.getString
class LibraryComposeController(
bundle: Bundle? = null,
val uiPreferences: UiPreferences = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
) : BaseCoroutineController<LibraryControllerBinding, LibraryComposePresenter>(bundle) ,
BottomSheetController,
RootSearchInterface,
FloatingSearchInterface {
override fun getTitle(): String? {
return view?.context?.getString(MR.strings.library)
}
override fun getSearchTitle(): String? {
val searchSuggestion by lazy { preferences.librarySearchSuggestion().get() }
return searchTitle(
if (preferences.showLibrarySearchSuggestions().get() && searchSuggestion.isNotBlank()) {
"\"$searchSuggestion\""
} else {
view?.context?.getString(MR.strings.your_library)?.lowercase(Locale.ROOT)
},
)
}
override val presenter = LibraryComposePresenter()
override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.composeView.isVisible = true
binding.swipeRefresh.isGone = true
binding.fastScroller.isGone = true
binding.composeView.setContent {
YokaiTheme {
ScreenContent()
}
}
}
@Composable
fun ScreenContent() {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
val state by presenter.state.collectAsState()
LibraryContent(
modifier = Modifier.nestedScroll(nestedScrollInterop),
items = (0..50).map { LibraryItem.Blank(it) },
columns = 3,
)
}
override fun showSheet() {
}
override fun hideSheet() {
}
override fun toggleSheet() {
}
}

View file

@ -1,16 +0,0 @@
package eu.kanade.tachiyomi.ui.library.compose
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.presenter.StateCoroutinePresenter
import eu.kanade.tachiyomi.ui.library.models.LibraryItem
typealias LibraryMap = Map<Category, List<LibraryItem>>
class LibraryComposePresenter :
StateCoroutinePresenter<LibraryComposePresenter.State, LibraryComposeController>(State()) {
data class State(
var isLoading: Boolean = true,
var library: LibraryMap = emptyMap()
)
}

View file

@ -2,14 +2,16 @@ package eu.kanade.tachiyomi.ui.library.display
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.databinding.LibraryCategoryLayoutBinding import eu.kanade.tachiyomi.databinding.LibraryCategoryLayoutBinding
import eu.kanade.tachiyomi.util.bindToPreference import eu.kanade.tachiyomi.util.bindToPreference
import eu.kanade.tachiyomi.util.lang.withSubtitle import eu.kanade.tachiyomi.util.lang.withSubtitle
import eu.kanade.tachiyomi.util.system.toInt import eu.kanade.tachiyomi.util.system.toInt
import eu.kanade.tachiyomi.widget.BaseLibraryDisplayView import eu.kanade.tachiyomi.widget.BaseLibraryDisplayView
import kotlin.math.min import kotlin.math.min
import yokai.i18n.MR
import yokai.util.lang.getString
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
BaseLibraryDisplayView<LibraryCategoryLayoutBinding>(context, attrs) { BaseLibraryDisplayView<LibraryCategoryLayoutBinding>(context, attrs) {
@ -18,7 +20,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
override fun initGeneralPreferences() { override fun initGeneralPreferences() {
with(binding) { with(binding) {
showAll.bindToPreference(preferences.showAllCategories()) { showAll.bindToPreference(preferences.showAllCategories()) {
controller?.presenter?.updateLibrary() controller?.presenter?.getLibrary()
binding.categoryShow.isEnabled = it binding.categoryShow.isEnabled = it
} }
categoryShow.isEnabled = showAll.isChecked categoryShow.isEnabled = showAll.isChecked
@ -28,7 +30,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
dynamicToBottom.text = context.getString(MR.strings.move_dynamic_to_bottom) dynamicToBottom.text = context.getString(MR.strings.move_dynamic_to_bottom)
.withSubtitle(context, MR.strings.when_grouping_by_sources_tags) .withSubtitle(context, MR.strings.when_grouping_by_sources_tags)
dynamicToBottom.bindToPreference(preferences.collapsedDynamicAtBottom()) { dynamicToBottom.bindToPreference(preferences.collapsedDynamicAtBottom()) {
controller?.presenter?.updateLibrary() controller?.presenter?.getLibrary()
} }
showEmptyCatsFiltering.bindToPreference(preferences.showEmptyCategoriesWhileFiltering()) { showEmptyCatsFiltering.bindToPreference(preferences.showEmptyCategoriesWhileFiltering()) {
controller?.presenter?.requestFilterUpdate() controller?.presenter?.requestFilterUpdate()

View file

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.library.LibraryGroup import eu.kanade.tachiyomi.ui.library.LibraryGroup
import eu.kanade.tachiyomi.ui.library.LibraryMangaItem
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
@ -37,8 +36,6 @@ import eu.kanade.tachiyomi.util.view.isCollapsed
import eu.kanade.tachiyomi.util.view.isExpanded import eu.kanade.tachiyomi.util.view.isExpanded
import eu.kanade.tachiyomi.util.view.isHidden import eu.kanade.tachiyomi.util.view.isHidden
import eu.kanade.tachiyomi.util.view.setText import eu.kanade.tachiyomi.util.view.setText
import kotlin.math.max
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
@ -51,6 +48,8 @@ import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import kotlin.math.max
import kotlin.math.roundToInt
class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
LinearLayout(context, attrs), LinearLayout(context, attrs),
@ -369,12 +368,11 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri
suspend fun checkForManhwa(sourceManager: SourceManager) { suspend fun checkForManhwa(sourceManager: SourceManager) {
if (checked) return if (checked) return
withIOContext { withIOContext {
val libraryManga = controller?.presenter?.currentLibraryItems ?: return@withIOContext val libraryManga = controller?.presenter?.allLibraryItems ?: return@withIOContext
checked = true checked = true
var types = mutableSetOf<StringResource>() var types = mutableSetOf<StringResource>()
libraryManga.forEach { libraryManga.forEach {
if (it !is LibraryMangaItem) return@forEach when (it.manga.seriesType(sourceManager = sourceManager)) {
when (it.manga.manga.seriesType(sourceManager = sourceManager)) {
Manga.TYPE_MANHWA, Manga.TYPE_WEBTOON -> types.add(MR.strings.manhwa) Manga.TYPE_MANHWA, Manga.TYPE_WEBTOON -> types.add(MR.strings.manhwa)
Manga.TYPE_MANHUA -> types.add(MR.strings.manhua) Manga.TYPE_MANHUA -> types.add(MR.strings.manhua)
Manga.TYPE_COMIC -> types.add(MR.strings.comic) Manga.TYPE_COMIC -> types.add(MR.strings.comic)
@ -639,7 +637,7 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri
SeriesType('m', MR.strings.series_type), SeriesType('m', MR.strings.series_type),
Bookmarked('b', MR.strings.bookmarked), Bookmarked('b', MR.strings.bookmarked),
Tracked('t', MR.strings.tracking), Tracked('t', MR.strings.tracking),
ContentType('s', MR.strings.content_type) ContentType('s', MR.strings.content_type);
; ;
companion object { companion object {

View file

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.ui.library.models
import eu.kanade.tachiyomi.data.database.models.LibraryManga
sealed interface LibraryItem {
data class Blank(val mangaCount: Int = 0) : LibraryItem
data class Hidden(val title: String, val hiddenItems: List<LibraryItem>) : LibraryItem
data class Manga(
val libraryManga: LibraryManga,
val isLocal: Boolean = false,
val downloadCount: Long = -1,
val unreadCount: Long = -1,
val language: String = "",
) : LibraryItem
}

View file

@ -86,7 +86,6 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.BaseLegacyController import eu.kanade.tachiyomi.ui.base.controller.BaseLegacyController
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.library.compose.LibraryComposeController
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.more.AboutController import eu.kanade.tachiyomi.ui.more.AboutController
import eu.kanade.tachiyomi.ui.more.OverflowDialog import eu.kanade.tachiyomi.ui.more.OverflowDialog
@ -118,7 +117,6 @@ import eu.kanade.tachiyomi.util.system.prepareSideNavContext
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.tryTakePersistableUriPermission import eu.kanade.tachiyomi.util.system.tryTakePersistableUriPermission
import eu.kanade.tachiyomi.util.system.withUIContext
import eu.kanade.tachiyomi.util.view.BackHandlerControllerInterface import eu.kanade.tachiyomi.util.view.BackHandlerControllerInterface
import eu.kanade.tachiyomi.util.view.backgroundColor import eu.kanade.tachiyomi.util.view.backgroundColor
import eu.kanade.tachiyomi.util.view.blurBehindWindow import eu.kanade.tachiyomi.util.view.blurBehindWindow
@ -138,10 +136,12 @@ import kotlin.collections.set
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToLong import kotlin.math.roundToLong
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.core.migration.Migrator import yokai.core.migration.Migrator
import yokai.domain.base.BasePreferences import yokai.domain.base.BasePreferences
@ -198,7 +198,6 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
dimenW to dimenH dimenW to dimenH
} }
@Deprecated("Create contract directly from Composable")
private val requestNotificationPermissionLauncher = private val requestNotificationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) { if (!isGranted) {
@ -539,7 +538,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
if (currentRoot?.tag()?.toIntOrNull() != id) { if (currentRoot?.tag()?.toIntOrNull() != id) {
setRoot( setRoot(
when (id) { when (id) {
R.id.nav_library -> if (basePreferences.composeLibrary().get()) LibraryComposeController() else LibraryController() R.id.nav_library -> LibraryController()
R.id.nav_recents -> RecentsController() R.id.nav_recents -> RecentsController()
else -> BrowseController() else -> BrowseController()
}, },
@ -1002,7 +1001,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
} }
private fun checkForAppUpdates() { private fun checkForAppUpdates() {
if (isUpdaterEnabled && router.backstack.lastOrNull()?.controller !is AboutController) { if (isUpdaterEnabled) {
lifecycleScope.launchIO { lifecycleScope.launchIO {
try { try {
val result = updateChecker.checkForUpdate(this@MainActivity) val result = updateChecker.checkForUpdate(this@MainActivity)
@ -1012,7 +1011,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
val isBeta = result.release.preRelease == true val isBeta = result.release.preRelease == true
// Create confirmation window // Create confirmation window
withUIContext { withContext(Dispatchers.Main) {
showNotificationPermissionPrompt() showNotificationPermissionPrompt()
AppUpdateNotifier.releasePageUrl = result.release.releaseLink AppUpdateNotifier.releasePageUrl = result.release.releaseLink
AboutController.NewUpdateDialogController(body, url, isBeta).showDialog(router) AboutController.NewUpdateDialogController(body, url, isBeta).showDialog(router)
@ -1038,7 +1037,6 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
@SuppressLint("MissingSuperCall") @SuppressLint("MissingSuperCall")
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
splashState.ready = true
if (!handleIntentAction(intent)) { if (!handleIntentAction(intent)) {
super.onNewIntent(intent) super.onNewIntent(intent)
} }
@ -1055,13 +1053,13 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
} }
when (intent.action) { when (intent.action) {
SHORTCUT_LIBRARY -> nav.selectedItemId = R.id.nav_library SHORTCUT_LIBRARY -> nav.selectedItemId = R.id.nav_library
SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ, Constants.SHORTCUT_RECENTS -> { SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ, SHORTCUT_RECENTS -> {
if (nav.selectedItemId != R.id.nav_recents) { if (nav.selectedItemId != R.id.nav_recents) {
nav.selectedItemId = R.id.nav_recents nav.selectedItemId = R.id.nav_recents
} else { } else {
router.popToRoot() router.popToRoot()
} }
if (intent.action == Constants.SHORTCUT_RECENTS) return true if (intent.action == SHORTCUT_RECENTS) return true
nav.post { nav.post {
val controller = val controller =
router.backstack.firstOrNull()?.controller as? RecentsController router.backstack.firstOrNull()?.controller as? RecentsController
@ -1094,11 +1092,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
SHORTCUT_UPDATE_NOTES -> { SHORTCUT_UPDATE_NOTES -> {
val extras = intent.extras ?: return false val extras = intent.extras ?: return false
if (router.backstack.isEmpty()) nav.selectedItemId = R.id.nav_library if (router.backstack.isEmpty()) nav.selectedItemId = R.id.nav_library
if ( if (router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController) {
router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController &&
// FIXME: Show Compose version of NewUpdateDialog for AboutController
router.backstack.lastOrNull()?.controller !is AboutController
) {
AboutController.NewUpdateDialogController(extras).showDialog(router) AboutController.NewUpdateDialogController(extras).showDialog(router)
} }
} }
@ -1128,6 +1122,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
else -> return false else -> return false
} }
splashState.ready = true
return true return true
} }
@ -1614,12 +1609,20 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
private const val SWIPE_THRESHOLD = 100 private const val SWIPE_THRESHOLD = 100
private const val SWIPE_VELOCITY_THRESHOLD = 100 private const val SWIPE_VELOCITY_THRESHOLD = 100
const val MAIN_ACTIVITY = Constants.MAIN_ACTIVITY
// Shortcut actions // Shortcut actions
const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
@Deprecated("Use the one from Constants object instead")
const val SHORTCUT_RECENTS = Constants.SHORTCUT_RECENTS
const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ" const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
const val SHORTCUT_BROWSE = "eu.kanade.tachiyomi.SHOW_BROWSE" const val SHORTCUT_BROWSE = "eu.kanade.tachiyomi.SHOW_BROWSE"
const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS" const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS"
@Deprecated("Use the one from Constants object instead")
const val SHORTCUT_MANGA = Constants.SHORTCUT_MANGA
@Deprecated("Use the one from Constants object instead")
const val SHORTCUT_MANGA_BACK = Constants.SHORTCUT_MANGA_BACK
const val SHORTCUT_UPDATE_NOTES = "eu.kanade.tachiyomi.SHOW_UPDATE_NOTES" const val SHORTCUT_UPDATE_NOTES = "eu.kanade.tachiyomi.SHOW_UPDATE_NOTES"
const val SHORTCUT_SOURCE = "eu.kanade.tachiyomi.SHOW_SOURCE" const val SHORTCUT_SOURCE = "eu.kanade.tachiyomi.SHOW_SOURCE"
const val SHORTCUT_READER_SETTINGS = "eu.kanade.tachiyomi.READER_SETTINGS" const val SHORTCUT_READER_SETTINGS = "eu.kanade.tachiyomi.READER_SETTINGS"

View file

@ -97,7 +97,7 @@ class SearchActivity : MainActivity() {
} }
private fun intentShouldGoBack() = private fun intentShouldGoBack() =
intent.action in listOf(Constants.SHORTCUT_MANGA, SHORTCUT_READER_SETTINGS, SHORTCUT_BROWSE) intent.action in listOf(SHORTCUT_MANGA, SHORTCUT_READER_SETTINGS, SHORTCUT_BROWSE)
override fun syncActivityViewWithController( override fun syncActivityViewWithController(
to: Controller?, to: Controller?,

View file

@ -43,8 +43,8 @@ import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.models.cover import yokai.domain.manga.models.cover
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.coil.asTarget import yokai.presentation.core.util.coil.asTarget
import yokai.util.coil.loadManga import yokai.presentation.core.util.coil.loadManga
import yokai.util.lang.getString import yokai.util.lang.getString
import android.R as AR import android.R as AR

View file

@ -806,8 +806,6 @@ class MangaDetailsController :
} }
private fun getHeader(): MangaHeaderHolder? { private fun getHeader(): MangaHeaderHolder? {
if (!isBindingInitialized) return null
return if (isTablet) { return if (isTablet) {
binding.tabletRecycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder binding.tabletRecycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder
} else { } else {

View file

@ -499,8 +499,7 @@ class MangaDetailsPresenter(
/** Refresh Manga Info and Chapter List (not tracking) */ /** Refresh Manga Info and Chapter List (not tracking) */
fun refreshAll() { fun refreshAll() {
val isLocal by lazy { manga.isLocal() } if (view?.isNotOnline() == true && !manga.isLocal()) return
if (view?.isNotOnline(!isLocal) == true && !isLocal) return
presenterScope.launch { presenterScope.launch {
isLoading = true isLoading = true
val tasks = listOf( val tasks = listOf(

View file

@ -47,7 +47,7 @@ import eu.kanade.tachiyomi.util.system.isInNightMode
import eu.kanade.tachiyomi.util.system.isLTR import eu.kanade.tachiyomi.util.system.isLTR
import eu.kanade.tachiyomi.util.view.resetStrokeColor import eu.kanade.tachiyomi.util.view.resetStrokeColor
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.coil.loadManga import yokai.presentation.core.util.coil.loadManga
import yokai.util.lang.getString import yokai.util.lang.getString
import android.R as AR import android.R as AR

View file

@ -18,8 +18,6 @@ import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SearchOff
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.WindowInsetsCompat.Type.systemBars
@ -33,6 +31,7 @@ import com.google.android.material.datepicker.MaterialDatePicker
import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.adapters.ItemAdapter import com.mikepenz.fastadapter.adapters.ItemAdapter
import com.mikepenz.fastadapter.listeners.addClickListener import com.mikepenz.fastadapter.listeners.addClickListener
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
@ -318,7 +317,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
if (results.isEmpty()) { if (results.isEmpty()) {
setMiddleTrackView(binding.searchEmptyView.id) setMiddleTrackView(binding.searchEmptyView.id)
binding.searchEmptyView.show( binding.searchEmptyView.show(
Icons.Filled.SearchOff, R.drawable.ic_search_off_24dp,
MR.strings.no_results_found, MR.strings.no_results_found,
) )
} else { } else {
@ -339,7 +338,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
binding.trackSearchRecycler.isVisible = false binding.trackSearchRecycler.isVisible = false
searchItemAdapter.clear() searchItemAdapter.clear()
binding.searchEmptyView.show( binding.searchEmptyView.show(
Icons.Filled.SearchOff, R.drawable.ic_search_off_24dp,
error.message ?: "", error.message ?: "",
) )
} }

View file

@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.databinding.MangaListItemBinding import eu.kanade.tachiyomi.databinding.MangaListItemBinding
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
import yokai.util.coil.loadManga import yokai.presentation.core.util.coil.loadManga
class MangaHolder( class MangaHolder(
view: View, view: View,

View file

@ -27,7 +27,7 @@ import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.models.cover import yokai.domain.manga.models.cover
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.coil.loadManga import yokai.presentation.core.util.coil.loadManga
import yokai.util.lang.getString import yokai.util.lang.getString
class MigrationProcessHolder( class MigrationProcessHolder(

View file

@ -1,41 +1,175 @@
package eu.kanade.tachiyomi.ui.more package eu.kanade.tachiyomi.ui.more
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Spanned
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.compose.runtime.Composable import androidx.core.content.getSystemService
import cafe.adriel.voyager.navigator.Navigator import androidx.core.net.toUri
import cafe.adriel.voyager.transitions.CrossfadeTransition import androidx.preference.PreferenceScreen
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateNotifier
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController
import eu.kanade.tachiyomi.ui.setting.add
import eu.kanade.tachiyomi.ui.setting.onClick
import eu.kanade.tachiyomi.ui.setting.preference
import eu.kanade.tachiyomi.ui.setting.preferenceCategory
import eu.kanade.tachiyomi.ui.setting.titleMRes
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.toTimestampString
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setNegativeButton import eu.kanade.tachiyomi.util.view.setNegativeButton
import eu.kanade.tachiyomi.util.view.setPositiveButton import eu.kanade.tachiyomi.util.view.setPositiveButton
import eu.kanade.tachiyomi.util.view.setTitle import eu.kanade.tachiyomi.util.view.setTitle
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import yokai.i18n.MR import yokai.i18n.MR
import yokai.presentation.settings.screen.about.AboutScreen import yokai.util.lang.getString
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import android.R as AR import android.R as AR
class AboutController : BaseComposeController() { class AboutController : SettingsLegacyController() {
@Composable /**
override fun ScreenContent() { * Checks for new releases
Navigator( */
screen = AboutScreen(), private val updateChecker by lazy { AppUpdateChecker() }
content = {
CrossfadeTransition(navigator = it) private val dateFormat: DateFormat by lazy {
}, preferences.dateFormat()
) }
private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleMRes = MR.strings.about
preference {
key = "pref_whats_new"
titleMRes = MR.strings.whats_new_this_release
onClick {
val intent = Intent(
Intent.ACTION_VIEW,
if (BuildConfig.DEBUG) {
"https://github.com/null2264/yokai/commits/master"
} else {
RELEASE_URL
}.toUri(),
)
startActivity(intent)
}
}
if (isUpdaterEnabled) {
preference {
key = "pref_check_for_updates"
titleMRes = MR.strings.check_for_updates
onClick {
if (activity!!.isOnline()) {
checkVersion()
} else {
activity!!.toast(MR.strings.no_network_connection)
}
}
}
}
preference {
key = "pref_version"
titleMRes = MR.strings.version
summary = if (BuildConfig.DEBUG || BuildConfig.NIGHTLY) {
"r" + BuildConfig.COMMIT_COUNT
} else {
BuildConfig.VERSION_NAME
}
onClick {
activity?.let {
val deviceInfo = CrashLogUtil(it.localeContext).getDebugInfo()
val clipboard = it.getSystemService<ClipboardManager>()!!
val appInfo = it.getString(MR.strings.app_info)
clipboard.setPrimaryClip(ClipData.newPlainText(appInfo, deviceInfo))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
view?.snack(context.getString(MR.strings._copied_to_clipboard, appInfo))
}
}
}
}
preference {
key = "pref_build_time"
titleMRes = MR.strings.build_time
summary = getFormattedBuildTime(dateFormat)
}
preferenceCategory {
preference {
key = "pref_oss"
titleMRes = MR.strings.open_source_licenses
onClick {
router.pushController(AboutLicenseController().withFadeTransaction())
}
}
}
add(AboutLinksPreference(context))
}
/**
* Checks version and shows a user prompt if an update is available.
*/
private fun checkVersion() {
val activity = activity ?: return
activity.toast(MR.strings.searching_for_updates)
viewScope.launch {
val result = try {
updateChecker.checkForUpdate(activity, true)
} catch (error: Exception) {
withContext(Dispatchers.Main) {
activity.toast(error.message)
Logger.e(error) { "Couldn't check new update" }
}
}
when (result) {
is AppUpdateResult.NewUpdate -> {
val body = result.release.info
val url = result.release.downloadLink
val isBeta = result.release.preRelease == true
// Create confirmation window
withContext(Dispatchers.Main) {
AppUpdateNotifier.releasePageUrl = result.release.releaseLink
NewUpdateDialogController(body, url, isBeta).showDialog(router)
}
}
is AppUpdateResult.NoNewUpdate -> {
withContext(Dispatchers.Main) {
activity.toast(MR.strings.no_new_updates_available)
}
}
}
}
} }
@Deprecated("Use [DialogHostState.showNewUpdateDialog] instead", ReplaceWith("DialogHostState.showNewUpdateDialog()"))
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) { class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
constructor(body: String, url: String, isBeta: Boolean?) : this( constructor(body: String, url: String, isBeta: Boolean?) : this(
@ -47,7 +181,9 @@ class AboutController : BaseComposeController() {
) )
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val info = activity!!.parseReleaseNotes(args.getString(BODY_KEY) ?: "") val releaseBody = (args.getString(BODY_KEY) ?: "")
.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
val info = Markwon.create(activity!!).toMarkdown(releaseBody)
val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val isBeta = args.getBoolean(IS_BETA, false) val isBeta = args.getBoolean(IS_BETA, false)
@ -84,9 +220,19 @@ class AboutController : BaseComposeController() {
const val IS_BETA = "NewUpdateDialogController.is_beta" const val IS_BETA = "NewUpdateDialogController.is_beta"
} }
} }
}
fun Context.parseReleaseNotes(releaseNotes: String): Spanned { companion object {
val releaseBody = releaseNotes.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "") fun getFormattedBuildTime(dateFormat: DateFormat): String {
return Markwon.create(this).toMarkdown(releaseBody) try {
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault())
inputDf.timeZone = TimeZone.getTimeZone("UTC")
val buildTime =
inputDf.parse(BuildConfig.BUILD_TIME) ?: return BuildConfig.BUILD_TIME
return buildTime.toTimestampString(dateFormat)
} catch (e: ParseException) {
return BuildConfig.BUILD_TIME
}
}
}
} }

View file

@ -1,4 +1,4 @@
package yokai.presentation.settings.screen.about package eu.kanade.tachiyomi.ui.more
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding

View file

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.ui.more
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransition
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import soup.compose.material.motion.animation.materialSharedAxisZ
class AboutLicenseController : BaseComposeController() {
@Composable
override fun ScreenContent() {
Navigator(
screen = AboutLicenseScreen(),
content = {
CompositionLocalProvider(LocalBackPress provides router::handleBack) {
ScreenTransition(
navigator = it,
transition = { materialSharedAxisZ(forward = it.lastEvent != StackEvent.Pop) },
)
}
},
)
}
}

View file

@ -1,4 +1,4 @@
package yokai.presentation.settings.screen.about package eu.kanade.tachiyomi.ui.more
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults

View file

@ -0,0 +1,46 @@
package eu.kanade.tachiyomi.ui.more
import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.view.compatToolTipText
class AboutLinksPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Preference(context, attrs) {
init {
layoutResource = R.layout.pref_about_links
isSelectable = false
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
/*
(holder.itemView as LinearLayout).apply {
checkHeightThen {
val childCount = (this.getChildAt(0) as ViewGroup).childCount
val childCount2 = (this.getChildAt(1) as ViewGroup).childCount
val fullCount = childCount + childCount2
orientation =
if (width >= (56 * fullCount).dpToPx) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
}
}
*/
holder.findViewById(R.id.btn_website).apply {
compatToolTipText = (contentDescription.toString())
setOnClickListener { context.openInBrowser("https://mihon.app") }
}
holder.findViewById(R.id.btn_discord).apply {
compatToolTipText = (contentDescription.toString())
setOnClickListener { context.openInBrowser("https://discord.gg/mihon") }
}
holder.findViewById(R.id.btn_github).apply {
compatToolTipText = (contentDescription.toString())
setOnClickListener { context.openInBrowser("https://github.com/null2264/yokai") }
}
}
}

View file

@ -28,9 +28,9 @@ import eu.kanade.tachiyomi.util.system.roundToTwoDecimal
import eu.kanade.tachiyomi.util.view.compatToolTipText import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.util.view.withFadeTransaction
import kotlin.math.roundToInt
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import kotlin.math.roundToInt
import android.R as AR import android.R as AR
class StatsController : BaseLegacyController<StatsControllerBinding>() { class StatsController : BaseLegacyController<StatsControllerBinding>() {
@ -61,7 +61,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
} }
private fun handleGeneralStats() { private fun handleGeneralStats() {
val mangaTracks = mangaDistinct.map { it to presenter.getTracks(it.manga) } val mangaTracks = mangaDistinct.map { it to presenter.getTracks(it) }
scoresList = getScoresList(mangaTracks) scoresList = getScoresList(mangaTracks)
with(binding) { with(binding) {
viewDetailLayout.isVisible = mangaDistinct.isNotEmpty() viewDetailLayout.isVisible = mangaDistinct.isNotEmpty()
@ -76,8 +76,8 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
} }
statsTrackedMangaText.text = mangaTracks.count { it.second.isNotEmpty() }.toString() statsTrackedMangaText.text = mangaTracks.count { it.second.isNotEmpty() }.toString()
statsChaptersDownloadedText.text = mangaDistinct.sumOf { presenter.getDownloadCount(it) }.toString() statsChaptersDownloadedText.text = mangaDistinct.sumOf { presenter.getDownloadCount(it) }.toString()
statsTotalTagsText.text = mangaDistinct.flatMap { it.manga.getTags() }.distinct().count().toString() statsTotalTagsText.text = mangaDistinct.flatMap { it.getTags() }.distinct().count().toString()
statsMangaLocalText.text = mangaDistinct.count { it.manga.isLocal() }.toString() statsMangaLocalText.text = mangaDistinct.count { it.isLocal() }.toString()
statsGlobalUpdateMangaText.text = presenter.getGlobalUpdateManga().count().toString() statsGlobalUpdateMangaText.text = presenter.getGlobalUpdateManga().count().toString()
statsSourcesText.text = presenter.getSources().count().toString() statsSourcesText.text = presenter.getSources().count().toString()
statsTrackersText.text = presenter.getLoggedTrackers().count().toString() statsTrackersText.text = presenter.getLoggedTrackers().count().toString()
@ -105,7 +105,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
val pieEntries = ArrayList<PieEntry>() val pieEntries = ArrayList<PieEntry>()
val mangaStatusDistributionList = statusMap.mapNotNull { (status, color) -> val mangaStatusDistributionList = statusMap.mapNotNull { (status, color) ->
val libraryCount = mangaDistinct.count { it.manga.status == status } val libraryCount = mangaDistinct.count { it.status == status }
if (status == SManga.UNKNOWN && libraryCount == 0) return@mapNotNull null if (status == SManga.UNKNOWN && libraryCount == 0) return@mapNotNull null
pieEntries.add(PieEntry(libraryCount.toFloat(), activity!!.mapStatus(status))) pieEntries.add(PieEntry(libraryCount.toFloat(), activity!!.mapStatus(status)))
StatusDistributionItem(activity!!.mapStatus(status), libraryCount, color) StatusDistributionItem(activity!!.mapStatus(status), libraryCount, color)

View file

@ -65,19 +65,19 @@ class StatsPresenter(
val includedCategories = prefs.libraryUpdateCategories().get().map(String::toInt) val includedCategories = prefs.libraryUpdateCategories().get().map(String::toInt)
val excludedCategories = prefs.libraryUpdateCategoriesExclude().get().map(String::toInt) val excludedCategories = prefs.libraryUpdateCategoriesExclude().get().map(String::toInt)
val restrictions = prefs.libraryUpdateMangaRestriction().get() val restrictions = prefs.libraryUpdateMangaRestriction().get()
return libraryMangas.groupBy { it.manga.id } return libraryMangas.groupBy { it.id }
.filterNot { it.value.any { manga -> manga.category in excludedCategories } } .filterNot { it.value.any { manga -> manga.category in excludedCategories } }
.filter { includedCategories.isEmpty() || it.value.any { manga -> manga.category in includedCategories } } .filter { includedCategories.isEmpty() || it.value.any { manga -> manga.category in includedCategories } }
.filterNot { .filterNot {
val manga = it.value.first() val manga = it.value.first()
(MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED) || (MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED) ||
(MANGA_HAS_UNREAD in restrictions && manga.unread != 0) || (MANGA_HAS_UNREAD in restrictions && manga.unread != 0) ||
(MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead) (MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead)
} }
} }
fun getDownloadCount(manga: LibraryManga): Int { fun getDownloadCount(manga: LibraryManga): Int {
return downloadManager.getDownloadCount(manga.manga) return downloadManager.getDownloadCount(manga)
} }
fun get10PointScore(track: Track): Float? { fun get10PointScore(track: Track): Float? {

View file

@ -12,8 +12,6 @@ import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HeartBroken
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.util.Pair import androidx.core.util.Pair
@ -460,10 +458,7 @@ class StatsDetailsController :
with(binding ?: headerBinding) { with(binding ?: headerBinding) {
val hasNoData = currentStats.isNullOrEmpty() || currentStats.all { it.count == 0 } val hasNoData = currentStats.isNullOrEmpty() || currentStats.all { it.count == 0 }
if (hasNoData) { if (hasNoData) {
this@StatsDetailsController.binding.noChartData.show( this@StatsDetailsController.binding.noChartData.show(R.drawable.ic_heart_off_24dp, MR.strings.no_data_for_filters)
Icons.Filled.HeartBroken,
MR.strings.no_data_for_filters,
)
presenter.currentStats?.removeAll { it.count == 0 } presenter.currentStats?.removeAll { it.count == 0 }
handleNoChartLayout() handleNoChartLayout()
this?.statsPieChart?.isVisible = false this?.statsPieChart?.isVisible = false

View file

@ -153,7 +153,7 @@ class StatsDetailsPresenter(
private suspend fun setupSeriesType() { private suspend fun setupSeriesType() {
currentStats = ArrayList() currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.seriesType() } val libraryFormat = mangasDistinct.filterByChip().groupBy { it.seriesType() }
libraryFormat.forEach { (seriesType, mangaList) -> libraryFormat.forEach { (seriesType, mangaList) ->
currentStats?.add( currentStats?.add(
@ -173,7 +173,7 @@ class StatsDetailsPresenter(
private suspend fun setupStatus() { private suspend fun setupStatus() {
currentStats = ArrayList() currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.status } val libraryFormat = mangasDistinct.filterByChip().groupBy { it.status }
libraryFormat.forEach { (status, mangaList) -> libraryFormat.forEach { (status, mangaList) ->
currentStats?.add( currentStats?.add(
@ -263,7 +263,7 @@ class StatsDetailsPresenter(
private suspend fun setupTrackers() { private suspend fun setupTrackers() {
currentStats = ArrayList() currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip() val libraryFormat = mangasDistinct.filterByChip()
.map { it to getTracks(it.manga).ifEmpty { listOf(null) } } .map { it to getTracks(it).ifEmpty { listOf(null) } }
.flatMap { it.second.map { track -> it.first to track } } .flatMap { it.second.map { track -> it.first to track } }
val loggedServices = trackManager.services.filter { it.isLogged } val loggedServices = trackManager.services.filter { it.isLogged }
@ -292,7 +292,7 @@ class StatsDetailsPresenter(
private suspend fun setupSources() { private suspend fun setupSources() {
currentStats = ArrayList() currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.source } val libraryFormat = mangasDistinct.filterByChip().groupBy { it.source }
libraryFormat.forEach { (sourceId, mangaList) -> libraryFormat.forEach { (sourceId, mangaList) ->
val source = sourceManager.getOrStub(sourceId) val source = sourceManager.getOrStub(sourceId)
@ -339,10 +339,10 @@ class StatsDetailsPresenter(
private suspend fun setupTags() { private suspend fun setupTags() {
currentStats = ArrayList() currentStats = ArrayList()
val mangaFiltered = mangasDistinct.filterByChip() val mangaFiltered = mangasDistinct.filterByChip()
val tags = mangaFiltered.flatMap { it.manga.getTags() }.distinctBy { it.uppercase() } val tags = mangaFiltered.flatMap { it.getTags() }.distinctBy { it.uppercase() }
val libraryFormat = tags.map { tag -> val libraryFormat = tags.map { tag ->
tag to mangaFiltered.filter { tag to mangaFiltered.filter {
it.manga.getTags().any { mangaTag -> mangaTag.equals(tag, true) } it.getTags().any { mangaTag -> mangaTag.equals(tag, true) }
} }
} }
@ -433,7 +433,7 @@ class StatsDetailsPresenter(
this this
} else { } else {
filter { manga -> filter { manga ->
context.mapSeriesType(manga.manga.seriesType()) in selectedSeriesType context.mapSeriesType(manga.seriesType()) in selectedSeriesType
} }
} }
} }
@ -443,7 +443,7 @@ class StatsDetailsPresenter(
this this
} else { } else {
filter { manga -> filter { manga ->
context.mapStatus(manga.manga.status) in selectedStatus context.mapStatus(manga.status) in selectedStatus
} }
} }
} }
@ -463,7 +463,7 @@ class StatsDetailsPresenter(
this this
} else { } else {
filter { manga -> filter { manga ->
manga.manga.source in selectedSource.map { it.id } manga.source in selectedSource.map { it.id }
} }
} }
} }
@ -504,10 +504,10 @@ class StatsDetailsPresenter(
* Get language name of a manga * Get language name of a manga
*/ */
private fun LibraryManga.getLanguage(): String { private fun LibraryManga.getLanguage(): String {
val code = if (manga.isLocal()) { val code = if (isLocal()) {
LocalSource.getMangaLang(this.manga) LocalSource.getMangaLang(this)
} else { } else {
sourceManager.get(manga.source)?.lang sourceManager.get(source)?.lang
} ?: return context.getString(MR.strings.unknown) } ?: return context.getString(MR.strings.unknown)
return LocaleHelper.getLocalizedDisplayName(code) return LocaleHelper.getLocalizedDisplayName(code)
} }
@ -516,7 +516,7 @@ class StatsDetailsPresenter(
* Get mean score rounded to two decimal of a list of manga * Get mean score rounded to two decimal of a list of manga
*/ */
private suspend fun List<LibraryManga>.getMeanScoreRounded(): Double? { private suspend fun List<LibraryManga>.getMeanScoreRounded(): Double? {
val mangaTracks = this.map { it to getTracks(it.manga) } val mangaTracks = this.map { it to getTracks(it) }
val scoresList = mangaTracks.filter { it.second.isNotEmpty() } val scoresList = mangaTracks.filter { it.second.isNotEmpty() }
.mapNotNull { it.second.getMeanScoreByTracker() } .mapNotNull { it.second.getMeanScoreByTracker() }
return if (scoresList.isEmpty()) null else scoresList.average().roundToTwoDecimal() return if (scoresList.isEmpty()) null else scoresList.average().roundToTwoDecimal()
@ -526,7 +526,7 @@ class StatsDetailsPresenter(
* Get mean score rounded to int of a single manga * Get mean score rounded to int of a single manga
*/ */
private suspend fun LibraryManga.getMeanScoreToInt(): Int? { private suspend fun LibraryManga.getMeanScoreToInt(): Int? {
val mangaTracks = getTracks(this.manga) val mangaTracks = getTracks(this)
val scoresList = mangaTracks.filter { it.score > 0 } val scoresList = mangaTracks.filter { it.score > 0 }
.mapNotNull { it.get10PointScore() } .mapNotNull { it.get10PointScore() }
return if (scoresList.isEmpty()) null else scoresList.average().roundToInt().coerceIn(1..10) return if (scoresList.isEmpty()) null else scoresList.average().roundToInt().coerceIn(1..10)
@ -550,8 +550,8 @@ class StatsDetailsPresenter(
} }
private suspend fun LibraryManga.getStartYear(): Int? { private suspend fun LibraryManga.getStartYear(): Int? {
if (getChapter.awaitAll(manga.id!!, false).any { it.read }) { if (getChapter.awaitAll(id!!, false).any { it.read }) {
val chapters = getHistory.awaitAllByMangaId(manga.id!!).filter { it.last_read > 0 } val chapters = getHistory.awaitAllByMangaId(id!!).filter { it.last_read > 0 }
val date = chapters.minOfOrNull { it.last_read } ?: return null val date = chapters.minOfOrNull { it.last_read } ?: return null
val cal = Calendar.getInstance().apply { timeInMillis = date } val cal = Calendar.getInstance().apply { timeInMillis = date }
return if (date <= 0L) null else cal.get(Calendar.YEAR) return if (date <= 0L) null else cal.get(Calendar.YEAR)
@ -564,7 +564,7 @@ class StatsDetailsPresenter(
} }
private fun getEnabledSources(): List<Source> { private fun getEnabledSources(): List<Source> {
return mangasDistinct.mapNotNull { sourceManager.get(it.manga.source) } return mangasDistinct.mapNotNull { sourceManager.get(it.source) }
.distinct().sortedBy { it.name } .distinct().sortedBy { it.name }
} }
@ -589,7 +589,7 @@ class StatsDetailsPresenter(
} }
private suspend fun List<LibraryManga>.getReadDuration(): Long { private suspend fun List<LibraryManga>.getReadDuration(): Long {
return sumOf { manga -> getHistory.awaitAllByMangaId(manga.manga.id!!).sumOf { it.time_read } } return sumOf { manga -> getHistory.awaitAllByMangaId(manga.id!!).sumOf { it.time_read } }
} }
/** /**

View file

@ -148,14 +148,6 @@ import eu.kanade.tachiyomi.util.view.setMessage
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.doOnEnd import eu.kanade.tachiyomi.widget.doOnEnd
import eu.kanade.tachiyomi.widget.doOnStart import eu.kanade.tachiyomi.widget.doOnStart
import java.io.ByteArrayOutputStream
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Collections
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -175,6 +167,13 @@ import yokai.domain.ui.settings.ReaderPreferences
import yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour import yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import java.io.ByteArrayOutputStream
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
import android.R as AR import android.R as AR
/** /**
@ -513,6 +512,7 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
} }
} }
} }
viewModel.onSaveInstanceState()
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
@ -1304,13 +1304,13 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
} }
override fun onPause() { override fun onPause() {
viewModel.flushReadTimer() viewModel.saveCurrentChapterReadingProgress()
super.onPause() super.onPause()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.restartReadTimer() viewModel.setReadStartTime()
} }
fun reloadChapters(doublePages: Boolean, force: Boolean = false) { fun reloadChapters(doublePages: Boolean, force: Boolean = false) {
@ -1655,7 +1655,7 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
} }
private fun showSetCoverPrompt(page: ReaderPage) { private fun showSetCoverPrompt(page: ReaderPage) {
if (page.status !is Page.State.Ready) return if (page.status != Page.State.READY) return
materialAlertDialog() materialAlertDialog()
.setMessage(MR.strings.use_image_as_cover) .setMessage(MR.strings.use_image_as_cover)

View file

@ -12,7 +12,6 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.create
import eu.kanade.tachiyomi.data.database.models.defaultReaderType import eu.kanade.tachiyomi.data.database.models.defaultReaderType
import eu.kanade.tachiyomi.data.database.models.orientationType import eu.kanade.tachiyomi.data.database.models.orientationType
import eu.kanade.tachiyomi.data.database.models.readingModeType import eu.kanade.tachiyomi.data.database.models.readingModeType
@ -55,7 +54,9 @@ import eu.kanade.tachiyomi.util.system.withIOContext
import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.system.withUIContext
import java.util.Date import java.util.Date
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -67,7 +68,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -80,7 +80,6 @@ import yokai.domain.chapter.models.ChapterUpdate
import yokai.domain.download.DownloadPreferences import yokai.domain.download.DownloadPreferences
import yokai.domain.history.interactor.GetHistory import yokai.domain.history.interactor.GetHistory
import yokai.domain.history.interactor.UpsertHistory import yokai.domain.history.interactor.UpsertHistory
import yokai.domain.library.LibraryPreferences
import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.interactor.InsertManga import yokai.domain.manga.interactor.InsertManga
import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.interactor.UpdateManga
@ -102,7 +101,6 @@ class ReaderViewModel(
private val chapterFilter: ChapterFilter = Injekt.get(), private val chapterFilter: ChapterFilter = Injekt.get(),
private val storageManager: StorageManager = Injekt.get(), private val storageManager: StorageManager = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
) : ViewModel() { ) : ViewModel() {
private val getCategories: GetCategories by injectLazy() private val getCategories: GetCategories by injectLazy()
private val getChapter: GetChapter by injectLazy() private val getChapter: GetChapter by injectLazy()
@ -157,15 +155,12 @@ class ReaderViewModel(
private var finished = false private var finished = false
private var chapterToDownload: Download? = null private var chapterToDownload: Download? = null
private val unfilteredChapterList by lazy {
val manga = manga!!
runBlocking { getChapter.awaitAll(manga, filterScanlators = false) }
}
private lateinit var chapterList: List<ReaderChapter> private lateinit var chapterList: List<ReaderChapter>
private var chapterItems = emptyList<ReaderChapterItem>() private var chapterItems = emptyList<ReaderChapterItem>()
private var scope = CoroutineScope(Job() + Dispatchers.Default)
private var hasTrackers: Boolean = false private var hasTrackers: Boolean = false
private suspend fun checkTrackers(manga: Manga) = getTrack.awaitAllByMangaId(manga.id).isNotEmpty() private suspend fun checkTrackers(manga: Manga) = getTrack.awaitAllByMangaId(manga.id).isNotEmpty()
@ -196,12 +191,24 @@ class ReaderViewModel(
val currentChapters = state.value.viewerChapters val currentChapters = state.value.viewerChapters
if (currentChapters != null) { if (currentChapters != null) {
currentChapters.unref() currentChapters.unref()
saveReadingProgress(currentChapters.currChapter)
chapterToDownload?.let { chapterToDownload?.let {
downloadManager.addDownloadsToStartOfQueue(listOf(it)) downloadManager.addDownloadsToStartOfQueue(listOf(it))
} }
} }
} }
/**
* Called when the activity is saved and not changing configurations. It updates the database
* to persist the current progress of the active chapter.
*/
fun onSaveInstanceState() {
val currentChapter = getCurrentChapter() ?: return
viewModelScope.launchNonCancellableIO {
saveChapterProgress(currentChapter)
}
}
/** /**
* Whether this presenter is initialized yet. * Whether this presenter is initialized yet.
*/ */
@ -300,7 +307,6 @@ class ReaderViewModel(
return delegatedSource.pageNumber(url)?.minus(1) return delegatedSource.pageNumber(url)?.minus(1)
} }
// FIXME: Unused at the moment, handles J2K's delegated deep link, refactor or remove later
suspend fun loadChapterURL(url: Uri) { suspend fun loadChapterURL(url: Uri) {
val host = url.host ?: return val host = url.host ?: return
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@ -308,7 +314,9 @@ class ReaderViewModel(
context.getString(MR.strings.source_not_installed), context.getString(MR.strings.source_not_installed),
) )
val chapterUrl = delegatedSource.chapterUrl(url) val chapterUrl = delegatedSource.chapterUrl(url)
val sourceId = delegatedSource.delegate.id val sourceId = delegatedSource.delegate?.id ?: error(
context.getString(MR.strings.source_not_installed),
)
if (chapterUrl != null) { if (chapterUrl != null) {
val dbChapter = getChapter.awaitAllByUrl(chapterUrl, false).find { val dbChapter = getChapter.awaitAllByUrl(chapterUrl, false).find {
val source = getManga.awaitById(it.manga_id!!)?.source ?: return@find false val source = getManga.awaitById(it.manga_id!!)?.source ?: return@find false
@ -326,9 +334,7 @@ class ReaderViewModel(
} }
val info = delegatedSource.fetchMangaFromChapterUrl(url) val info = delegatedSource.fetchMangaFromChapterUrl(url)
if (info != null) { if (info != null) {
val (sChapter, sManga, chapters) = info val (chapter, manga, chapters) = info
val manga = Manga.create(sManga.url, sManga.title, sourceId).apply { copyFrom(sManga) }
val chapter = Chapter.create().apply { copyFrom(sChapter) }
val id = insertManga.await(manga) val id = insertManga.await(manga)
manga.id = id ?: manga.id manga.id = id ?: manga.id
chapter.manga_id = manga.id chapter.manga_id = manga.id
@ -367,15 +373,12 @@ class ReaderViewModel(
* Called when the user changed to the given [chapter] when changing pages from the viewer. * Called when the user changed to the given [chapter] when changing pages from the viewer.
* It's used only to set this chapter as active. * It's used only to set this chapter as active.
*/ */
private fun loadNewChapter(chapter: ReaderChapter) { private suspend fun loadNewChapter(chapter: ReaderChapter) {
val loader = loader ?: return val loader = loader ?: return
viewModelScope.launchIO {
Logger.d { "Loading ${chapter.chapter.url}" } Logger.d { "Loading ${chapter.chapter.url}" }
flushReadTimer() withIOContext {
restartReadTimer()
try { try {
loadChapter(loader, chapter) loadChapter(loader, chapter)
} catch (e: Throwable) { } catch (e: Throwable) {
@ -506,15 +509,28 @@ class ReaderViewModel(
val selectedChapter = page.chapter val selectedChapter = page.chapter
// Save last page read and mark as read if needed // Save last page read and mark as read if needed
viewModelScope.launchNonCancellableIO { selectedChapter.chapter.last_page_read = page.index
saveChapterProgress(selectedChapter, page, hasExtraPage) selectedChapter.chapter.pages_left =
(selectedChapter.pages?.size ?: page.index) - page.index
val shouldTrack = !preferences.incognitoMode().get() || hasTrackers
if (shouldTrack &&
// For double pages, check if the second to last page is doubled up
(
(selectedChapter.pages?.lastIndex == page.index && page.firstHalf != true) ||
(hasExtraPage && selectedChapter.pages?.lastIndex?.minus(1) == page.index)
)
) {
selectedChapter.chapter.read = true
updateTrackChapterAfterReading(selectedChapter)
deleteChapterIfNeeded(selectedChapter)
} }
if (selectedChapter != currentChapters.currChapter) { if (selectedChapter != currentChapters.currChapter) {
Logger.d { "Setting ${selectedChapter.chapter.url} as active" } Logger.d { "Setting ${selectedChapter.chapter.url} as active" }
loadNewChapter(selectedChapter) saveReadingProgress(currentChapters.currChapter)
setReadStartTime()
scope.launch { loadNewChapter(selectedChapter) }
} }
val pages = page.chapter.pages ?: return val pages = page.chapter.pages ?: return
val inDownloadRange = page.number.toDouble() / pages.size > 0.2 val inDownloadRange = page.number.toDouble() / pages.size > 0.2
if (inDownloadRange) { if (inDownloadRange) {
@ -602,28 +618,28 @@ class ReaderViewModel(
} }
} }
/**
* Called when reader chapter is changed in reader or when activity is paused.
*/
private fun saveReadingProgress(readerChapter: ReaderChapter) {
viewModelScope.launchNonCancellableIO {
saveChapterProgress(readerChapter)
saveChapterHistory(readerChapter)
}
}
fun saveCurrentChapterReadingProgress() = getCurrentChapter()?.let { saveReadingProgress(it) }
/** /**
* Saves this [readerChapter]'s progress (last read page and whether it's read). * Saves this [readerChapter]'s progress (last read page and whether it's read).
* If incognito mode isn't on or has at least 1 tracker * If incognito mode isn't on or has at least 1 tracker
*/ */
private suspend fun saveChapterProgress(readerChapter: ReaderChapter, page: ReaderPage, hasExtraPage: Boolean) { private suspend fun saveChapterProgress(readerChapter: ReaderChapter) {
readerChapter.requestedPage = readerChapter.chapter.last_page_read readerChapter.requestedPage = readerChapter.chapter.last_page_read
getChapter.awaitById(readerChapter.chapter.id!!)?.let { dbChapter -> getChapter.awaitById(readerChapter.chapter.id!!)?.let { dbChapter ->
readerChapter.chapter.bookmark = dbChapter.bookmark readerChapter.chapter.bookmark = dbChapter.bookmark
} }
if (!preferences.incognitoMode().get() || hasTrackers) {
val shouldTrack = !preferences.incognitoMode().get() || hasTrackers
if (shouldTrack && page.status !is Page.State.Error) {
readerChapter.chapter.last_page_read = page.index
readerChapter.chapter.pages_left = (readerChapter.pages?.size ?: page.index) - page.index
// For double pages, check if the second to last page is doubled up
if (
(readerChapter.pages?.lastIndex == page.index && page.firstHalf != true) ||
(hasExtraPage && readerChapter.pages?.lastIndex?.minus(1) == page.index)
) {
onChapterReadComplete(readerChapter)
}
updateChapter.await( updateChapter.await(
ChapterUpdate( ChapterUpdate(
id = readerChapter.chapter.id!!, id = readerChapter.chapter.id!!,
@ -636,57 +652,25 @@ class ReaderViewModel(
} }
} }
private suspend fun onChapterReadComplete(readerChapter: ReaderChapter) {
readerChapter.chapter.read = true
updateTrackChapterAfterReading(readerChapter)
deleteChapterIfNeeded(readerChapter)
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get()
.contains(LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_EXISTING)
if (!markDuplicateAsRead) return
val duplicateUnreadChapters = unfilteredChapterList
.mapNotNull { chapter ->
if (
!chapter.read &&
chapter.isRecognizedNumber &&
chapter.chapter_number == readerChapter.chapter.chapter_number
) {
ChapterUpdate(id = chapter.id!!, read = true)
} else {
null
}
}
updateChapter.awaitAll(duplicateUnreadChapters)
}
fun restartReadTimer() {
chapterReadStartTime = Date().time
}
fun flushReadTimer() {
getCurrentChapter()?.let {
viewModelScope.launchNonCancellableIO {
saveChapterHistory(it)
}
}
}
/** /**
* Saves this [readerChapter] last read history. * Saves this [readerChapter] last read history.
*/ */
private suspend fun saveChapterHistory(readerChapter: ReaderChapter) { private suspend fun saveChapterHistory(readerChapter: ReaderChapter) {
if (preferences.incognitoMode().get()) return if (!preferences.incognitoMode().get()) {
val readAt = Date().time
val endTime = Date().time val sessionReadDuration = chapterReadStartTime?.let { readAt - it } ?: 0
val sessionReadDuration = chapterReadStartTime?.let { endTime - it } ?: 0
val history = History.create(readerChapter.chapter).apply { val history = History.create(readerChapter.chapter).apply {
last_read = endTime last_read = readAt
time_read = sessionReadDuration time_read = sessionReadDuration
} }
upsertHistory.await(history) upsertHistory.await(history)
chapterReadStartTime = null chapterReadStartTime = null
} }
}
fun setReadStartTime() {
chapterReadStartTime = Date().time
}
/** /**
* Called from the activity to preload the given [chapter]. * Called from the activity to preload the given [chapter].
@ -860,7 +844,7 @@ class ReaderViewModel(
* There's also a notification to allow sharing the image somewhere else or deleting it. * There's also a notification to allow sharing the image somewhere else or deleting it.
*/ */
fun saveImage(page: ReaderPage) { fun saveImage(page: ReaderPage) {
if (page.status !is Page.State.Ready) return if (page.status != Page.State.READY) return
val manga = manga ?: return val manga = manga ?: return
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@ -890,9 +874,9 @@ class ReaderViewModel(
} }
fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
viewModelScope.launch { scope.launch {
if (firstPage.status !is Page.State.Ready) return@launch if (firstPage.status != Page.State.READY) return@launch
if (secondPage.status !is Page.State.Ready) return@launch if (secondPage.status != Page.State.READY) return@launch
val manga = manga ?: return@launch val manga = manga ?: return@launch
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@ -926,7 +910,7 @@ class ReaderViewModel(
* image will be kept so it won't be taking lots of internal disk space. * image will be kept so it won't be taking lots of internal disk space.
*/ */
fun shareImage(page: ReaderPage) { fun shareImage(page: ReaderPage) {
if (page.status !is Page.State.Ready) return if (page.status != Page.State.READY) return
val manga = manga ?: return val manga = manga ?: return
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@ -939,9 +923,9 @@ class ReaderViewModel(
} }
fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
viewModelScope.launch { scope.launch {
if (firstPage.status !is Page.State.Ready) return@launch if (firstPage.status != Page.State.READY) return@launch
if (secondPage.status !is Page.State.Ready) return@launch if (secondPage.status != Page.State.READY) return@launch
val manga = manga ?: return@launch val manga = manga ?: return@launch
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@ -958,7 +942,7 @@ class ReaderViewModel(
* Sets the image of this [page] as cover and notifies the UI of the result. * Sets the image of this [page] as cover and notifies the UI of the result.
*/ */
fun setAsCover(page: ReaderPage) { fun setAsCover(page: ReaderPage) {
if (page.status !is Page.State.Ready) return if (page.status != Page.State.READY) return
val manga = manga ?: return val manga = manga ?: return
val stream = page.stream ?: return val stream = page.stream ?: return

View file

@ -11,8 +11,6 @@ import yokai.core.archive.ArchiveReader
*/ */
internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() { internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() {
override val isLocal: Boolean = true
/** /**
* Recycles this loader and the open archive. * Recycles this loader and the open archive.
*/ */
@ -31,7 +29,7 @@ internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader
.mapIndexed { i, entry -> .mapIndexed { i, entry ->
ReaderPage(i).apply { ReaderPage(i).apply {
stream = { reader.getInputStream(entry.name)!! } stream = { reader.getInputStream(entry.name)!! }
status = Page.State.Ready status = Page.State.READY
} }
} }
.toList() .toList()

View file

@ -10,8 +10,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withIOContext
import yokai.core.archive.util.archiveReader import yokai.core.archive.archiveReader
import yokai.core.archive.util.epubReader
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
@ -83,7 +82,7 @@ class ChapterLoader(
when (format) { when (format) {
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file) is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
is LocalSource.Format.Archive -> ArchivePageLoader(format.file.archiveReader(context)) is LocalSource.Format.Archive -> ArchivePageLoader(format.file.archiveReader(context))
is LocalSource.Format.Epub -> EpubPageLoader(format.file.epubReader(context)) is LocalSource.Format.Epub -> EpubPageLoader(format.file.archiveReader(context))
} }
} }
else -> error(context.getString(MR.strings.source_not_installed)) else -> error(context.getString(MR.strings.source_not_installed))

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