Compare commits

..

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

379 changed files with 4713 additions and 8219 deletions

View file

@ -1,28 +1,13 @@
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
indent_style=space
insert_final_newline=true
[*.xml]
indent_size = 4
[*.{json,json5}]
indent_size=2
# noinspection EditorConfigKeyCorrectness
[*.{kt,kts}]
indent_size = 4
max_line_length = 120
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled
ktlint_standard_discouraged-comment-location = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-signature = disabled
indent_size=4
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true

View file

@ -35,24 +35,9 @@ body:
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.
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
- label: I have checked through the app settings for my feature.
required: true
- label: I will fill out all of the requested information in this form.
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
- label: I have written a short but informative title.
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
- 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
- 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
- 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
- label: I have updated all installed extensions.
required: true
- label: I have filled out all of the requested information in this form.
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 }}
cancel-in-progress: true
on:
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'
on: [pull_request]
jobs:
build:
@ -30,21 +22,18 @@ jobs:
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
- name: Setup Gradle
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
with:
java: 17
distro: temurin
distro: adopt
- name: Copy CI gradle.properties
run: |
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build the app
run: ./gradlew assembleStandardRelease
- name: Run unit tests
run: ./gradlew testReleaseUnitTest testStandardReleaseUnitTest
- name: Build and run tests
run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Publish test report
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"
- name: Setup Gradle
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
with:
java: 17
distro: temurin
distro: adopt
- name: Setup CHANGELOG parser
uses: taiki-e/install-action@parse-changelog
@ -80,7 +80,6 @@ jobs:
run: |
set -x
echo "VERSION_TAG=v${{github.event.inputs.version}}" >> $GITHUB_ENV
echo "BUILD_TYPE=StandardRelease" >> $GITHUB_ENV
# BETA
- name: Prepare beta build
@ -89,14 +88,9 @@ jobs:
set -x
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="1"
else
BETA_COUNT=$((BETA_COUNT+1))
fi
[ "$BETA_COUNT" = "" ] && BETA_COUNT="1" || BETA_COUNT=$((BETA_COUNT+1))
echo "VERSION_TAG=v${{github.event.inputs.version}}-b${BETA_COUNT}" >> $GITHUB_ENV
echo "BUILD_TYPE=StandardBeta" >> $GITHUB_ENV
# NIGHTLY
- name: Prepare nightly build
@ -104,17 +98,23 @@ jobs:
run: |
set -x
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_SHORT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build the app
if: startsWith(env.BUILD_TYPE, 'Standard')
run: ./gradlew assemble${{ env.BUILD_TYPE }}
# PROD
- name: Build release build and run tests
if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta != 'true'
run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Run unit tests
if: startsWith(env.BUILD_TYPE, 'Standard')
run: ./gradlew testReleaseUnitTest test${{ env.BUILD_TYPE }}UnitTest
# BETA
- name: Build beta build and run tests
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
uses: actions/upload-artifact@v4
@ -154,7 +154,7 @@ jobs:
echo "STAGE=${stage}" >> $GITHUB_OUTPUT
- name: Sign APK
uses: null2264/actions/android-signer@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
uses: null2264/actions/android-signer@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
if: env.VERSION_TAG != ''
with:
releaseDir: app/build/outputs/apk/standard/${{ steps.version_stage.outputs.STAGE }}

1
.gitignore vendored
View file

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

View file

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

View file

@ -6,101 +6,9 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co
- `Additions` - New features
- `Changes` - Behaviour/visual changes
- `Fixes` - Bugfixes
- `Translation` - Translation 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]
### Changes

View file

@ -12,13 +12,10 @@
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)
[![Mirror: GitLab](https://img.shields.io/badge/mirror-GitLab-orange.svg?labelColor=27303D)](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/)
[![Mirror: GitLab](https://img.shields.io/badge/mirror-GitLab-orange.svg)](https://gitlab.com/null2264/yokai)
<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.gms.googleservices.GoogleServicesPlugin
import java.io.ByteArrayOutputStream
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("yokai.android.application")
id("yokai.android.application.compose")
alias(androidx.plugins.application)
alias(kotlinx.plugins.android)
alias(kotlinx.plugins.compose.compiler)
alias(kotlinx.plugins.serialization)
alias(kotlinx.plugins.parcelize)
alias(libs.plugins.aboutlibraries)
@ -21,12 +23,15 @@ if (gradle.startParameter.taskRequests.toString().contains("standard", true)) {
}
fun runCommand(command: String): String {
val result = providers.exec { commandLine(command.split(" ")) }
return result.standardOutput.asText.get().trim()
val byteOut = ByteArrayOutputStream()
project.exec {
commandLine = command.split(" ")
standardOutput = byteOut
}
return String(byteOut.toByteArray()).trim()
}
@Suppress("PropertyName")
val _versionName = "1.10.0"
val _versionName = "1.9.8"
val betaCount by lazy {
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 {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
versionCode = 158
versionCode = 157
versionName = _versionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled = true
@ -67,6 +72,11 @@ android {
//noinspection ChromeOsAbiSupport
abiFilters += supportedAbis
}
externalNativeBuild {
cmake {
this.arguments("-DHAVE_LIBJXL=FALSE")
}
}
}
splits {
@ -112,6 +122,7 @@ android {
buildFeatures {
viewBinding = true
compose = true
// If you're here because there's not BuildConfig, build the app first, it'll generate it for you
buildConfig = true
@ -145,8 +156,7 @@ android {
}
dependencies {
implementation(projects.core.archive)
implementation(projects.core.main)
implementation(projects.core)
implementation(projects.data)
implementation(projects.domain)
implementation(projects.i18n)
@ -241,6 +251,8 @@ dependencies {
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION))
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
@ -268,15 +280,17 @@ tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> {
compilerOptions.freeCompilerArgs.addAll(
"-Xcontext-receivers",
// "-opt-in=kotlin.Experimental",
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
// "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
// "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
@ -284,6 +298,19 @@ tasks {
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-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

View file

@ -300,7 +300,7 @@ fun buildLogWritersToAdd(
) = buildList {
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"

View file

@ -40,13 +40,9 @@ import eu.kanade.tachiyomi.util.system.launchIO
import java.util.Calendar
import java.util.Date
import kotlin.math.min
import kotlin.math.roundToLong
import kotlinx.coroutines.MainScope
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.models.cover
import yokai.domain.recents.interactor.GetRecents
class UpdatesGridGlanceWidget : GlanceAppWidget() {
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) {
coroutineScope.launchIO {
// Don't show anything when lock is active
@ -111,7 +80,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.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)
ids.forEach { update(app, it) }

View file

@ -17,15 +17,15 @@ import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import eu.kanade.tachiyomi.appwidget.ContainerModifier
import eu.kanade.tachiyomi.appwidget.util.stringResource
import yokai.i18n.MR
import yokai.presentation.core.Constants
import eu.kanade.tachiyomi.ui.main.MainActivity
@Composable
fun LockedWidget() {
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)
}
Box(

View file

@ -17,18 +17,19 @@ import androidx.glance.layout.Row
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding
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.util.calculateRowAndColumnCount
import eu.kanade.tachiyomi.appwidget.util.stringResource
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.SearchActivity
import yokai.i18n.MR
import yokai.presentation.core.Constants
@Composable
fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
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)
Column(
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.remember
import eu.kanade.tachiyomi.core.preference.Preference
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
@Composable
fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() }
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,
) {
fun getMangaImpl(): MangaImpl {
return MangaImpl(
source = this.source,
url = this.url,
).apply {
return MangaImpl().apply {
url = this@BackupManga.url
title = this@BackupManga.title
artist = this@BackupManga.artist
author = this@BackupManga.author
@ -69,6 +67,7 @@ data class BackupManga(
status = this@BackupManga.status
thumbnail_url = this@BackupManga.thumbnailUrl
favorite = this@BackupManga.favorite
source = this@BackupManga.source
date_added = this@BackupManga.dateAdded
viewer_flags = (
this@BackupManga.viewer_flags

View file

@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
import android.content.Context
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.ui.library.LibrarySort
import java.io.Serializable
import yokai.i18n.MR
import yokai.util.lang.getString
import java.io.Serializable
interface Category : Serializable {
@ -37,7 +37,8 @@ interface Category : Serializable {
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
get() = (
@ -55,21 +56,7 @@ interface Category : Serializable {
fun mangaOrderToString(): String =
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 {
const val sourceSplitter = "◘•◘"
const val langSplitter = "⨼⨦⨠"
var lastCategoriesAddedTo = emptySet<Int>()
fun create(name: String): Category = CategoryImpl().apply {

View file

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

View file

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

View file

@ -1,10 +1,10 @@
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 yokai.data.updateStrategyAdapter
data class LibraryManga(
val manga: Manga,
var unread: Int = 0,
var read: Int = 0,
var category: Int = 0,
@ -13,11 +13,41 @@ data class LibraryManga(
var latestUpdate: Long = 0,
var lastRead: 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
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 {
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(
// manga
id: Long,
@ -48,37 +78,34 @@ data class LibraryManga(
latestUpdate: Long,
lastRead: Long,
lastFetch: Long,
): LibraryManga = LibraryManga(
manga = Manga.mapper(
id = id,
source = source,
url = url,
artist = artist,
author = author,
description = description,
genre = genre,
title = title,
status = status,
thumbnailUrl = thumbnailUrl,
favorite = favorite,
lastUpdate = lastUpdate,
initialized = initialized,
viewerFlags = viewerFlags,
hideTitle = hideTitle,
chapterFlags = chapterFlags,
dateAdded = dateAdded,
filteredScanlators = filteredScanlators,
updateStrategy = updateStrategy,
coverLastModified = coverLastModified,
),
read = readCount.roundToInt(),
unread = maxOf((total - readCount).roundToInt(), 0),
totalChapters = total.toInt(),
bookmarkCount = bookmarkCount.roundToInt(),
category = categoryId.toInt(),
latestUpdate = latestUpdate,
lastRead = lastRead,
lastFetch = lastFetch,
)
): LibraryManga = createBlank(categoryId.toInt()).apply {
this.id = id
this.source = source
this.url = url
this.artist = artist
this.author = author
this.description = description
this.genre = genre
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnailUrl
this.favorite = favorite
this.last_update = lastUpdate ?: 0L
this.initialized = initialized
this.viewer_flags = viewerFlags.toInt()
this.hide_title = hideTitle
this.chapter_flags = chapterFlags.toInt()
this.date_added = dateAdded ?: 0L
this.filtered_scanlators = filteredScanlators
this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode)
this.cover_last_modified = coverLastModified
this.read = readCount.roundToInt()
this.unread = maxOf((total - readCount).roundToInt(), 0)
this.totalChapters = total.toInt()
this.bookmarkCount = bookmarkCount.roundToInt()
this.latestUpdate = latestUpdate
this.lastRead = lastRead
this.lastFetch = lastFetch
}
}
}

View file

@ -182,13 +182,15 @@ var Manga.vibrantCoverColor: Int?
id?.let { MangaCoverMetadata.setVibrantColor(it, value) }
}
fun Manga.Companion.create(url: String, title: String, source: Long = 0) =
MangaImpl(
source = source,
url = url,
).apply {
this.title = title
}
fun Manga.Companion.create(source: Long) = MangaImpl().apply {
this.source = source
}
fun Manga.Companion.create(pathUrl: String, title: String, source: Long = 0) = MangaImpl().apply {
url = pathUrl
this.title = title
this.source = source
}
fun Manga.Companion.mapper(
id: Long,
@ -211,12 +213,14 @@ fun Manga.Companion.mapper(
filteredScanlators: String?,
updateStrategy: Long,
coverLastModified: Long,
) = create(url, title, source).apply {
) = create(source).apply {
this.id = id
this.url = url
this.artist = artist
this.author = author
this.description = description
this.genre = genre
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnailUrl
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()) {
companion object {
fun createBlank() = MangaChapterHistory(
MangaImpl(null, -1, ""),
ChapterImpl(),
HistoryImpl(),
)
fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl())
fun mapper(
// manga

View file

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

View file

@ -21,8 +21,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
import yokai.domain.download.DownloadPreferences
import yokai.i18n.MR
import yokai.util.lang.getString
@ -171,7 +171,7 @@ class DownloadManager(
return files.sortedBy { it.name }
.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
import android.content.Context
import android.os.Looper
import co.touchlab.kermit.Logger
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.cache.ChapterCache
@ -54,8 +55,8 @@ import kotlinx.coroutines.supervisorScope
import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
import yokai.core.archive.ZipWriter
import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo
@ -364,11 +365,11 @@ class Downloader(
// Get all the URLs to the source images, fetch pages if necessary
pageList.filter { it.imageUrl.isNullOrEmpty() }.forEach { page ->
page.status = Page.State.LoadPage
page.status = Page.State.LOAD_PAGE
try {
page.imageUrl = download.source.getImageUrl(page)
} catch (e: Throwable) {
page.status = Page.State.Error
page.status = Page.State.ERROR
}
}
@ -493,12 +494,12 @@ class Downloader(
page.uri = file.uri
page.progress = 100
page.status = Page.State.Ready
page.status = Page.State.READY
} catch (e: Throwable) {
if (e is CancellationException) throw e
// Mark this page as error and allow to download the remaining
page.progress = 0
page.status = Page.State.Error
page.status = Page.State.ERROR
notifier.onError(e.message, chapName, download.manga.title)
}
}
@ -517,7 +518,7 @@ class Downloader(
tmpDir: UniFile,
filename: String,
): UniFile {
page.status = Page.State.DownloadImage
page.status = Page.State.DOWNLOAD_IMAGE
page.progress = 0
return flow {
val response = source.getImage(page)
@ -603,9 +604,8 @@ class Downloader(
dirname: String,
tmpDir: UniFile,
) {
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 ->
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
ZipWriter(context, zip).use { writer ->
tmpDir.listFiles()?.forEach { 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
val downloadedImages: Int
get() = pages?.count { it.status is Page.State.Ready } ?: 0
get() = pages?.count { it.status == Page.State.READY } ?: 0
@Transient
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)

View file

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

View file

@ -173,17 +173,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val mangaList = (
if (savedMangasList != null) {
val mangas =
getLibraryManga.await()
.filter { it.manga.id in savedMangasList }
.distinctBy { it.manga.id }
val mangas = getLibraryManga.await().filter {
it.id in savedMangasList
}.distinctBy { it.id }
val categoryId = inputData.getInt(KEY_CATEGORY, -1)
if (categoryId > -1) categoryIds.add(categoryId)
mangas
} else {
getMangaToUpdate()
}
).sortedBy { it.manga.title }
).sortedBy { it.title }
return withIOContext {
try {
@ -228,7 +227,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
// Initialize the variables holding the progress of the updates.
mangaToUpdate.addAll(mangaToAdd)
mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.manga.source })
mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.source })
checkIfMassiveUpdate()
coroutineScope {
val list = mangaToUpdateMap.keys.map { source ->
@ -258,42 +257,42 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateDetails(mangaToUpdate: List<LibraryManga>) = coroutineScope {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
val asyncList = mangaToUpdate.groupBy { it.manga.source }.values.map { list ->
val asyncList = mangaToUpdate.groupBy { it.source }.values.map { list ->
async {
requestSemaphore.withPermit {
list.forEach { manga ->
ensureActive()
val source = sourceManager.get(manga.manga.source) as? HttpSource ?: return@async
val source = sourceManager.get(manga.source) as? HttpSource ?: return@async
notifier.showProgressNotification(
manga.manga,
manga,
count.andIncrement,
mangaToUpdate.size,
)
ensureActive()
val networkManga = try {
source.getMangaDetails(manga.manga.copy())
source.getMangaDetails(manga.copy())
} catch (e: java.lang.Exception) {
Logger.e(e)
null
}
if (networkManga != null) {
manga.manga.prepareCoverUpdate(coverCache, networkManga, false)
val thumbnailUrl = manga.manga.thumbnail_url
manga.manga.copyFrom(networkManga)
manga.manga.initialized = true
manga.prepareCoverUpdate(coverCache, networkManga, false)
val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga)
manga.initialized = true
val request: ImageRequest =
if (thumbnailUrl != manga.manga.thumbnail_url) {
if (thumbnailUrl != manga.thumbnail_url) {
// load new covers in background
ImageRequest.Builder(context).data(manga.manga.cover())
ImageRequest.Builder(context).data(manga.cover())
.memoryCachePolicy(CachePolicy.DISABLED).build()
} else {
ImageRequest.Builder(context).data(manga.manga.cover())
ImageRequest.Builder(context).data(manga.cover())
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.WRITE_ONLY)
.build()
}
context.imageLoader.execute(request)
updateManga.await(manga.manga.toMangaUpdate())
updateManga.await(manga.toMangaUpdate())
}
}
}
@ -314,9 +313,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val loggedServices = trackManager.services.filter { it.isLogged }
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 ->
val service = trackManager.getService(track.sync_id)
@ -325,7 +324,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val newTrack = service.refresh(track)
insertTrack.await(newTrack)
syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.manga.id!!, false), track, service)
syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.id!!, false), track, service)
} catch (e: Exception) {
Logger.e(e)
}
@ -377,7 +376,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private fun checkIfMassiveUpdate() {
val largestSourceSize = mangaToUpdate
.groupBy { it.manga.source }
.groupBy { it.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (largestSourceSize > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
@ -392,7 +391,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val httpSource = sourceManager.get(source) as? HttpSource ?: return false
while (count < mangaToUpdateMap[source]!!.size) {
val manga = mangaToUpdateMap[source]!![count]
val shouldDownload = manga.manga.shouldDownloadNewChapters(preferences)
val shouldDownload = manga.shouldDownloadNewChapters(preferences)
if (updateMangaChapters(manga, this.count.andIncrement, httpSource, shouldDownload)) {
hasDownloads = true
}
@ -411,15 +410,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
try {
var hasDownloads = false
ensureActive()
notifier.showProgressNotification(manga.manga, progress, mangaToUpdate.size)
val fetchedChapters = source.getChapterList(manga.manga.copy())
notifier.showProgressNotification(manga, progress, mangaToUpdate.size)
val fetchedChapters = source.getChapterList(manga.copy())
if (fetchedChapters.isNotEmpty()) {
val newChapters = syncChaptersWithSource(fetchedChapters, manga.manga, source)
val newChapters = syncChaptersWithSource(fetchedChapters, manga, source)
if (newChapters.first.isNotEmpty()) {
if (shouldDownload) {
downloadChapters(
manga.manga,
manga,
newChapters.first.sortedBy { it.chapter_number },
)
hasDownloads = true
@ -429,24 +428,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
if (deleteRemoved && newChapters.second.isNotEmpty()) {
val removedChapters = newChapters.second.filter {
downloadManager.isChapterDownloaded(it, manga.manga) &&
downloadManager.isChapterDownloaded(it, manga) &&
newChapters.first.none { newChapter ->
newChapter.chapter_number == it.chapter_number && it.scanlator.isNullOrBlank()
}
}
if (removedChapters.isNotEmpty()) {
downloadManager.deleteChapters(removedChapters, manga.manga, source)
downloadManager.deleteChapters(removedChapters, manga, source)
}
}
if (newChapters.first.size + newChapters.second.size > 0) {
sendUpdate(manga.manga.id)
sendUpdate(manga.id)
}
}
return@coroutineScope hasDownloads
} catch (e: Exception) {
if (e !is CancellationException) {
failedUpdates[manga.manga] = e.message
Logger.e { "Failed updating: ${manga.manga.title}: $e" }
failedUpdates[manga] = e.message
Logger.e { "Failed updating: ${manga.title}: $e" }
}
return@coroutineScope false
}
@ -462,17 +461,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val restrictions = preferences.libraryUpdateMangaRestriction().get()
return mangaToAdd.filter { manga ->
when {
MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_completed)
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_completed)
}
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 -> {
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 -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_always_update)
manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_always_update)
}
else -> {
return@filter true
@ -504,10 +503,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) {
categoryIds.addAll(categoriesToUpdate)
libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.manga.id }
libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.id }
} else {
categoryIds.addAll(getCategories.await().mapNotNull { it.id } + 0)
libraryManga.distinctBy { it.manga.id }
libraryManga.distinctBy { it.id }
}
}
@ -565,13 +564,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
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)
addManga(mangas)
}
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)
addManga(mangas)
}
@ -580,7 +579,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val distinctManga = mangaToAdd.filter { it !in mangaToUpdate }
mangaToUpdate.addAll(distinctManga)
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
// finished running
if (mangaToUpdateMap[it.key].isNullOrEmpty()) {
@ -728,9 +727,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (mangaToUse != null) {
builder.putLongArray(
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()

View file

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

View file

@ -487,7 +487,7 @@ class NotificationReceiver : BroadcastReceiver() {
*/
internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent {
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)
.putExtra(Constants.MANGA_EXTRA, manga.id)
.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.PreferenceStore
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.domain.manga.models.Manga
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.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.system.Themes
import java.text.DateFormat
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
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.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 dateFormatRaw() = preferenceStore.getString(Keys.dateFormat, "")
@Deprecated("Use dateFormatRaw().get().asDateFormat() instead")
fun dateFormat(format: String = dateFormatRaw().get()): DateFormat = format.asDateFormat()
fun dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
}
fun appLanguage() = preferenceStore.getString("app_language", "")

View file

@ -49,13 +49,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("completedAt", createDate(track.finished_reading_date))
}
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALAddMangaResult>()
.let {
track.library_id = it.data.entry.id
track
}
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALAddMangaResult>()
.let {
track.library_id = it.data.entry.id
track
}
}
}
}
@ -72,9 +74,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("completedAt", createDate(track.finished_reading_date))
}
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
track
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
track
}
}
}
@ -86,11 +90,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("query", search)
}
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALSearchResult>()
.data.page.media
.map { it.toALManga().toTrack() }
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALSearchResult>()
.data.page.media
.map { it.toALManga().toTrack() }
}
}
}
@ -103,13 +109,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("manga_id", track.media_id)
}
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALUserListMangaQueryResult>()
.data.page.mediaList
.map { it.toALUserManga() }
.firstOrNull()
?.toTrack()
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALUserListMangaQueryResult>()
.data.page.mediaList
.map { it.toALUserManga() }
.firstOrNull()
?.toTrack()
}
}
}

View file

@ -75,25 +75,29 @@ class BangumiApi(
.appendQueryParameter("responseGroup", "large")
.appendQueryParameter("max_results", "20")
.build()
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<BGMSearchResult>()
.let { result ->
if (result.code == 404) emptyList<TrackSearch>()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<BGMSearchResult>()
.let { result ->
if (result.code == 404) emptyList<TrackSearch>()
result.list
?.map { it.toTrackSearch(trackId) }
.orEmpty()
}
result.list
?.map { it.toTrackSearch(trackId) }
.orEmpty()
}
}
}
}
suspend fun findLibManga(track: Track): Track? {
return withIOContext {
authClient.newCall(GET("$API_URL/subject/${track.media_id}"))
.awaitSuccess()
.parseAs<BGMSearchItem>()
.toTrackSearch(trackId)
with(json) {
authClient.newCall(GET("$API_URL/subject/${track.media_id}"))
.awaitSuccess()
.parseAs<BGMSearchItem>()
.toTrackSearch(trackId)
}
}
}
@ -107,25 +111,29 @@ class BangumiApi(
.build()
// TODO: get user readed chapter here
authClient.newCall(requestUserRead)
.awaitSuccess()
.parseAs<BGMCollectionResponse>()
.let {
if (it.code == 400) return@let null
with(json) {
authClient.newCall(requestUserRead)
.awaitSuccess()
.parseAs<BGMCollectionResponse>()
.let {
if (it.code == 400) return@let null
track.status = it.status?.id?.toInt() ?: Bangumi.DEFAULT_STATUS
track.last_chapter_read = it.epStatus!!.toFloat()
track.score = it.rating!!.toFloat()
track
}
track.status = it.status?.id?.toInt() ?: Bangumi.DEFAULT_STATUS
track.last_chapter_read = it.epStatus!!.toFloat()
track.score = it.rating!!.toFloat()
track
}
}
}
}
suspend fun accessToken(code: String): BGMOAuth {
return withIOContext {
client.newCall(accessTokenRequest(code))
.awaitSuccess()
.parseAs()
with(json) {
client.newCall(accessTokenRequest(code))
.awaitSuccess()
.parseAs()
}
}
}

View file

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

View file

@ -131,12 +131,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
suspend fun search(query: String): List<TrackSearch> {
return withIOContext {
authClient.newCall(GET(ALGOLIA_KEY_URL))
.awaitSuccess()
.parseAs<KitsuSearchResult>()
.let {
algoliaSearch(it.media.key, query)
}
with(json) {
authClient.newCall(GET(ALGOLIA_KEY_URL))
.awaitSuccess()
.parseAs<KitsuSearchResult>()
.let {
algoliaSearch(it.media.key, query)
}
}
}
}
@ -145,23 +147,25 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
val jsonObject = buildJsonObject {
put("params", "query=$query$ALGOLIA_FILTER")
}
client.newCall(
POST(
ALGOLIA_URL,
headers = headersOf(
"X-Algolia-Application-Id",
ALGOLIA_APP_ID,
"X-Algolia-API-Key",
key,
with(json) {
client.newCall(
POST(
ALGOLIA_URL,
headers = headersOf(
"X-Algolia-Application-Id",
ALGOLIA_APP_ID,
"X-Algolia-API-Key",
key,
),
body = jsonObject.toString().toRequestBody(jsonMime),
),
body = jsonObject.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
.parseAs<KitsuAlgoliaSearchResult>()
.hits
.filter { it.subtype != "novel" }
.map { it.toTrack() }
)
.awaitSuccess()
.parseAs<KitsuAlgoliaSearchResult>()
.hits
.filter { it.subtype != "novel" }
.map { it.toTrack() }
}
}
}
@ -171,16 +175,18 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
.appendQueryParameter("include", "manga")
.build()
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuListSearchResult>()
.let {
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
it.firstToTrack()
} else {
null
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuListSearchResult>()
.let {
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
it.firstToTrack()
} else {
null
}
}
}
}
}
}
@ -190,16 +196,18 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[id]=${track.media_id}")
.appendQueryParameter("include", "manga")
.build()
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuListSearchResult>()
.let {
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
it.firstToTrack()
} else {
throw Exception("Could not find manga")
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuListSearchResult>()
.let {
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
it.firstToTrack()
} else {
throw Exception("Could not find manga")
}
}
}
}
}
}
@ -212,9 +220,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.add("client_id", CLIENT_ID)
.add("client_secret", CLIENT_SECRET)
.build()
client.newCall(POST(LOGIN_URL, body = formBody))
.awaitSuccess()
.parseAs()
with(json) {
client.newCall(POST(LOGIN_URL, body = formBody))
.awaitSuccess()
.parseAs()
}
}
}
@ -223,11 +233,13 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
val url = "${BASE_URL}users".toUri().buildUpon()
.encodedQuery("filter[self]=true")
.build()
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuCurrentUserResult>()
.data[0]
.id
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuCurrentUserResult>()
.data[0]
.id
}
}
}

View file

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

View file

@ -37,13 +37,15 @@ class MangaUpdatesApi(
suspend fun getSeriesListItem(track: Track): Pair<MUListItem, MURating?> {
val listItem =
authClient.newCall(
GET(
url = "$BASE_URL/v1/lists/series/${track.media_id}",
),
)
.awaitSuccess()
.parseAs<MUListItem>()
with(json) {
authClient.newCall(
GET(
url = "$BASE_URL/v1/lists/series/${track.media_id}",
),
)
.awaitSuccess()
.parseAs<MUListItem>()
}
val rating = getSeriesRating(track)
@ -102,13 +104,15 @@ class MangaUpdatesApi(
private suspend fun getSeriesRating(track: Track): MURating? {
return try {
authClient.newCall(
GET(
url = "$BASE_URL/v1/series/${track.media_id}/rating",
),
)
.awaitSuccess()
.parseAs<MURating>()
with(json) {
authClient.newCall(
GET(
url = "$BASE_URL/v1/series/${track.media_id}/rating",
),
)
.awaitSuccess()
.parseAs<MURating>()
}
} catch (e: Exception) {
null
}
@ -147,16 +151,18 @@ class MangaUpdatesApi(
},
)
}
return client.newCall(
POST(
url = "$BASE_URL/v1/series/search",
body = body.toString().toRequestBody(CONTENT_TYPE),
),
)
.awaitSuccess()
.parseAs<MUSearchResult>()
.results
.map { it.record }
return with(json) {
client.newCall(
POST(
url = "$BASE_URL/v1/series/search",
body = body.toString().toRequestBody(CONTENT_TYPE),
),
)
.awaitSuccess()
.parseAs<MUSearchResult>()
.results
.map { it.record }
}
}
suspend fun authenticate(username: String, password: String): MUContext? {
@ -164,15 +170,17 @@ class MangaUpdatesApi(
put("username", username)
put("password", password)
}
return client.newCall(
PUT(
url = "$BASE_URL/v1/account/login",
body = body.toString().toRequestBody(CONTENT_TYPE),
),
)
.awaitSuccess()
.parseAs<MULoginResponse>()
.context
return with(json) {
client.newCall(
PUT(
url = "$BASE_URL/v1/account/login",
body = body.toString().toRequestBody(CONTENT_TYPE),
),
)
.awaitSuccess()
.parseAs<MULoginResponse>()
.context
}
}
companion object {

View file

@ -45,9 +45,11 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.add("code_verifier", codeVerifier)
.add("grant_type", "authorization_code")
.build()
client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
.awaitSuccess()
.parseAs()
with(json) {
client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
.awaitSuccess()
.parseAs()
}
}
}
@ -57,10 +59,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url("$BASE_API_URL/users/@me")
.get()
.build()
authClient.newCall(request)
.awaitSuccess()
.parseAs<MALUser>()
.name
with(json) {
authClient.newCall(request)
.awaitSuccess()
.parseAs<MALUser>()
.name
}
}
}
@ -71,13 +75,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true")
.build()
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALSearchResult>()
.data
.map { async { getMangaDetails(it.node.id) } }
.awaitAll()
.filter { !it.publishing_type.contains("novel") }
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALSearchResult>()
.data
.map { async { getMangaDetails(it.node.id) } }
.awaitAll()
.filter { !it.publishing_type.contains("novel") }
}
}
}
@ -87,22 +93,24 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(id.toString())
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
.build()
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALManga>()
.let {
TrackSearch.create(TrackManager.MYANIMELIST).apply {
media_id = it.id
title = it.title
summary = it.synopsis
total_chapters = it.numChapters
cover_url = (it.covers?.large ?: it.covers?.medium).orEmpty()
tracking_url = "https://myanimelist.net/manga/$media_id"
publishing_status = it.status.replace("_", " ")
publishing_type = it.mediaType.replace("_", " ")
start_date = it.startDate ?: ""
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALManga>()
.let {
TrackSearch.create(TrackManager.MYANIMELIST).apply {
media_id = it.id
title = it.title
summary = it.synopsis
total_chapters = it.numChapters
cover_url = it.covers.large
tracking_url = "https://myanimelist.net/manga/$media_id"
publishing_status = it.status.replace("_", " ")
publishing_type = it.mediaType.replace("_", " ")
start_date = it.startDate ?: ""
}
}
}
}
}
}
@ -124,10 +132,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(mangaUrl(track.media_id).toString())
.put(formBodyBuilder.build())
.build()
authClient.newCall(request)
.awaitSuccess()
.parseAs<MALListItemStatus>()
.let { parseMangaItem(it, track) }
with(json) {
authClient.newCall(request)
.awaitSuccess()
.parseAs<MALListItemStatus>()
.let { parseMangaItem(it, track) }
}
}
}
@ -137,13 +147,15 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(track.media_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build()
authClient.newCall(GET(uri.toString()))
.awaitSuccess()
.parseAs<MALListItem>()
.let { item ->
track.total_chapters = item.numChapters
item.myListStatus?.let { parseMangaItem(it, track) }
}
with(json) {
authClient.newCall(GET(uri.toString()))
.awaitSuccess()
.parseAs<MALListItem>()
.let { item ->
track.total_chapters = item.numChapters
item.myListStatus?.let { parseMangaItem(it, track) }
}
}
}
}
@ -178,9 +190,11 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(urlBuilder.build().toString())
.get()
.build()
authClient.newCall(request)
.awaitSuccess()
.parseAs()
with(json) {
authClient.newCall(request)
.awaitSuccess()
.parseAs()
}
}
}

View file

@ -34,7 +34,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
// Add the authorization header to the original request
val authRequest = originalRequest.newBuilder()
.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()
return chain.proceed(authRequest)
@ -66,7 +66,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
return runCatching {
if (response.isSuccessful) {
response.parseAs<MALOAuth>()
with(json) { response.parseAs<MALOAuth>() }
} else {
response.close()
null

View file

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

View file

@ -74,10 +74,12 @@ class ShikimoriApi(
.appendQueryParameter("search", search)
.appendQueryParameter("limit", "20")
.build()
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<List<SMManga>>()
.map { it.toTrack(trackId) }
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<List<SMManga>>()
.map { it.toTrack(trackId) }
}
}
}
@ -100,44 +102,51 @@ class ShikimoriApi(
val urlMangas = "$API_URL/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString())
.build()
val manga =
val manga = with(json) {
authClient.newCall(GET(urlMangas.toString()))
.awaitSuccess()
.parseAs<SMManga>()
}
val url = "$API_URL/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga")
.build()
authClient.newCall(GET(url.toString()))
.execute()
.parseAs<List<SMUserListEntry>>()
.let { entries ->
if (entries.size > 1) {
throw Exception("Too manga manga in response")
with(json) {
authClient.newCall(GET(url.toString()))
.execute()
.parseAs<List<SMUserListEntry>>()
.let { entries ->
if (entries.size > 1) {
throw Exception("Too manga manga in response")
}
entries
.map { it.toTrack(trackId, manga) }
.firstOrNull()
}
entries
.map { it.toTrack(trackId, manga) }
.firstOrNull()
}
}
}
}
suspend fun getCurrentUser(): Int {
return withIOContext {
authClient.newCall(GET("$API_URL/users/whoami"))
.awaitSuccess()
.parseAs<SMUser>()
.id
with(json) {
authClient.newCall(GET("$API_URL/users/whoami"))
.awaitSuccess()
.parseAs<SMUser>()
.id
}
}
}
suspend fun accessToken(code: String): SMOAuth {
return withIOContext {
client.newCall(accessTokenRequest(code))
.awaitSuccess()
.parseAs()
with(json) {
client.newCall(accessTokenRequest(code))
.awaitSuccess()
.parseAs()
}
}
}

View file

@ -52,7 +52,9 @@ class TachideskApi {
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 {
title = manga.title
@ -72,7 +74,9 @@ class TachideskApi {
suspend fun updateProgress(track: Track): Track {
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
client.newCall(

View file

@ -11,12 +11,12 @@ import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.withIOContext
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.base.models.Version
import java.util.*
import java.util.concurrent.*
class AppUpdateChecker(
private val json: Json = Injekt.get(),
@ -31,46 +31,48 @@ class AppUpdateChecker(
}
return withIOContext {
val result = if (preferences.checkForBetas().get()) {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases"))
.await()
.parseAs<List<GithubRelease>>()
.let { githubReleases ->
val releases =
githubReleases.take(10).filter { isNewVersion(it.version) }
// Check if any of the latest versions are newer than the current version
val release = releases
.maxWithOrNull { r1, r2 ->
when {
r1.version == r2.version -> 0
isNewVersion(r2.version, r1.version) -> -1
else -> 1
val result = with(json) {
if (preferences.checkForBetas().get()) {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases"))
.await()
.parseAs<List<GithubRelease>>()
.let { githubReleases ->
val releases =
githubReleases.take(10).filter { isNewVersion(it.version) }
// Check if any of the latest versions are newer than the current version
val release = releases
.maxWithOrNull { r1, r2 ->
when {
r1.version == r2.version -> 0
isNewVersion(r2.version, r1.version) -> -1
else -> 1
}
}
preferences.lastAppCheck().set(Date().time)
if (release != null) {
AppUpdateResult.NewUpdate(release)
} else {
AppUpdateResult.NoNewUpdate
}
preferences.lastAppCheck().set(Date().time)
if (release != null) {
AppUpdateResult.NewUpdate(release)
} else {
AppUpdateResult.NoNewUpdate
}
}
} else {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
.await()
.parseAs<GithubRelease>()
.let {
preferences.lastAppCheck().set(Date().time)
} else {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
.await()
.parseAs<GithubRelease>()
.let {
preferences.lastAppCheck().set(Date().time)
// Check if latest version is newer than the current version
if (isNewVersion(it.version)) {
AppUpdateResult.NewUpdate(it)
} else {
AppUpdateResult.NoNewUpdate
// Check if latest version is newer than the current version
if (isNewVersion(it.version)) {
AppUpdateResult.NewUpdate(it)
} else {
AppUpdateResult.NoNewUpdate
}
}
}
}
}
if (doExtrasAfterNewUpdate && result is AppUpdateResult.NewUpdate) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&

View file

@ -7,7 +7,6 @@ import android.os.Parcelable
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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.InstallStep
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.util.system.launchNow
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.async
import kotlinx.coroutines.flow.Flow
@ -31,6 +27,8 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.base.BasePreferences
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

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.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Process
import androidx.localbroadcastmanager.content.LocalBroadcastManager
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.isShizukuInstalled
import java.io.BufferedReader
import java.io.InputStream
import java.lang.reflect.Method
import java.util.*
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -18,21 +23,38 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuRemoteProcess
import yokai.i18n.MR
import yokai.util.lang.getString
import rikka.sui.Sui
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(
context: Context,
finishedQueue: (Installer) -> Unit,
) : Installer(context, finishedQueue) {
class ShizukuInstaller(private val context: Context, val finishedQueue: (ShizukuInstaller) -> Unit) {
private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
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 {
Logger.d { "Shizuku was killed prematurely" }
finishedQueue(this)
}
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener {
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
@ -47,13 +69,13 @@ class ShizukuInstaller(
}
}
override var ready = false
var ready = false
private val newProcess: Method
init {
Shizuku.addBinderDeadListener(shizukuDeadListener)
require(Shizuku.pingBinder() && context.isShizukuInstalled) {
require(Shizuku.pingBinder() && (context.isPackageInstalled(shizukuPkgName) || Sui.isSui())) {
finishedQueue(this)
context.getString(MR.strings.ext_installer_shizuku_stopped)
}
@ -69,8 +91,9 @@ class ShizukuInstaller(
newProcess.isAccessible = true
}
override fun processEntry(entry: Entry) {
super.processEntry(entry)
@Suppress("BlockingMethodInNonBlockingContext")
fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
ioScope.launch {
var sessionId: String? = null
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.removeRequestPermissionResultListener(shizukuPermissionListener)
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 {

View file

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -23,6 +24,7 @@ import yokai.domain.extension.repo.model.ExtensionRepo
internal class ExtensionApi {
private val json: Json by injectLazy()
private val networkService: NetworkHelper by injectLazy()
private val getExtensionRepo: GetExtensionRepo by injectLazy()
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
@ -45,9 +47,11 @@ internal class ExtensionApi {
.newCall(GET("$repoBaseUrl/index.min.json"))
.awaitSuccess()
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions(repoBaseUrl)
with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions(repoBaseUrl)
}
} catch (e: Throwable) {
Logger.e(e) { "Failed to get extensions from $repoBaseUrl" }
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 eu.kanade.tachiyomi.extension.ExtensionInstallerJob
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.ui.extension.ExtensionIntallInfo
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.launchUI
import eu.kanade.tachiyomi.util.system.toast
import java.io.File
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -48,6 +47,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences
import java.io.File
/**
* 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.util.lang.compareToCaseInsensitiveNaturalOrder
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.extension
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.api.get
import uy.kohesive.injekt.injectLazy
import yokai.core.archive.util.archiveReader
import yokai.core.archive.util.epubReader
import yokai.core.archive.archiveReader
import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo
import yokai.core.metadata.copyFromComicInfo
@ -41,7 +42,6 @@ import yokai.domain.chapter.services.ChapterRecognition
import yokai.domain.source.SourcePreferences
import yokai.domain.storage.StorageManager
import yokai.i18n.MR
import yokai.util.fillMetadata
import yokai.util.lang.getString
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
@ -410,7 +410,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
}
is Format.Epub -> {
format.file.epubReader(context).use { epub ->
EpubFile(format.file.archiveReader(context)).use { epub ->
val entry = epub.getImagesFromPages().firstOrNull()
entry?.let { updateCover(manga, epub.getInputStream(it)!!, context) }
@ -433,7 +433,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
true
}
is Format.Epub -> {
format.file.epubReader(context).use { epub ->
EpubFile(format.file.archiveReader(context)).use { epub ->
epub.fillMetadata(chapter, manga)
}
true

View file

@ -1,13 +1,20 @@
package eu.kanade.tachiyomi.source
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.source.model.Page
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.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.Dispatchers
import kotlinx.coroutines.Job
@ -17,8 +24,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import yokai.i18n.MR
import yokai.util.lang.getString
import java.util.concurrent.ConcurrentHashMap
class SourceManager(
private val context: Context,
@ -34,8 +40,28 @@ class SourceManager(
val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
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 = emptyList<DelegatedSource>().associateBy { it.sourceId }
private val delegatedSources = listOf(
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 {
scope.launch {
@ -45,8 +71,8 @@ class SourceManager(
extensions.forEach { extension ->
extension.sources.forEach {
mutableMap[it.id] = it
//delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource
//registerStubSource(it)
delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource
// registerStubSource(it)
}
}
sourcesMapFlow.value = mutableMap

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
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.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
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.HttpSource
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR
import yokai.util.lang.getString
class Cubari(delegate: HttpSource) :
DelegatedHttpSource(delegate) {
private val getManga: GetManga = Injekt.get()
private val getChapter: GetChapter = Injekt.get()
override val lang = "all"
class Cubari : DelegatedHttpSource() {
override val domainName: String = "cubari"
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 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 cubariPath = uri.pathSegments.getOrNull(2) ?: return null
val chapterNumber = uri.pathSegments.getOrNull(3)?.replace("-", ".")?.toFloatOrNull() ?: return null
val mangaUrl = "/read/$cubariType/$cubariPath"
return withContext(Dispatchers.IO) {
val deferredManga = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl)
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
}
val deferredChapters = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate.id)?.let { manga ->
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!)?.let { manga ->
val chapters = getChapter.awaitAll(manga, false)
val chapter = findChapter(chapters, cubariType, chapterNumber)
if (chapter != null) {
return@async chapters
}
}
getChapterListByUrl(mangaUrl)
getChapters(mangaUrl)
}
val manga = deferredManga.await()
val chapters = deferredChapters.await()
@ -59,7 +50,11 @@ class Cubari(delegate: HttpSource) :
?: error(
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
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.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager
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.HttpSource
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@ -20,15 +20,10 @@ import okhttp3.CacheControl
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR
import yokai.util.lang.getString
class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) {
private val getManga: GetManga = Injekt.get()
override val lang: String = "all"
class MangaDex : DelegatedHttpSource() {
override val domainName: String = "mangadex"
@ -47,13 +42,13 @@ class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) {
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 request =
GET("https:///api.mangadex.org/v2$url", delegate!!.headers, CacheControl.FORCE_NETWORK)
val response = network.client.newCall(request).await()
if (response.code != 200) throw Exception("HTTP error ${response.code}")
val body = response.body.string()
val body = response.body.string().orEmpty()
if (body.isEmpty()) {
throw Exception("Null Response")
}
@ -61,19 +56,28 @@ class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) {
val jsonObject = Json.decodeFromString<MangaDexChapterData>(body)
val dataObject = jsonObject.data ?: throw Exception("Chapter not found")
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/"
return withContext(Dispatchers.IO) {
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 chapters = deferredChapters.await()
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),
)
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
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.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
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.HttpSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import okhttp3.CacheControl
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR
import yokai.util.lang.getString
class MangaPlus(delegate: HttpSource) :
DelegatedHttpSource(delegate) {
private val getManga: GetManga = Injekt.get()
override val lang: String get() = delegate.lang
class MangaPlus : DelegatedHttpSource() {
override val domainName: String = "jumpg-webapi.tokyo-cdn"
private val titleIdRegex =
@ -41,11 +34,11 @@ class MangaPlus(delegate: HttpSource) :
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 request = GET(
chapterUrlTemplate.replace("##", uri.pathSegments[1]),
delegate.headers,
delegate!!.headers,
CacheControl.FORCE_NETWORK,
)
return withContext(Dispatchers.IO) {
@ -60,22 +53,26 @@ class MangaPlus(delegate: HttpSource) :
val trimmedTitle = title.substring(0, title.length - 1)
val mangaUrl = "#/titles/$titleId"
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 chapters = deferredChapters.await()
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),
)
Triple(
trueChapter,
manga.apply {
this.title = trimmedTitle
},
chapters,
)
if (manga != null) {
Triple(
trueChapter,
manga.apply {
this.title = trimmedTitle
},
chapters,
)
} else {
null
}
}
}
}

View file

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

View file

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

View file

@ -20,13 +20,11 @@ import eu.kanade.tachiyomi.util.view.isControllerVisible
abstract class BaseLegacyController<VB : ViewBinding>(bundle: Bundle? = null) :
BaseController(bundle) {
override val shouldHideLegacyAppBar = false
lateinit var binding: VB
val isBindingInitialized get() = this::binding.isInitialized
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
setAppBarVisibility()
showLegacyAppBar()
binding = createBinding(inflater)
binding.root.backgroundColor = binding.root.context.getResourceColor(R.attr.background)
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.view.MenuItem
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.isInvisible
import androidx.core.view.updateLayoutParams
@ -214,7 +212,7 @@ class DownloadBottomSheet @JvmOverloads constructor(
setBottomSheet()
if (presenter.downloadQueueState.value.isEmpty()) {
binding.emptyView.show(
Icons.Filled.FileDownloadOff,
R.drawable.ic_download_off_24dp,
MR.strings.nothing_is_downloading,
)
} 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.timeSpanFromNow
import eu.kanade.tachiyomi.util.system.withDefContext
import java.util.*
import kotlinx.coroutines.runBlocking
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.i18n.MR
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.
@ -117,8 +117,8 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/
fun indexOf(manga: Manga): Int {
return currentItems.indexOfFirst {
if (it is LibraryMangaItem) {
it.manga.manga.id == manga.id
if (it is LibraryItem) {
it.manga.id == manga.id
} else {
false
}
@ -142,7 +142,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/
fun allIndexOf(manga: Manga): List<Int> {
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
} else {
null
@ -164,7 +164,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} else {
val filteredManga = withDefContext { mangas.filter { it.filter(s) } }
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) }
updateDataSet(blankItem ?: emptyList())
} else {
@ -173,7 +173,6 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
}
isLongPressDragEnabled = libraryListener?.canDrag() == true && s.isNullOrBlank()
setItemsPerCategoryMap()
notifyDataSetChanged()
}
private fun getFirstLetter(name: String): String {
@ -203,19 +202,18 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
vibrateOnCategoryChange(item.category.name)
item.category.name
}
is LibraryPlaceholderItem -> {
item.header?.category?.name.orEmpty()
}
is LibraryMangaItem -> {
val text =
is LibraryItem -> {
val text = if (item.manga.isBlank()) {
return item.header?.category?.name.orEmpty()
} else {
when (getSort(position)) {
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
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)
} else {
val title = item.manga.manga.title
val title = item.manga.title
if (preferences.removeArticles().get()) {
title.removeArticles().chop(15)
} else {
@ -224,14 +222,14 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
}
}
LibrarySort.DateFetched -> {
val id = item.manga.manga.id ?: return ""
val id = item.manga.id ?: return ""
// FIXME: Don't do blocking
val history = runBlocking { getChapter.awaitAll(id, false) }
val last = history.maxOfOrNull { it.date_fetch }
context.timeSpanFromNow(MR.strings.fetched_, last ?: 0)
}
LibrarySort.LastRead -> {
val id = item.manga.manga.id ?: return ""
val id = item.manga.id ?: return ""
// FIXME: Don't do blocking
val history = runBlocking { getHistory.awaitAllByMangaId(id) }
val last = history.maxOfOrNull { it.last_read }
@ -258,23 +256,21 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
}
}
LibrarySort.LatestChapter -> {
context.timeSpanFromNow(MR.strings.updated_, item.manga.manga.last_update)
context.timeSpanFromNow(MR.strings.updated_, item.manga.last_update)
}
LibrarySort.DateAdded -> {
context.timeSpanFromNow(MR.strings.added_, item.manga.manga.date_added)
context.timeSpanFromNow(MR.strings.added_, item.manga.date_added)
}
LibrarySort.Title -> {
val title = if (preferences.removeArticles().get()) {
item.manga.manga.title.removeArticles()
item.manga.title.removeArticles()
} else {
item.manga.manga.title
item.manga.title
}
getFirstLetter(title)
}
LibrarySort.Random -> {
context.getString(MR.strings.random)
}
}
}
if (!isSingleCategory) {
vibrateOnCategoryChange(item.header?.category?.name.orEmpty())
}

View file

@ -28,8 +28,6 @@ import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
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.core.animation.doOnEnd
import androidx.core.view.ViewCompat
@ -451,7 +449,7 @@ open class LibraryController(
private fun setActiveCategory() {
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) {
binding.categoryRecycler.setCategories(currentCategory)
@ -521,13 +519,14 @@ open class LibraryController(
}
private fun openRandomManga(global: Boolean) {
val items =
if (global) { presenter.currentLibraryItems } else { adapter.currentItems }
.filterIsInstance<LibraryMangaItem>()
.filter { !it.manga.manga.initialized || it.manga.unread > 0 }
val items = if (global) {
presenter.allLibraryItems
} else {
adapter.currentItems
}.filter { (it is LibraryItem && !it.manga.isBlank() && !it.manga.isHidden() && (!it.manga.initialized || it.manga.unread > 0)) }
if (items.isNotEmpty()) {
val item = items.random() as LibraryMangaItem
openManga(item.manga.manga)
val item = items.random() as LibraryItem
openManga(item.manga)
}
}
@ -558,7 +557,7 @@ open class LibraryController(
}
presenter.groupType = item
shouldScrollToTop = true
presenter.updateLibrary()
presenter.getLibrary()
true
}.show()
}
@ -661,7 +660,7 @@ open class LibraryController(
createActionModeIfNeeded()
}
if (presenter.libraryItemsToDisplay.isNotEmpty() && !isSubClass) {
if (presenter.libraryItems.isNotEmpty() && !isSubClass) {
presenter.restoreLibrary()
if (justStarted) {
val activityBinding = activityBinding ?: return
@ -705,7 +704,7 @@ open class LibraryController(
if (!LibraryUpdateJob.isRunning(context)) {
when {
!presenter.showAllCategories && presenter.groupType == BY_DEFAULT -> {
presenter.currentCategory?.let {
presenter.findCurrentCategory()?.let {
updateLibrary(it)
}
}
@ -903,7 +902,7 @@ open class LibraryController(
}
} else {
val newOffset =
presenter.categories.indexOfFirst { presenter.currentCategoryId == it.id } +
presenter.categories.indexOfFirst { presenter.currentCategory == it.id } +
(if (next) 1 else -1)
if (if (!next) {
newOffset > -1
@ -1012,7 +1011,7 @@ open class LibraryController(
override fun getSpanSize(position: Int): Int {
if (libraryLayout == LibraryItem.LAYOUT_LIST) return managerSpanCount
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
} else {
1
@ -1057,7 +1056,7 @@ open class LibraryController(
if (type.isEnter) {
binding.filterBottomSheet.filterBottomSheet.isVisible = true
if (type == ControllerChangeType.POP_ENTER) {
presenter.updateLibrary()
presenter.getLibrary()
isPoppingIn = true
}
binding.recyclerCover.isClickable = false
@ -1096,7 +1095,7 @@ open class LibraryController(
if (!isBindingInitialized) return
updateFilterSheetY()
if (observeLater) {
presenter.updateLibrary()
presenter.getLibrary()
}
}
@ -1136,7 +1135,7 @@ open class LibraryController(
binding.emptyView.hide()
} else {
binding.emptyView.show(
Icons.Filled.HeartBroken,
R.drawable.ic_heart_off_24dp,
if (hasActiveFilters) {
MR.strings.no_matches_for_filters
} else {
@ -1375,7 +1374,7 @@ open class LibraryController(
setActiveCategory()
return
}
val headerPosition = mAdapter?.indexOf(pos) ?: return
val headerPosition = adapter.indexOf(pos)
if (headerPosition > -1) {
val activityBinding = activityBinding ?: return
val index = adapter.headerItems.indexOf(adapter.getItem(headerPosition))
@ -1409,7 +1408,7 @@ open class LibraryController(
private fun onRefresh() {
showCategories(false)
presenter.updateLibrary()
presenter.getLibrary()
destroyActionModeIfNeeded()
}
@ -1433,14 +1432,14 @@ open class LibraryController(
val isShowAllCategoriesSet = preferences.showAllCategories().get()
if (!query.isNullOrBlank() && this.query.isBlank() && !isShowAllCategoriesSet) {
presenter.forceShowAllCategories = preferences.showAllCategoriesWhenSearchingSingleCategory().get()
presenter.updateLibrary()
presenter.getLibrary()
} else if (query.isNullOrBlank() && this.query.isNotBlank() && !isShowAllCategoriesSet) {
if (!isSubClass) {
preferences.showAllCategoriesWhenSearchingSingleCategory()
.set(presenter.forceShowAllCategories)
}
presenter.forceShowAllCategories = false
presenter.updateLibrary()
presenter.getLibrary()
}
if (query != this.query && !query.isNullOrBlank()) {
@ -1458,7 +1457,7 @@ open class LibraryController(
adapter.removeAllScrollableHeaders()
}
adapter.setFilter(query)
if (presenter.currentLibraryItems.isEmpty()) return true
if (presenter.allLibraryItems.isEmpty()) return true
viewScope.launchUI {
adapter.performFilterAsync()
}
@ -1477,6 +1476,7 @@ open class LibraryController(
}
private fun setSelection(manga: Manga, selected: Boolean) {
if (manga.isBlank()) return
val currentMode = adapter.mode
if (selected) {
if (selectedMangas.add(manga)) {
@ -1526,7 +1526,7 @@ open class LibraryController(
toggleSelection(position)
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 chapter = presenter.getFirstUnread(manga) ?: return
activity.apply {
@ -1542,8 +1542,9 @@ open class LibraryController(
}
private fun toggleSelection(position: Int) {
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
setSelection(item.manga.manga, !adapter.isSelected(position))
val item = adapter.getItem(position) as? LibraryItem ?: return
if (item.manga.isBlank()) return
setSelection(item.manga, !adapter.isSelected(position))
invalidateActionMode()
}
@ -1559,14 +1560,14 @@ open class LibraryController(
* @return true if the item should be selected, false otherwise.
*/
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) {
snack?.dismiss()
lastClickPosition = position
toggleSelection(position)
false
} else {
openManga(item.manga.manga)
openManga(item.manga)
false
}
}
@ -1588,10 +1589,10 @@ open class LibraryController(
*/
override fun onItemLongClick(position: Int) {
val item = adapter.getItem(position)
if (item !is LibraryMangaItem) return
if (item !is LibraryItem) return
snack?.dismiss()
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
view.elevation = 15f.dpToPx
}
@ -1640,14 +1641,14 @@ open class LibraryController(
if (mangaId == null) {
adapter.getHeaderPositions().forEach { adapter.notifyItemChanged(it) }
} else {
presenter.updateLibrary()
presenter.updateManga()
}
}
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()
}
@ -1671,7 +1672,7 @@ open class LibraryController(
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
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
if (toPosition < 1) return false
return (adapter.getItem(toPosition) !is LibraryHeaderItem) && (
@ -1686,18 +1687,18 @@ open class LibraryController(
lastItem = null
isDragging = false
binding.swipeRefresh.isEnabled = true
if (mAdapter == null || adapter.selectedItemCount > 0) {
if (adapter.selectedItemCount > 0) {
lastItemPosition = null
return
}
destroyActionModeIfNeeded()
// if nothing moved
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 libraryItems = getSectionItems(adapter.getSectionHeader(position), item)
.filterIsInstance<LibraryMangaItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id }
.filterIsInstance<LibraryItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
if (newHeader?.category?.id == item.manga.category) {
presenter.rearrangeCategory(item.manga.category, mangaIds)
} else {
@ -1819,7 +1820,7 @@ open class LibraryController(
val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return
if (!category.isDynamic) {
ManageCategoryDialog(category) {
presenter.updateLibrary()
presenter.getLibrary()
}.showDialog(router)
}
}
@ -1829,8 +1830,8 @@ open class LibraryController(
if (category?.isDynamic == false && sortBy == LibrarySort.DragAndDrop.categoryValue) {
val item = adapter.findCategoryHeader(catId) ?: return
val libraryItems = adapter.getSectionItems(item)
.filterIsInstance<LibraryMangaItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id }
.filterIsInstance<LibraryItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
presenter.rearrangeCategory(catId, mangaIds)
} else {
presenter.sortCategory(catId, sortBy)
@ -1912,7 +1913,7 @@ open class LibraryController(
isGone = true
setOnClickListener {
presenter.forceShowAllCategories = !presenter.forceShowAllCategories
presenter.updateLibrary()
presenter.getLibrary()
isSelected = presenter.forceShowAllCategories
}
val pad = 12.dpToPx
@ -2192,7 +2193,7 @@ open class LibraryController(
val activity = activity ?: return
viewScope.launchIO {
selectedMangas.toList().moveCategories(activity) {
presenter.updateLibrary()
presenter.getLibrary()
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.setCards
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.
@ -64,24 +64,23 @@ class LibraryGridHolder(
* @param item the manga item to bind.
*/
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.
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.playButton.transitionName = "library chapter $bindingAdapterPosition transition"
binding.constraintLayout.isVisible = item.manga.manga.id != Long.MIN_VALUE
binding.title.text = item.manga.manga.title.highlightText(item.filter, color)
binding.behindTitle.text = item.manga.manga.title
val mangaColor = item.manga.manga.dominantCoverColors
binding.constraintLayout.isVisible = !item.manga.isBlank()
binding.title.text = item.manga.title.highlightText(item.filter, color)
binding.behindTitle.text = item.manga.title
val mangaColor = item.manga.dominantCoverColors
binding.coverConstraint.backgroundColor = mangaColor?.first ?: itemView.context.getResourceColor(R.attr.background)
binding.behindTitle.setTextColor(
mangaColor?.second ?: itemView.context.getResourceColor(R.attr.colorOnBackground),
)
val authorArtist = if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) {
item.manga.manga.author?.trim() ?: ""
val authorArtist = if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
item.manga.author?.trim() ?: ""
} else {
listOfNotNull(
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() },
item.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
).joinToString(", ")
}
binding.subtitle.text = authorArtist.highlightText(item.filter, color)
@ -102,7 +101,7 @@ class LibraryGridHolder(
// Update the cover.
binding.coverThumbnail.dispose()
setCover(item.manga.manga)
setCover(item.manga)
}
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.setText
import eu.kanade.tachiyomi.util.view.text
import kotlin.random.Random
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.library.LibraryPreferences
import yokai.i18n.MR
import yokai.util.lang.getString
class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
BaseFlexibleViewHolder(view, adapter, true) {
private val libraryPreferences: LibraryPreferences = Injekt.get()
private val binding = LibraryCategoryHeaderItemBinding.bind(view)
val progressDrawableStart = CircularProgressDrawable(itemView.context)
val progressDrawableEnd = CircularProgressDrawable(itemView.context)
@ -186,17 +183,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
binding.categoryTitle.text = categoryName +
if (adapter.showNumber) {
val filteredCount = adapter.currentItems.count {
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
" (${adapter.itemsPerCategory[item.catId]})"
} else { "" }
if (category.sourceId != null) {
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 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.setText(category.sortRes())
@ -276,7 +263,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
val cat = category ?: return
adapter.controller?.activity?.let { activity ->
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(
activity,
items,
@ -285,68 +272,69 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
) { sheet, item ->
onCatSortClicked(cat, item)
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
}
sheet.updateSortIcon(cat, sortingMode)
val isAscending = cat.isAscending()
val drawableRes = getSortRes(sortingMode, isAscending)
sheet.setDrawable(sortingMode?.mainValue ?: -1, drawableRes)
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(
sortMode: LibrarySort?,
isAscending: Boolean,
isDynamic: Boolean,
onSelection: Boolean,
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_sort_24dp,
@DrawableRes defaultSelectedDrawableRes: Int = R.drawable.ic_check_24dp,
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_check_24dp,
): Int {
sortMode ?: return if (onSelection) defaultSelectedDrawableRes else defaultDrawableRes
if (sortMode.isDirectional) {
return if (if (sortMode.hasInvertedSort) !isAscending else isAscending) {
R.drawable.ic_arrow_downward_24dp
} else {
R.drawable.ic_arrow_upward_24dp
sortMode ?: return defaultDrawableRes
return when (sortMode) {
LibrarySort.DragAndDrop -> defaultDrawableRes
else -> {
if (if (sortMode.hasInvertedSort) !isAscending else isAscending) {
R.drawable.ic_arrow_downward_24dp
} else {
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
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
}
}
}
return sortMode.iconRes(isDynamic)
}
private fun onCatSortClicked(category: Category, menuId: Int?) {
val (mode, modType) = if (menuId == null) {
val modType = if (menuId == null) {
val sortingMode = category.sortingMode() ?: LibrarySort.Title
sortingMode to
if (sortingMode != LibrarySort.Random && category.isAscending()) {
sortingMode.categoryValueDescending
} else {
sortingMode.categoryValue
}
if (category.isAscending()) {
sortingMode.categoryValueDescending
} else {
sortingMode.categoryValue
}
} else {
val sortingMode = LibrarySort.valueOf(menuId) ?: LibrarySort.Title
if (sortingMode != LibrarySort.DragAndDrop && sortingMode == category.sortingMode()) {
onCatSortClicked(category, null)
return
}
sortingMode to sortingMode.categoryValue
}
if (mode == LibrarySort.Random) {
libraryPreferences.randomSortSeed().set(Random.nextInt())
sortingMode.categoryValue
}
adapter.libraryListener?.sortCategory(category.id!!, modType)
}

View file

@ -5,6 +5,9 @@ import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import com.google.android.material.card.MaterialCardView
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.util.isLocal
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.
* @param view the inflated view for 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(
view: View,
val adapter: LibraryCategoryAdapter,
@ -38,7 +43,7 @@ abstract class LibraryHolder(
*/
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
badge.setUnreadDownload(
when {
@ -49,7 +54,7 @@ abstract class LibraryHolder(
},
when {
item.downloadCount == -1 -> -1
item.manga.manga.isLocal() -> -2
item.manga.isLocal() -> -2
else -> item.downloadCount
},
showTotal,
@ -58,7 +63,7 @@ abstract class LibraryHolder(
)
}
fun setReadingButton(item: LibraryMangaItem) {
fun setReadingButton(item: LibraryItem) {
itemView.findViewById<View>(R.id.play_layout)?.isVisible =
item.manga.unread > 0 && !item.hideReadingButton
}
@ -75,8 +80,8 @@ abstract class LibraryHolder(
override fun onLongClick(view: View?): Boolean {
return if (adapter.isLongPressDragEnabled) {
val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryMangaItem)?.manga
if (manga != null && !isDraggable) {
val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryItem)?.manga
if (manga != null && !isDraggable && !manga.isBlank() && !manga.isHidden()) {
adapter.mItemLongClickListener.onItemLongClick(flexibleAdapterPosition)
toggleActivation()
true

View file

@ -1,48 +1,205 @@
package eu.kanade.tachiyomi.ui.library
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.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFilterable
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.databinding.MangaGridItemBinding
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 yokai.domain.ui.UiPreferences
abstract class LibraryItem(
class LibraryItem(
val manga: LibraryManga,
header: LibraryHeaderItem,
internal val context: Context?,
private val context: Context?,
) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
var downloadCount = -1
var unreadType = 2
var sourceLanguage: String? = null
var filter = ""
internal val sourceManager: SourceManager by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val uiPreferences: UiPreferences by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
internal val uniformSize: Boolean
private val uniformSize: Boolean
get() = uiPreferences.uniformGrid().get()
internal val libraryLayout: Int
private val libraryLayout: Int
get() = preferences.libraryLayout().get()
val hideReadingButton: Boolean
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(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LibraryHolder,
position: Int,
payloads: MutableList<Any?>?,
) {
if (holder is LibraryGridHolder && !holder.fixedSize) {
holder.setFreeformCoverRatio(manga, adapter.recyclerView as? AutofitRecyclerView)
}
holder.onSetValues(this)
(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 {

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.view.setCards
import yokai.i18n.MR
import yokai.util.coil.loadManga
import yokai.presentation.core.util.coil.loadManga
import yokai.util.lang.getString
/**
@ -39,25 +39,22 @@ class LibraryListHolder(
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.title.isVisible = true
binding.constraintLayout.minHeight = 56.dpToPx
if (item is LibraryPlaceholderItem) {
if (item.manga.isBlank()) {
binding.constraintLayout.minHeight = 0
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = ViewGroup.MarginLayoutParams.WRAP_CONTENT
}
when (item.type) {
is LibraryPlaceholderItem.Type.Blank -> {
binding.title.text = itemView.context.getString(
if (adapter.hasActiveFilters && item.type.mangaCount >= 1) {
MR.strings.no_matches_for_filters_short
} else {
MR.strings.category_is_empty
},
)
}
is LibraryPlaceholderItem.Type.Hidden -> {
binding.title.text = null
binding.title.isVisible = false
}
if (item.manga.status == -1) {
binding.title.text = null
binding.title.isVisible = false
} else {
binding.title.text = itemView.context.getString(
if (adapter.hasActiveFilters && item.manga.realMangaCount >= 1) {
MR.strings.no_matches_for_filters_short
} else {
MR.strings.category_is_empty
},
)
}
binding.title.textAlignment = View.TEXT_ALIGNMENT_CENTER
binding.card.isVisible = false
@ -66,9 +63,6 @@ class LibraryListHolder(
binding.subtitle.isVisible = false
return
}
if (item !is LibraryMangaItem) error("${item::class.qualifiedName} is not a valid item")
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = 52.dpToPx
}
@ -77,16 +71,16 @@ class LibraryListHolder(
binding.title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
// 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)
val authorArtist =
if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) {
item.manga.manga.author?.trim() ?: ""
if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
item.manga.author?.trim() ?: ""
} else {
listOfNotNull(
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() },
item.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
).joinToString(", ")
}
@ -101,7 +95,7 @@ class LibraryListHolder(
// Update the cover.
binding.coverThumbnail.dispose()
binding.coverThumbnail.loadManga(item.manga.manga)
binding.coverThumbnail.loadManga(item.manga)
}
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
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
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(
val mainValue: Int,
@ -30,11 +33,7 @@ enum class LibrarySort(
MR.strings.category,
R.drawable.ic_label_outline_24dp,
),
Random(
8,
MR.strings.random,
R.drawable.ic_shuffle_24dp,
),
;
val categoryValue: Char
@ -51,7 +50,6 @@ enum class LibrarySort(
LatestChapter -> "LATEST_CHAPTER"
DateFetched -> "CHAPTER_FETCH_DATE"
DateAdded -> "DATE_ADDED"
Random -> "RANDOM"
else -> "ALPHABETICAL"
}
return "$type,ASCENDING"
@ -65,9 +63,6 @@ enum class LibrarySort(
val hasInvertedSort: Boolean
get() = this in listOf(LastRead, DateAdded, LatestChapter, DateFetched)
val isDirectional: Boolean
get() = this !in listOf(DragAndDrop, Random)
fun menuSheetItem(isDynamic: Boolean): MaterialMenuSheet.MenuSheetItem {
return MaterialMenuSheet.MenuSheetItem(
mainValue,
@ -90,7 +85,6 @@ enum class LibrarySort(
"LATEST_CHAPTER" -> LatestChapter
"CHAPTER_FETCH_DATE" -> DateFetched
"DATE_ADDED" -> DateAdded
"RANDOM" -> Random
else -> Title
}
} 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.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.util.bindToPreference
import eu.kanade.tachiyomi.util.lang.withSubtitle
import eu.kanade.tachiyomi.util.system.toInt
import eu.kanade.tachiyomi.widget.BaseLibraryDisplayView
import kotlin.math.min
import yokai.i18n.MR
import yokai.util.lang.getString
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
BaseLibraryDisplayView<LibraryCategoryLayoutBinding>(context, attrs) {
@ -18,7 +20,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
override fun initGeneralPreferences() {
with(binding) {
showAll.bindToPreference(preferences.showAllCategories()) {
controller?.presenter?.updateLibrary()
controller?.presenter?.getLibrary()
binding.categoryShow.isEnabled = it
}
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)
.withSubtitle(context, MR.strings.when_grouping_by_sources_tags)
dynamicToBottom.bindToPreference(preferences.collapsedDynamicAtBottom()) {
controller?.presenter?.updateLibrary()
controller?.presenter?.getLibrary()
}
showEmptyCatsFiltering.bindToPreference(preferences.showEmptyCategoriesWhileFiltering()) {
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.ui.library.LibraryController
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.util.system.dpToPx
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.isHidden
import eu.kanade.tachiyomi.util.view.setText
import kotlin.math.max
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop
@ -51,6 +48,8 @@ import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.i18n.MR
import yokai.util.lang.getString
import kotlin.math.max
import kotlin.math.roundToInt
class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
LinearLayout(context, attrs),
@ -369,12 +368,11 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri
suspend fun checkForManhwa(sourceManager: SourceManager) {
if (checked) return
withIOContext {
val libraryManga = controller?.presenter?.currentLibraryItems ?: return@withIOContext
val libraryManga = controller?.presenter?.allLibraryItems ?: return@withIOContext
checked = true
var types = mutableSetOf<StringResource>()
libraryManga.forEach {
if (it !is LibraryMangaItem) return@forEach
when (it.manga.manga.seriesType(sourceManager = sourceManager)) {
when (it.manga.seriesType(sourceManager = sourceManager)) {
Manga.TYPE_MANHWA, Manga.TYPE_WEBTOON -> types.add(MR.strings.manhwa)
Manga.TYPE_MANHUA -> types.add(MR.strings.manhua)
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),
Bookmarked('b', MR.strings.bookmarked),
Tracked('t', MR.strings.tracking),
ContentType('s', MR.strings.content_type)
ContentType('s', MR.strings.content_type);
;
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.DialogController
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.more.AboutController
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.toast
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.backgroundColor
import eu.kanade.tachiyomi.util.view.blurBehindWindow
@ -138,10 +136,12 @@ import kotlin.collections.set
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToLong
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy
import yokai.core.migration.Migrator
import yokai.domain.base.BasePreferences
@ -198,7 +198,6 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
dimenW to dimenH
}
@Deprecated("Create contract directly from Composable")
private val requestNotificationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) {
@ -539,7 +538,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
if (currentRoot?.tag()?.toIntOrNull() != id) {
setRoot(
when (id) {
R.id.nav_library -> if (basePreferences.composeLibrary().get()) LibraryComposeController() else LibraryController()
R.id.nav_library -> LibraryController()
R.id.nav_recents -> RecentsController()
else -> BrowseController()
},
@ -1002,7 +1001,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
}
private fun checkForAppUpdates() {
if (isUpdaterEnabled && router.backstack.lastOrNull()?.controller !is AboutController) {
if (isUpdaterEnabled) {
lifecycleScope.launchIO {
try {
val result = updateChecker.checkForUpdate(this@MainActivity)
@ -1012,7 +1011,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
val isBeta = result.release.preRelease == true
// Create confirmation window
withUIContext {
withContext(Dispatchers.Main) {
showNotificationPermissionPrompt()
AppUpdateNotifier.releasePageUrl = result.release.releaseLink
AboutController.NewUpdateDialogController(body, url, isBeta).showDialog(router)
@ -1038,7 +1037,6 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
@SuppressLint("MissingSuperCall")
override fun onNewIntent(intent: Intent) {
splashState.ready = true
if (!handleIntentAction(intent)) {
super.onNewIntent(intent)
}
@ -1055,13 +1053,13 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
}
when (intent.action) {
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) {
nav.selectedItemId = R.id.nav_recents
} else {
router.popToRoot()
}
if (intent.action == Constants.SHORTCUT_RECENTS) return true
if (intent.action == SHORTCUT_RECENTS) return true
nav.post {
val controller =
router.backstack.firstOrNull()?.controller as? RecentsController
@ -1094,11 +1092,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
SHORTCUT_UPDATE_NOTES -> {
val extras = intent.extras ?: return false
if (router.backstack.isEmpty()) nav.selectedItemId = R.id.nav_library
if (
router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController &&
// FIXME: Show Compose version of NewUpdateDialog for AboutController
router.backstack.lastOrNull()?.controller !is AboutController
) {
if (router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController) {
AboutController.NewUpdateDialogController(extras).showDialog(router)
}
}
@ -1128,6 +1122,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
else -> return false
}
splashState.ready = true
return true
}
@ -1614,12 +1609,20 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
private const val SWIPE_THRESHOLD = 100
private const val SWIPE_VELOCITY_THRESHOLD = 100
const val MAIN_ACTIVITY = Constants.MAIN_ACTIVITY
// Shortcut actions
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_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
const val SHORTCUT_BROWSE = "eu.kanade.tachiyomi.SHOW_BROWSE"
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_SOURCE = "eu.kanade.tachiyomi.SHOW_SOURCE"
const val SHORTCUT_READER_SETTINGS = "eu.kanade.tachiyomi.READER_SETTINGS"

View file

@ -97,7 +97,7 @@ class SearchActivity : MainActivity() {
}
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(
to: Controller?,

View file

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

View file

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

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.view.resetStrokeColor
import yokai.i18n.MR
import yokai.util.coil.loadManga
import yokai.presentation.core.util.coil.loadManga
import yokai.util.lang.getString
import android.R as AR

View file

@ -18,8 +18,6 @@ import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
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.core.net.toUri
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.adapters.ItemAdapter
import com.mikepenz.fastadapter.listeners.addClickListener
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
@ -318,7 +317,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
if (results.isEmpty()) {
setMiddleTrackView(binding.searchEmptyView.id)
binding.searchEmptyView.show(
Icons.Filled.SearchOff,
R.drawable.ic_search_off_24dp,
MR.strings.no_results_found,
)
} else {
@ -339,7 +338,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
binding.trackSearchRecycler.isVisible = false
searchItemAdapter.clear()
binding.searchEmptyView.show(
Icons.Filled.SearchOff,
R.drawable.ic_search_off_24dp,
error.message ?: "",
)
}

View file

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

View file

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

View file

@ -1,41 +1,175 @@
package eu.kanade.tachiyomi.ui.more
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.Bundle
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.CrossfadeTransition
import androidx.core.content.getSystemService
import androidx.core.net.toUri
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.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.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.toast
import eu.kanade.tachiyomi.util.view.setNegativeButton
import eu.kanade.tachiyomi.util.view.setPositiveButton
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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
class AboutController : BaseComposeController() {
class AboutController : SettingsLegacyController() {
@Composable
override fun ScreenContent() {
Navigator(
screen = AboutScreen(),
content = {
CrossfadeTransition(navigator = it)
},
)
/**
* Checks for new releases
*/
private val updateChecker by lazy { AppUpdateChecker() }
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) {
constructor(body: String, url: String, isBeta: Boolean?) : this(
@ -47,7 +181,9 @@ class AboutController : BaseComposeController() {
)
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 isBeta = args.getBoolean(IS_BETA, false)
@ -84,9 +220,19 @@ class AboutController : BaseComposeController() {
const val IS_BETA = "NewUpdateDialogController.is_beta"
}
}
}
fun Context.parseReleaseNotes(releaseNotes: String): Spanned {
val releaseBody = releaseNotes.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
return Markwon.create(this).toMarkdown(releaseBody)
companion object {
fun getFormattedBuildTime(dateFormat: DateFormat): String {
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.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.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.scrollViewWith
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import kotlin.math.roundToInt
import yokai.i18n.MR
import yokai.util.lang.getString
import kotlin.math.roundToInt
import android.R as AR
class StatsController : BaseLegacyController<StatsControllerBinding>() {
@ -61,7 +61,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
}
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)
with(binding) {
viewDetailLayout.isVisible = mangaDistinct.isNotEmpty()
@ -76,8 +76,8 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
}
statsTrackedMangaText.text = mangaTracks.count { it.second.isNotEmpty() }.toString()
statsChaptersDownloadedText.text = mangaDistinct.sumOf { presenter.getDownloadCount(it) }.toString()
statsTotalTagsText.text = mangaDistinct.flatMap { it.manga.getTags() }.distinct().count().toString()
statsMangaLocalText.text = mangaDistinct.count { it.manga.isLocal() }.toString()
statsTotalTagsText.text = mangaDistinct.flatMap { it.getTags() }.distinct().count().toString()
statsMangaLocalText.text = mangaDistinct.count { it.isLocal() }.toString()
statsGlobalUpdateMangaText.text = presenter.getGlobalUpdateManga().count().toString()
statsSourcesText.text = presenter.getSources().count().toString()
statsTrackersText.text = presenter.getLoggedTrackers().count().toString()
@ -105,7 +105,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
val pieEntries = ArrayList<PieEntry>()
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
pieEntries.add(PieEntry(libraryCount.toFloat(), activity!!.mapStatus(status)))
StatusDistributionItem(activity!!.mapStatus(status), libraryCount, color)

View file

@ -65,19 +65,19 @@ class StatsPresenter(
val includedCategories = prefs.libraryUpdateCategories().get().map(String::toInt)
val excludedCategories = prefs.libraryUpdateCategoriesExclude().get().map(String::toInt)
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 } }
.filter { includedCategories.isEmpty() || it.value.any { manga -> manga.category in includedCategories } }
.filterNot {
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_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead)
}
}
fun getDownloadCount(manga: LibraryManga): Int {
return downloadManager.getDownloadCount(manga.manga)
return downloadManager.getDownloadCount(manga)
}
fun get10PointScore(track: Track): Float? {

View file

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

View file

@ -153,7 +153,7 @@ class StatsDetailsPresenter(
private suspend fun setupSeriesType() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.seriesType() }
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.seriesType() }
libraryFormat.forEach { (seriesType, mangaList) ->
currentStats?.add(
@ -173,7 +173,7 @@ class StatsDetailsPresenter(
private suspend fun setupStatus() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.status }
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.status }
libraryFormat.forEach { (status, mangaList) ->
currentStats?.add(
@ -263,7 +263,7 @@ class StatsDetailsPresenter(
private suspend fun setupTrackers() {
currentStats = ArrayList()
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 } }
val loggedServices = trackManager.services.filter { it.isLogged }
@ -292,7 +292,7 @@ class StatsDetailsPresenter(
private suspend fun setupSources() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.source }
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.source }
libraryFormat.forEach { (sourceId, mangaList) ->
val source = sourceManager.getOrStub(sourceId)
@ -339,10 +339,10 @@ class StatsDetailsPresenter(
private suspend fun setupTags() {
currentStats = ArrayList()
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 ->
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
} else {
filter { manga ->
context.mapSeriesType(manga.manga.seriesType()) in selectedSeriesType
context.mapSeriesType(manga.seriesType()) in selectedSeriesType
}
}
}
@ -443,7 +443,7 @@ class StatsDetailsPresenter(
this
} else {
filter { manga ->
context.mapStatus(manga.manga.status) in selectedStatus
context.mapStatus(manga.status) in selectedStatus
}
}
}
@ -463,7 +463,7 @@ class StatsDetailsPresenter(
this
} else {
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
*/
private fun LibraryManga.getLanguage(): String {
val code = if (manga.isLocal()) {
LocalSource.getMangaLang(this.manga)
val code = if (isLocal()) {
LocalSource.getMangaLang(this)
} else {
sourceManager.get(manga.source)?.lang
sourceManager.get(source)?.lang
} ?: return context.getString(MR.strings.unknown)
return LocaleHelper.getLocalizedDisplayName(code)
}
@ -516,7 +516,7 @@ class StatsDetailsPresenter(
* Get mean score rounded to two decimal of a list of manga
*/
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() }
.mapNotNull { it.second.getMeanScoreByTracker() }
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
*/
private suspend fun LibraryManga.getMeanScoreToInt(): Int? {
val mangaTracks = getTracks(this.manga)
val mangaTracks = getTracks(this)
val scoresList = mangaTracks.filter { it.score > 0 }
.mapNotNull { it.get10PointScore() }
return if (scoresList.isEmpty()) null else scoresList.average().roundToInt().coerceIn(1..10)
@ -550,8 +550,8 @@ class StatsDetailsPresenter(
}
private suspend fun LibraryManga.getStartYear(): Int? {
if (getChapter.awaitAll(manga.id!!, false).any { it.read }) {
val chapters = getHistory.awaitAllByMangaId(manga.id!!).filter { it.last_read > 0 }
if (getChapter.awaitAll(id!!, false).any { it.read }) {
val chapters = getHistory.awaitAllByMangaId(id!!).filter { it.last_read > 0 }
val date = chapters.minOfOrNull { it.last_read } ?: return null
val cal = Calendar.getInstance().apply { timeInMillis = date }
return if (date <= 0L) null else cal.get(Calendar.YEAR)
@ -564,7 +564,7 @@ class StatsDetailsPresenter(
}
private fun getEnabledSources(): List<Source> {
return mangasDistinct.mapNotNull { sourceManager.get(it.manga.source) }
return mangasDistinct.mapNotNull { sourceManager.get(it.source) }
.distinct().sortedBy { it.name }
}
@ -589,7 +589,7 @@ class StatsDetailsPresenter(
}
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.widget.doOnEnd
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.Job
import kotlinx.coroutines.delay
@ -175,6 +167,13 @@ import yokai.domain.ui.settings.ReaderPreferences
import yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour
import yokai.i18n.MR
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
/**
@ -513,6 +512,7 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
}
}
}
viewModel.onSaveInstanceState()
super.onSaveInstanceState(outState)
}
@ -1304,13 +1304,13 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
}
override fun onPause() {
viewModel.flushReadTimer()
viewModel.saveCurrentChapterReadingProgress()
super.onPause()
}
override fun onResume() {
super.onResume()
viewModel.restartReadTimer()
viewModel.setReadStartTime()
}
fun reloadChapters(doublePages: Boolean, force: Boolean = false) {
@ -1655,7 +1655,7 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
}
private fun showSetCoverPrompt(page: ReaderPage) {
if (page.status !is Page.State.Ready) return
if (page.status != Page.State.READY) return
materialAlertDialog()
.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.database.models.Chapter
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.orientationType
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 java.util.Date
import java.util.concurrent.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -67,7 +68,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -80,7 +80,6 @@ import yokai.domain.chapter.models.ChapterUpdate
import yokai.domain.download.DownloadPreferences
import yokai.domain.history.interactor.GetHistory
import yokai.domain.history.interactor.UpsertHistory
import yokai.domain.library.LibraryPreferences
import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.interactor.InsertManga
import yokai.domain.manga.interactor.UpdateManga
@ -102,7 +101,6 @@ class ReaderViewModel(
private val chapterFilter: ChapterFilter = Injekt.get(),
private val storageManager: StorageManager = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
) : ViewModel() {
private val getCategories: GetCategories by injectLazy()
private val getChapter: GetChapter by injectLazy()
@ -157,15 +155,12 @@ class ReaderViewModel(
private var finished = false
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 var chapterItems = emptyList<ReaderChapterItem>()
private var scope = CoroutineScope(Job() + Dispatchers.Default)
private var hasTrackers: Boolean = false
private suspend fun checkTrackers(manga: Manga) = getTrack.awaitAllByMangaId(manga.id).isNotEmpty()
@ -196,12 +191,24 @@ class ReaderViewModel(
val currentChapters = state.value.viewerChapters
if (currentChapters != null) {
currentChapters.unref()
saveReadingProgress(currentChapters.currChapter)
chapterToDownload?.let {
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.
*/
@ -300,7 +307,6 @@ class ReaderViewModel(
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) {
val host = url.host ?: return
val context = Injekt.get<Application>()
@ -308,7 +314,9 @@ class ReaderViewModel(
context.getString(MR.strings.source_not_installed),
)
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) {
val dbChapter = getChapter.awaitAllByUrl(chapterUrl, false).find {
val source = getManga.awaitById(it.manga_id!!)?.source ?: return@find false
@ -326,9 +334,7 @@ class ReaderViewModel(
}
val info = delegatedSource.fetchMangaFromChapterUrl(url)
if (info != null) {
val (sChapter, sManga, chapters) = info
val manga = Manga.create(sManga.url, sManga.title, sourceId).apply { copyFrom(sManga) }
val chapter = Chapter.create().apply { copyFrom(sChapter) }
val (chapter, manga, chapters) = info
val id = insertManga.await(manga)
manga.id = 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.
* 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
viewModelScope.launchIO {
Logger.d { "Loading ${chapter.chapter.url}" }
flushReadTimer()
restartReadTimer()
Logger.d { "Loading ${chapter.chapter.url}" }
withIOContext {
try {
loadChapter(loader, chapter)
} catch (e: Throwable) {
@ -506,15 +509,28 @@ class ReaderViewModel(
val selectedChapter = page.chapter
// Save last page read and mark as read if needed
viewModelScope.launchNonCancellableIO {
saveChapterProgress(selectedChapter, page, hasExtraPage)
selectedChapter.chapter.last_page_read = page.index
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) {
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 inDownloadRange = page.number.toDouble() / pages.size > 0.2
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).
* 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
getChapter.awaitById(readerChapter.chapter.id!!)?.let { dbChapter ->
readerChapter.chapter.bookmark = dbChapter.bookmark
}
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)
}
if (!preferences.incognitoMode().get() || hasTrackers) {
updateChapter.await(
ChapterUpdate(
id = readerChapter.chapter.id!!,
@ -636,56 +652,24 @@ 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.
*/
private suspend fun saveChapterHistory(readerChapter: ReaderChapter) {
if (preferences.incognitoMode().get()) return
val endTime = Date().time
val sessionReadDuration = chapterReadStartTime?.let { endTime - it } ?: 0
val history = History.create(readerChapter.chapter).apply {
last_read = endTime
time_read = sessionReadDuration
if (!preferences.incognitoMode().get()) {
val readAt = Date().time
val sessionReadDuration = chapterReadStartTime?.let { readAt - it } ?: 0
val history = History.create(readerChapter.chapter).apply {
last_read = readAt
time_read = sessionReadDuration
}
upsertHistory.await(history)
chapterReadStartTime = null
}
upsertHistory.await(history)
chapterReadStartTime = null
}
fun setReadStartTime() {
chapterReadStartTime = Date().time
}
/**
@ -860,7 +844,7 @@ class ReaderViewModel(
* There's also a notification to allow sharing the image somewhere else or deleting it.
*/
fun saveImage(page: ReaderPage) {
if (page.status !is Page.State.Ready) return
if (page.status != Page.State.READY) return
val manga = manga ?: return
val context = Injekt.get<Application>()
@ -890,9 +874,9 @@ class ReaderViewModel(
}
fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
viewModelScope.launch {
if (firstPage.status !is Page.State.Ready) return@launch
if (secondPage.status !is Page.State.Ready) return@launch
scope.launch {
if (firstPage.status != Page.State.READY) return@launch
if (secondPage.status != Page.State.READY) return@launch
val manga = manga ?: return@launch
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.
*/
fun shareImage(page: ReaderPage) {
if (page.status !is Page.State.Ready) return
if (page.status != Page.State.READY) return
val manga = manga ?: return
val context = Injekt.get<Application>()
@ -939,9 +923,9 @@ class ReaderViewModel(
}
fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
viewModelScope.launch {
if (firstPage.status !is Page.State.Ready) return@launch
if (secondPage.status !is Page.State.Ready) return@launch
scope.launch {
if (firstPage.status != Page.State.READY) return@launch
if (secondPage.status != Page.State.READY) return@launch
val manga = manga ?: return@launch
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.
*/
fun setAsCover(page: ReaderPage) {
if (page.status !is Page.State.Ready) return
if (page.status != Page.State.READY) return
val manga = manga ?: 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() {
override val isLocal: Boolean = true
/**
* Recycles this loader and the open archive.
*/
@ -31,7 +29,7 @@ internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader
.mapIndexed { i, entry ->
ReaderPage(i).apply {
stream = { reader.getInputStream(entry.name)!! }
status = Page.State.Ready
status = Page.State.READY
}
}
.toList()

View file

@ -10,8 +10,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.util.system.withIOContext
import yokai.core.archive.util.archiveReader
import yokai.core.archive.util.epubReader
import yokai.core.archive.archiveReader
import yokai.i18n.MR
import yokai.util.lang.getString
@ -83,7 +82,7 @@ class ChapterLoader(
when (format) {
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
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))

View file

@ -11,8 +11,6 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
*/
class DirectoryPageLoader(val file: UniFile) : PageLoader() {
override val isLocal: Boolean = true
/**
* Returns the pages found on this directory ordered with a natural comparator.
*/
@ -24,7 +22,7 @@ class DirectoryPageLoader(val file: UniFile) : PageLoader() {
val streamFn = { file.openInputStream() }
ReaderPage(i).apply {
stream = streamFn
status = Page.State.Ready
status = Page.State.READY
}
} ?: emptyList()
}

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