mirror of
https://github.com/mihonapp/mihon.git
synced 2025-08-25 07:31:31 +02:00
Compare commits
69 Commits
1de161b26c
...
e392d10e69
Author | SHA1 | Date | |
---|---|---|---|
|
e392d10e69 | ||
|
2e800f90b6 | ||
|
e75488f5d9 | ||
|
3838dbcf08 | ||
|
b3ca097e5a | ||
|
70c2443e82 | ||
|
c0a888807b | ||
|
34930920a5 | ||
|
6682b5dd39 | ||
|
3c5f4a317a | ||
|
6a2bfd5e87 | ||
|
ef6cad58fe | ||
|
7e9340aa7f | ||
|
3f2c8e9ef6 | ||
|
a29870c01e | ||
|
583aa430ba | ||
|
0ea0138a73 | ||
|
59bedb33ff | ||
|
ebee275110 | ||
|
015d9b3bd0 | ||
|
f2ccfb0817 | ||
|
1b60c5f0f4 | ||
|
0a91b57f67 | ||
|
bcdf17fe27 | ||
|
a08e03f5cb | ||
|
f087135876 | ||
|
f66f52c244 | ||
|
0d6f426dbd | ||
|
edd7d0522c | ||
|
4ae9dbe524 | ||
|
402e579a69 | ||
|
d0e64d3a66 | ||
|
154f4d327c | ||
|
d8b9a9f593 | ||
|
b7e091d5d0 | ||
|
31e052ac15 | ||
|
60480686da | ||
|
d6ba3c8249 | ||
|
c56f4665ef | ||
|
b51a0a38bd | ||
|
f72b6e4d7c | ||
|
84984ef7e1 | ||
|
9f48def1e2 | ||
|
e83bfb0d35 | ||
|
0301362430 | ||
|
9d5978aca0 | ||
|
4bfc5e7b51 | ||
|
5859a8bbf6 | ||
|
802a2c5c1e | ||
|
c1c1746985 | ||
|
4fcbd80a8e | ||
|
16969193c7 | ||
|
55637ddfe1 | ||
|
e50358dc4b | ||
|
1e40199b7d | ||
|
410b918b77 | ||
|
a4f5dfab1a | ||
|
085147b15b | ||
|
085ad8d446 | ||
|
9254079957 | ||
|
7974a1fc0c | ||
|
1521c35941 | ||
|
2247f6004a | ||
|
c15f3f2fd5 | ||
|
21020e1797 | ||
|
7edecae57f | ||
|
07f963d5ae | ||
|
ab02568ac6 | ||
|
617bf491ee |
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -53,7 +53,7 @@ body:
|
||||
label: Mihon version
|
||||
description: You can find your Mihon version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.16.3"
|
||||
Example: "0.16.4"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -96,7 +96,7 @@ body:
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.16.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.16.4](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -31,7 +31,7 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.16.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.16.4](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
2
.github/renovate.json5
vendored
2
.github/renovate.json5
vendored
@@ -3,7 +3,7 @@
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"schedule": ["every sunday"],
|
||||
"schedule": ["every friday"],
|
||||
"labels": ["Dependencies"],
|
||||
"packageRules": [
|
||||
{
|
||||
|
10
.github/workflows/build_pull_request.yml
vendored
10
.github/workflows/build_pull_request.yml
vendored
@@ -20,22 +20,22 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v2
|
||||
uses: gradle/wrapper-validation-action@b231772637bb498f11fdbc86052b6e8a8dc9fc92 # v2.1.2
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@0fa40c3c10055986a88de3baa0d6ec17c5a894b3 # v4.2.3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
|
||||
|
||||
- name: Build app and run unit tests
|
||||
run: ./gradlew detekt assembleStandardRelease testReleaseUnitTest
|
||||
|
12
.github/workflows/build_push.yml
vendored
12
.github/workflows/build_push.yml
vendored
@@ -17,23 +17,23 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v2
|
||||
uses: gradle/wrapper-validation-action@b231772637bb498f11fdbc86052b6e8a8dc9fc92 # v2.1.2
|
||||
|
||||
- name: Setup Android SDK
|
||||
run: |
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
|
||||
|
||||
- name: Build app and run unit tests
|
||||
run: ./gradlew detekt assembleStandardRelease testReleaseUnitTest
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Sign APK
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/standard/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v2.0.4
|
||||
with:
|
||||
tag_name: ${{ env.VERSION_TAG }}
|
||||
name: Mihon ${{ env.VERSION_TAG }}
|
||||
|
14
.github/workflows/issue_moderator.yml
vendored
14
.github/workflows/issue_moderator.yml
vendored
@@ -11,28 +11,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v2.6.0
|
||||
uses: keiyoushi/issue-moderator-action@a017be83547db6e107431ce7575f53c1dfa3296a
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
duplicate-label: Duplicate
|
||||
|
||||
auto-close-rules: |
|
||||
[
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template was not filled out."
|
||||
},
|
||||
{
|
||||
"type": "both",
|
||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||
"ignoreCase": true,
|
||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||
"message": "Mihon does not support anime, and has no plans to support anime. In addition Mihon is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||
},
|
||||
{
|
||||
"type": "both",
|
||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '2'
|
||||
|
@@ -22,8 +22,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "app.mihon"
|
||||
|
||||
versionCode = 4
|
||||
versionName = "0.16.3"
|
||||
versionCode = 6
|
||||
versionName = "0.16.4"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -284,7 +284,7 @@ tasks {
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
@@ -304,6 +304,12 @@ tasks {
|
||||
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
||||
)
|
||||
}
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.9
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:nonSkippingGroupOptimization=true",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
13
app/src/main/java/eu/kanade/core/util/SourceUtil.kt
Normal file
13
app/src/main/java/eu/kanade/core/util/SourceUtil.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun ifSourcesLoaded(): Boolean {
|
||||
return remember { Injekt.get<SourceManager>().isInitialized }.collectAsState().value
|
||||
}
|
@@ -28,4 +28,6 @@ class BasePreferences(
|
||||
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||
PRIVATE(MR.strings.ext_installer_private, false),
|
||||
}
|
||||
|
||||
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
||||
}
|
||||
|
@@ -95,7 +95,13 @@ fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||
/**
|
||||
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||
*/
|
||||
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories: List<String>?) = ComicInfo(
|
||||
fun getComicInfo(
|
||||
manga: Manga,
|
||||
chapter: Chapter,
|
||||
urls: List<String>,
|
||||
categories: List<String>?,
|
||||
sourceName: String,
|
||||
) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
||||
@@ -105,7 +111,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories:
|
||||
ComicInfo.Number(it.toString())
|
||||
}
|
||||
},
|
||||
web = ComicInfo.Web(chapterUrl),
|
||||
web = ComicInfo.Web(urls.joinToString(" ")),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
@@ -115,6 +121,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories:
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
|
||||
source = ComicInfo.SourceMihon(sourceName),
|
||||
inker = null,
|
||||
colorist = null,
|
||||
letterer = null,
|
||||
|
@@ -21,7 +21,7 @@ class TrackChapter(
|
||||
private val delayedTrackingStore: DelayedTrackingStore,
|
||||
) {
|
||||
|
||||
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) {
|
||||
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) {
|
||||
withNonCancellableContext {
|
||||
val tracks = getTracks.await(mangaId)
|
||||
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||
@@ -43,7 +43,9 @@ class TrackChapter(
|
||||
delayedTrackingStore.remove(track.id)
|
||||
} catch (e: Exception) {
|
||||
delayedTrackingStore.add(track.id, chapterNumber)
|
||||
DelayedTrackingUpdateJob.setupTask(context)
|
||||
if (setupJobOnFailure) {
|
||||
DelayedTrackingUpdateJob.setupTask(context)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
@@ -45,7 +45,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
|
||||
logcat(LogPriority.DEBUG) {
|
||||
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
|
||||
}
|
||||
trackChapter.await(context, track.mangaId, track.lastChapterRead)
|
||||
trackChapter.await(context, track.mangaId, track.lastChapterRead, setupJobOnFailure = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -25,7 +25,7 @@ import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import eu.kanade.domain.source.model.icon
|
||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -29,7 +30,6 @@ import androidx.compose.ui.util.fastForEachIndexed
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.components.material.TabText
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@@ -78,9 +78,8 @@ fun TabbedDialog(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
state = pagerState,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) { page ->
|
||||
content(page)
|
||||
}
|
||||
pageContent = { page -> content(page) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
@@ -24,7 +25,6 @@ import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.TabText
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -22,7 +23,6 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
|
@@ -1,16 +1,33 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.SwapVert
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.components.AdaptiveSheet
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
@@ -18,42 +35,92 @@ fun DuplicateMangaDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
onOpenManga: () -> Unit,
|
||||
onMigrate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AlertDialog(
|
||||
val minHeight = LocalPreferenceMinHeight.current
|
||||
|
||||
AdaptiveSheet(
|
||||
modifier = modifier,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.are_you_sure))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.confirm_add_duplicate_manga))
|
||||
},
|
||||
confirmButton = {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = TabbedDialogPaddings.Vertical,
|
||||
horizontal = TabbedDialogPaddings.Horizontal,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(TitlePadding),
|
||||
text = stringResource(MR.strings.are_you_sure),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(MR.strings.confirm_add_duplicate_manga),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(PaddingSize))
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_show_manga),
|
||||
icon = Icons.Outlined.Book,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onOpenManga()
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_migrate_duplicate),
|
||||
icon = Icons.Outlined.SwapVert,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onMigrate()
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_add_anyway),
|
||||
icon = Icons.Outlined.Add,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onConfirm()
|
||||
},
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.sizeIn(minHeight = minHeight)
|
||||
.clickable { onDismissRequest.invoke() }
|
||||
.padding(ButtonPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onOpenManga()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_show_manga))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onConfirm()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_add))
|
||||
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp),
|
||||
text = stringResource(MR.strings.action_cancel),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val PaddingSize = 16.dp
|
||||
|
||||
private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
|
||||
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
|
||||
|
@@ -2,7 +2,6 @@ package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -24,8 +23,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -91,6 +91,7 @@ private fun NotDownloadedIndicator(
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.commonClickable(
|
||||
enabled = enabled,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
|
||||
onClick = { onClick(ChapterDownloadAction.START) },
|
||||
)
|
||||
@@ -120,6 +121,7 @@ private fun DownloadingIndicator(
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.commonClickable(
|
||||
enabled = enabled,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
|
||||
onClick = { isMenuExpanded = true },
|
||||
),
|
||||
@@ -136,6 +138,8 @@ private fun DownloadingIndicator(
|
||||
modifier = IndicatorModifier,
|
||||
color = strokeColor,
|
||||
strokeWidth = IndicatorStrokeWidth,
|
||||
trackColor = Color.Transparent,
|
||||
strokeCap = StrokeCap.Butt,
|
||||
)
|
||||
} else {
|
||||
val animatedProgress by animateFloatAsState(
|
||||
@@ -153,6 +157,9 @@ private fun DownloadingIndicator(
|
||||
modifier = IndicatorModifier,
|
||||
color = strokeColor,
|
||||
strokeWidth = IndicatorSize / 2,
|
||||
trackColor = Color.Transparent,
|
||||
strokeCap = StrokeCap.Butt,
|
||||
gapSize = 0.dp,
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
|
||||
@@ -192,6 +199,7 @@ private fun DownloadedIndicator(
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.commonClickable(
|
||||
enabled = enabled,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
onLongClick = { isMenuExpanded = true },
|
||||
onClick = { isMenuExpanded = true },
|
||||
),
|
||||
@@ -226,6 +234,7 @@ private fun ErrorIndicator(
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.commonClickable(
|
||||
enabled = enabled,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
onLongClick = { onClick(ChapterDownloadAction.START) },
|
||||
onClick = { onClick(ChapterDownloadAction.START) },
|
||||
),
|
||||
@@ -242,26 +251,23 @@ private fun ErrorIndicator(
|
||||
|
||||
private fun Modifier.commonClickable(
|
||||
enabled: Boolean,
|
||||
hapticFeedback: HapticFeedback,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) = composed {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
Modifier.combinedClickable(
|
||||
enabled = enabled,
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = onClick,
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(
|
||||
bounded = false,
|
||||
radius = IconButtonTokens.StateLayerSize / 2,
|
||||
),
|
||||
)
|
||||
}
|
||||
) = this.combinedClickable(
|
||||
enabled = enabled,
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = onClick,
|
||||
role = Role.Button,
|
||||
interactionSource = null,
|
||||
indication = ripple(
|
||||
bounded = false,
|
||||
radius = IconButtonTokens.StateLayerSize / 2,
|
||||
),
|
||||
)
|
||||
|
||||
private val IndicatorSize = 26.dp
|
||||
private val IndicatorPadding = 2.dp
|
||||
|
@@ -24,36 +24,27 @@ import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.platform.ViewConfiguration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
import me.saket.swipe.rememberSwipeableActionsState
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ReadItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.selectedBackground
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun MangaChapterListItem(
|
||||
@@ -75,142 +66,117 @@ fun MangaChapterListItem(
|
||||
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val textAlpha = if (read) ReadItemAlpha else 1f
|
||||
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
|
||||
|
||||
// Increase touch slop of swipe action to reduce accidental trigger
|
||||
val configuration = LocalViewConfiguration.current
|
||||
CompositionLocalProvider(
|
||||
LocalViewConfiguration provides object : ViewConfiguration by configuration {
|
||||
override val touchSlop: Float = configuration.touchSlop * 3f
|
||||
},
|
||||
val start = getSwipeAction(
|
||||
action = chapterSwipeStartAction,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
downloadState = downloadStateProvider(),
|
||||
background = MaterialTheme.colorScheme.primaryContainer,
|
||||
onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
|
||||
)
|
||||
val end = getSwipeAction(
|
||||
action = chapterSwipeEndAction,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
downloadState = downloadStateProvider(),
|
||||
background = MaterialTheme.colorScheme.primaryContainer,
|
||||
onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
|
||||
)
|
||||
|
||||
SwipeableActionsBox(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
startActions = listOfNotNull(start),
|
||||
endActions = listOfNotNull(end),
|
||||
swipeThreshold = swipeActionThreshold,
|
||||
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||
) {
|
||||
val start = getSwipeAction(
|
||||
action = chapterSwipeStartAction,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
downloadState = downloadStateProvider(),
|
||||
background = MaterialTheme.colorScheme.primaryContainer,
|
||||
onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
|
||||
)
|
||||
val end = getSwipeAction(
|
||||
action = chapterSwipeEndAction,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
downloadState = downloadStateProvider(),
|
||||
background = MaterialTheme.colorScheme.primaryContainer,
|
||||
onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
|
||||
)
|
||||
|
||||
val swipeableActionsState = rememberSwipeableActionsState()
|
||||
LaunchedEffect(Unit) {
|
||||
// Haptic effect when swipe over threshold
|
||||
val swipeActionThresholdPx = with(density) { swipeActionThreshold.toPx() }
|
||||
snapshotFlow { swipeableActionsState.offset.value.absoluteValue > swipeActionThresholdPx }
|
||||
.collect { if (it) haptic.performHapticFeedback(HapticFeedbackType.LongPress) }
|
||||
}
|
||||
|
||||
SwipeableActionsBox(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
state = swipeableActionsState,
|
||||
startActions = listOfNotNull(start),
|
||||
endActions = listOfNotNull(end),
|
||||
swipeThreshold = swipeActionThreshold,
|
||||
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||
Row(
|
||||
modifier = modifier
|
||||
.selectedBackground(selected)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.selectedBackground(selected)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
var textHeight by remember { mutableIntStateOf(0) }
|
||||
if (!read) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Circle,
|
||||
contentDescription = stringResource(MR.strings.unread),
|
||||
modifier = Modifier
|
||||
.height(8.dp)
|
||||
.padding(end = 4.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Bookmark,
|
||||
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = LocalContentColor.current.copy(alpha = textAlpha),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
var textHeight by remember { mutableIntStateOf(0) }
|
||||
if (!read) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Circle,
|
||||
contentDescription = stringResource(MR.strings.unread),
|
||||
modifier = Modifier
|
||||
.height(8.dp)
|
||||
.padding(end = 4.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Bookmark,
|
||||
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = LocalContentColor.current.copy(alpha = textAlpha),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
)
|
||||
}
|
||||
|
||||
Row {
|
||||
ProvideTextStyle(
|
||||
value = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontSize = 12.sp,
|
||||
color = LocalContentColor.current.copy(alpha = textSubtitleAlpha),
|
||||
),
|
||||
) {
|
||||
if (date != null) {
|
||||
Text(
|
||||
text = date,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (readProgress != null || scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (readProgress != null) {
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (scanlator != null) {
|
||||
Text(
|
||||
text = scanlator,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.alpha(textSubtitleAlpha)) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
if (date != null) {
|
||||
Text(
|
||||
text = date,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (readProgress != null || scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (readProgress != null) {
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (scanlator != null) {
|
||||
Text(
|
||||
text = scanlator,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChapterDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = { onDownloadClick?.invoke(it) },
|
||||
)
|
||||
}
|
||||
|
||||
ChapterDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = { onDownloadClick?.invoke(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
|
@@ -37,10 +37,10 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import coil3.imageLoader
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.size.Size
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
@@ -168,7 +168,9 @@ fun MangaCoverDialog(
|
||||
.data(coverDataProvider())
|
||||
.size(Size.ORIGINAL)
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.target { drawable ->
|
||||
.target { image ->
|
||||
val drawable = image.asDrawable(view.context.resources)
|
||||
|
||||
// Copy bitmap in case it came from memory cache
|
||||
// Because SSIV needs to thoroughly read the image
|
||||
val copy = (drawable as? BitmapDrawable)?.let {
|
||||
|
@@ -73,7 +73,7 @@ import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
@@ -6,6 +6,8 @@ import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.webkit.WebStorage
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -123,6 +125,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
getDataGroup(),
|
||||
getNetworkGroup(networkPreferences = networkPreferences),
|
||||
getLibraryGroup(),
|
||||
getReaderGroup(basePreferences = basePreferences),
|
||||
getExtensionsGroup(basePreferences = basePreferences),
|
||||
)
|
||||
}
|
||||
@@ -313,6 +316,34 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getReaderGroup(
|
||||
basePreferences: BasePreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val chooseColorProfile = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
basePreferences.displayProfile().set(uri.toString())
|
||||
}
|
||||
}
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_reader),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_display_profile),
|
||||
subtitle = basePreferences.displayProfile().get(),
|
||||
onClick = {
|
||||
chooseColorProfile.launch(arrayOf("*/*"))
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getExtensionsGroup(
|
||||
basePreferences: BasePreferences,
|
||||
|
@@ -29,6 +29,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val readerPref = remember { Injekt.get<ReaderPreferences>() }
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.defaultReadingMode(),
|
||||
@@ -56,11 +57,6 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_show_navigation_mode),
|
||||
subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.trueColor(),
|
||||
title = stringResource(MR.strings.pref_true_color),
|
||||
subtitle = stringResource(MR.strings.pref_true_color_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.pageTransitions(),
|
||||
title = stringResource(MR.strings.pref_page_transitions),
|
||||
@@ -341,7 +337,10 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||
title = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
enabled = true,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDisableZoomOut(),
|
||||
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@@ -32,12 +32,13 @@ class OpenSourceLicensesScreen : Screen() {
|
||||
.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
onLibraryClick = {
|
||||
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
||||
name = it.library.name,
|
||||
website = it.library.website,
|
||||
license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||
navigator.push(
|
||||
OpenSourceLibraryLicenseScreen(
|
||||
name = it.name,
|
||||
website = it.website,
|
||||
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||
)
|
||||
)
|
||||
navigator.push(libraryLicenseScreen)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@@ -197,6 +197,10 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
||||
label = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
pref = screenModel.preferences.webtoonDisableZoomOut(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@@ -173,7 +173,6 @@ internal fun UpdateUpcomingSmallContent(
|
||||
onClick = onClickUpcoming,
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
@@ -285,7 +284,6 @@ internal fun UpdateUpcomingLargeContent(
|
||||
onClick = onClickUpcoming,
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
@@ -299,6 +297,5 @@ internal fun UpdateUpcomingLargeContent(
|
||||
|
||||
sealed interface UpcomingUIModel {
|
||||
data class Header(val date: LocalDate) : UpcomingUIModel
|
||||
|
||||
data class Item(val item: Manga) : UpcomingUIModel
|
||||
}
|
||||
|
@@ -121,7 +121,7 @@ private fun CalendarGrid(
|
||||
val firstDayOfMonth = startDayOfMonth.dayOfWeek
|
||||
|
||||
val dayEntries = (getFirstDayOfMonth(firstDayOfMonth)..daysInMonth).toImmutableList()
|
||||
val height = (((((dayEntries.size - 1) / DaysOfWeek) + ceil(1.0f - widthModifier)) * HeightMultiplier)).dp
|
||||
val height = (((dayEntries.size - 1) / DaysOfWeek + ceil(1.0f - widthModifier)) * HeightMultiplier).dp
|
||||
|
||||
val modeModifier = if (isTabletUi) {
|
||||
modifier
|
||||
|
@@ -15,12 +15,14 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.disk.DiskCache
|
||||
import coil.util.DebugLogger
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.disk.directory
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.request.crossfade
|
||||
import coil3.util.DebugLogger
|
||||
import eu.kanade.domain.DomainModule
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
@@ -58,7 +60,7 @@ import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.security.Security
|
||||
|
||||
class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
|
||||
|
||||
private val basePreferences: BasePreferences by injectLazy()
|
||||
private val networkPreferences: NetworkPreferences by injectLazy()
|
||||
@@ -131,24 +133,19 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
}
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
override fun newImageLoader(context: Context): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
|
||||
val diskCacheInit = { CoilDiskCache.get(this@App) }
|
||||
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
|
||||
val diskCacheLazy = lazy { CoilDiskCache.get(this@App) }
|
||||
components {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
||||
add(TachiyomiImageDecoder.Factory())
|
||||
add(MangaCoverFetcher.MangaFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||
add(MangaCoverFetcher.MangaCoverFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||
add(MangaCoverFetcher.MangaFactory(callFactoryLazy, diskCacheLazy))
|
||||
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy, diskCacheLazy))
|
||||
add(MangaKeyer())
|
||||
add(MangaCoverKeyer())
|
||||
}
|
||||
callFactory(callFactoryInit)
|
||||
diskCache(diskCacheInit)
|
||||
diskCache(diskCacheLazy::value)
|
||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
||||
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
||||
@@ -156,7 +153,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
// Coil spawns a new thread for every image load by default
|
||||
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
|
||||
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
}.build()
|
||||
}
|
||||
|
||||
|
@@ -98,4 +98,5 @@ private fun Manga.toBackupManga() =
|
||||
updateStrategy = this.updateStrategy,
|
||||
lastModifiedAt = this.lastModifiedAt,
|
||||
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||
version = this.version,
|
||||
)
|
||||
|
@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Serializable
|
||||
data class BackupChapter(
|
||||
// in 1.x some of these values have different names
|
||||
@@ -21,6 +22,7 @@ data class BackupChapter(
|
||||
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
||||
@ProtoNumber(10) var sourceOrder: Long = 0,
|
||||
@ProtoNumber(11) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(12) var version: Long = 0,
|
||||
) {
|
||||
fun toChapterImpl(): Chapter {
|
||||
return Chapter.create().copy(
|
||||
@@ -35,6 +37,7 @@ data class BackupChapter(
|
||||
dateUpload = this@BackupChapter.dateUpload,
|
||||
sourceOrder = this@BackupChapter.sourceOrder,
|
||||
lastModifiedAt = this@BackupChapter.lastModifiedAt,
|
||||
version = this@BackupChapter.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -53,6 +56,8 @@ val backupChapterMapper = {
|
||||
dateFetch: Long,
|
||||
dateUpload: Long,
|
||||
lastModifiedAt: Long,
|
||||
version: Long,
|
||||
_: Long,
|
||||
->
|
||||
BackupChapter(
|
||||
url = url,
|
||||
@@ -66,5 +71,6 @@ val backupChapterMapper = {
|
||||
dateUpload = dateUpload,
|
||||
sourceOrder = sourceOrder,
|
||||
lastModifiedAt = lastModifiedAt,
|
||||
version = version,
|
||||
)
|
||||
}
|
||||
|
@@ -5,7 +5,10 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Suppress(
|
||||
"DEPRECATION",
|
||||
"MagicNumber",
|
||||
)
|
||||
@Serializable
|
||||
data class BackupManga(
|
||||
// in 1.x some of these values have different names
|
||||
@@ -39,6 +42,7 @@ data class BackupManga(
|
||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
||||
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
|
||||
@ProtoNumber(109) var version: Long = 0,
|
||||
) {
|
||||
fun getMangaImpl(): Manga {
|
||||
return Manga.create().copy(
|
||||
@@ -58,6 +62,7 @@ data class BackupManga(
|
||||
updateStrategy = this@BackupManga.updateStrategy,
|
||||
lastModifiedAt = this@BackupManga.lastModifiedAt,
|
||||
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
||||
version = this@BackupManga.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ class MangaRestorer(
|
||||
}
|
||||
|
||||
private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga {
|
||||
return if (manga.lastModifiedAt > dbManga.lastModifiedAt) {
|
||||
return if (manga.version > dbManga.version) {
|
||||
updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id))
|
||||
} else {
|
||||
updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id))
|
||||
@@ -100,6 +100,7 @@ class MangaRestorer(
|
||||
thumbnailUrl = newer.thumbnailUrl,
|
||||
status = newer.status,
|
||||
initialized = this.initialized || newer.initialized,
|
||||
version = newer.version,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,6 +127,8 @@ class MangaRestorer(
|
||||
dateAdded = manga.dateAdded,
|
||||
mangaId = manga.id,
|
||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||
version = manga.version,
|
||||
isSyncing = 1,
|
||||
)
|
||||
}
|
||||
return manga
|
||||
@@ -137,6 +140,7 @@ class MangaRestorer(
|
||||
return manga.copy(
|
||||
initialized = manga.description != null,
|
||||
id = insertManga(manga),
|
||||
version = manga.version,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -183,7 +187,7 @@ class MangaRestorer(
|
||||
}
|
||||
|
||||
private fun Chapter.forComparison() =
|
||||
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L)
|
||||
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L)
|
||||
|
||||
private suspend fun insertNewChapters(chapters: List<Chapter>) {
|
||||
handler.await(true) {
|
||||
@@ -200,6 +204,7 @@ class MangaRestorer(
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
chapter.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -221,6 +226,8 @@ class MangaRestorer(
|
||||
dateFetch = null,
|
||||
dateUpload = null,
|
||||
chapterId = chapter.id,
|
||||
version = chapter.version,
|
||||
isSyncing = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -253,6 +260,7 @@ class MangaRestorer(
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
version = manga.version,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
|
@@ -1,19 +1,19 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
import coil.disk.DiskCache
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.network.HttpException
|
||||
import coil.request.Options
|
||||
import coil.request.Parameters
|
||||
import coil3.Extras
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.getOrDefault
|
||||
import coil3.request.Options
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER_KEY
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import logcat.LogPriority
|
||||
@@ -22,6 +22,7 @@ import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.http.HTTP_NOT_MODIFIED
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
@@ -33,6 +34,7 @@ import tachiyomi.domain.manga.model.MangaCover
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* A [Fetcher] that fetches cover image for [Manga] object.
|
||||
@@ -42,7 +44,7 @@ import java.io.File
|
||||
* handled by Coil's [DiskCache].
|
||||
*
|
||||
* Available request parameter:
|
||||
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
||||
* - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true
|
||||
*/
|
||||
class MangaCoverFetcher(
|
||||
private val url: String?,
|
||||
@@ -61,7 +63,7 @@ class MangaCoverFetcher(
|
||||
|
||||
override suspend fun fetch(): FetchResult {
|
||||
// Use custom cover if exists
|
||||
val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
|
||||
val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)
|
||||
if (useCustomCover) {
|
||||
val customCoverFile = customCoverFileLazy.value
|
||||
if (customCoverFile.exists()) {
|
||||
@@ -80,8 +82,12 @@ class MangaCoverFetcher(
|
||||
}
|
||||
|
||||
private fun fileLoader(file: File): FetchResult {
|
||||
return SourceResult(
|
||||
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
diskCacheKey = diskCacheKey
|
||||
),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
@@ -92,8 +98,8 @@ class MangaCoverFetcher(
|
||||
.openInputStream()
|
||||
.source()
|
||||
.buffer()
|
||||
return SourceResult(
|
||||
source = ImageSource(source = source, context = options.context),
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(source = source, fileSystem = FileSystem.SYSTEM),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
@@ -121,7 +127,7 @@ class MangaCoverFetcher(
|
||||
}
|
||||
|
||||
// Read from snapshot
|
||||
return SourceResult(
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
@@ -141,7 +147,7 @@ class MangaCoverFetcher(
|
||||
// Read from disk cache
|
||||
snapshot = writeToDiskCache(response)
|
||||
if (snapshot != null) {
|
||||
return SourceResult(
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.NETWORK,
|
||||
@@ -149,8 +155,8 @@ class MangaCoverFetcher(
|
||||
}
|
||||
|
||||
// Read from response if cache is unused or unusable
|
||||
return SourceResult(
|
||||
source = ImageSource(source = responseBody.source(), context = options.context),
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
|
||||
mimeType = "image/*",
|
||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
|
||||
)
|
||||
@@ -169,17 +175,20 @@ class MangaCoverFetcher(
|
||||
val response = client.newCall(newRequest()).await()
|
||||
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
|
||||
response.close()
|
||||
throw HttpException(response)
|
||||
throw IOException(response.message)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
private fun newRequest(): Request {
|
||||
val request = Request.Builder()
|
||||
.url(url!!)
|
||||
.headers(sourceLazy.value?.headers ?: options.headers)
|
||||
// Support attaching custom data to the network request.
|
||||
.tag(Parameters::class.java, options.parameters)
|
||||
val request = Request.Builder().apply {
|
||||
url(url!!)
|
||||
|
||||
val sourceHeaders = sourceLazy.value?.headers
|
||||
if (sourceHeaders != null) {
|
||||
headers(sourceHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
options.networkCachePolicy.readEnabled -> {
|
||||
@@ -264,7 +273,12 @@ class MangaCoverFetcher(
|
||||
}
|
||||
|
||||
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
||||
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
|
||||
return ImageSource(
|
||||
file = data,
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
diskCacheKey = diskCacheKey,
|
||||
closeable = this,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getResourceType(cover: String?): Type? {
|
||||
@@ -330,7 +344,7 @@ class MangaCoverFetcher(
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val USE_CUSTOM_COVER = "use_custom_cover"
|
||||
val USE_CUSTOM_COVER_KEY = Extras.Key(true)
|
||||
|
||||
private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
|
||||
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import coil3.key.Keyer
|
||||
import coil3.request.Options
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
|
@@ -1,13 +1,12 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DecodeResult
|
||||
import coil.decode.Decoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.ImageSource
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import coil3.ImageLoader
|
||||
import coil3.asCoilImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
@@ -24,20 +23,20 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
||||
|
||||
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
|
||||
|
||||
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
||||
val bitmap = decoder.decode()
|
||||
decoder.recycle()
|
||||
|
||||
check(bitmap != null) { "Failed to decode image" }
|
||||
|
||||
return DecodeResult(
|
||||
drawable = bitmap.toDrawable(options.context.resources),
|
||||
image = bitmap.asCoilImage(),
|
||||
isSampled = false,
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||
if (!isApplicable(result.source.source())) return null
|
||||
return TachiyomiImageDecoder(result.source, options)
|
||||
}
|
||||
@@ -52,7 +51,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
|
||||
override fun equals(other: Any?) = other is Factory
|
||||
|
||||
override fun hashCode() = javaClass.hashCode()
|
||||
}
|
||||
|
@@ -21,6 +21,8 @@ interface Chapter : SChapter, Serializable {
|
||||
var source_order: Int
|
||||
|
||||
var last_modified: Long
|
||||
|
||||
var version: Long
|
||||
}
|
||||
|
||||
fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
@@ -39,5 +41,6 @@ fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
chapterNumber = chapter_number.toDouble(),
|
||||
scanlator = scanlator,
|
||||
lastModifiedAt = last_modified,
|
||||
version = version,
|
||||
)
|
||||
}
|
||||
|
@@ -28,6 +28,8 @@ class ChapterImpl : Chapter {
|
||||
|
||||
override var last_modified: Long = 0
|
||||
|
||||
override var version: Long = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
@@ -18,6 +18,7 @@ import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
@@ -311,18 +312,12 @@ class DownloadCache(
|
||||
}
|
||||
|
||||
// Try to wait until extensions and sources have loaded
|
||||
var sources = getSources()
|
||||
if (sources.isEmpty()) {
|
||||
withTimeoutOrNull(30.seconds) {
|
||||
while (!extensionManager.isInitialized) {
|
||||
delay(2.seconds)
|
||||
}
|
||||
var sources = emptyList<Source>()
|
||||
withTimeoutOrNull(30.seconds) {
|
||||
extensionManager.isInitialized.first { it }
|
||||
sourceManager.isInitialized.first { it }
|
||||
|
||||
while (extensionManager.availableExtensionsFlow.value.isNotEmpty() && sources.isEmpty()) {
|
||||
delay(2.seconds)
|
||||
sources = getSources()
|
||||
}
|
||||
}
|
||||
sources = getSources()
|
||||
}
|
||||
|
||||
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
||||
|
@@ -54,6 +54,7 @@ import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -78,6 +79,7 @@ class Downloader(
|
||||
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||
private val xml: XML = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val getTracks: GetTracks = Injekt.get(),
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -626,9 +628,22 @@ class Downloader(
|
||||
chapter: Chapter,
|
||||
source: HttpSource,
|
||||
) {
|
||||
val chapterUrl = source.getChapterUrl(chapter.toSChapter())
|
||||
val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() }
|
||||
val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories)
|
||||
val urls = getTracks.await(manga.id)
|
||||
.mapNotNull { track ->
|
||||
track.remoteUrl.takeUnless { url -> url.isBlank() }?.trim()
|
||||
}
|
||||
.plus(source.getChapterUrl(chapter.toSChapter()).trim())
|
||||
.distinct()
|
||||
|
||||
val comicInfo = getComicInfo(
|
||||
manga,
|
||||
chapter,
|
||||
urls,
|
||||
categories,
|
||||
source.name
|
||||
)
|
||||
|
||||
// Remove the old file
|
||||
dir.findFile(COMIC_INFO_FILE, true)?.delete()
|
||||
dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use {
|
||||
|
@@ -9,9 +9,10 @@ import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil3.imageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transformations
|
||||
import coil3.transform.CircleCropTransformation
|
||||
import eu.kanade.presentation.util.formatChapterNumber
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
@@ -294,7 +295,7 @@ class LibraryUpdateNotifier(
|
||||
.transformations(CircleCropTransformation())
|
||||
.size(NOTIF_ICON_SIZE)
|
||||
.build()
|
||||
val drawable = context.imageLoader.execute(request).drawable
|
||||
val drawable = context.imageLoader.execute(request).image?.asDrawable(context.resources)
|
||||
return drawable?.getBitmapOrNull()
|
||||
}
|
||||
|
||||
|
@@ -79,7 +79,7 @@ class ImageSaver(
|
||||
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
|
||||
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
||||
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
||||
MediaStore.Images.Media.DATE_MODIFIED to Instant.now().toEpochMilli(),
|
||||
MediaStore.Images.Media.DATE_MODIFIED to Instant.now().epochSecond,
|
||||
)
|
||||
|
||||
val picture = findUriOrDefault(relativePath, filename) {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
@@ -32,7 +31,8 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
|
||||
// Add the authorization header to the original request
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
||||
// TODO(antsy): Add back custom user agent when they stop blocking us for no apparent reason
|
||||
// .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
|
@@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import logcat.LogPriority
|
||||
@@ -41,8 +42,8 @@ class ExtensionManager(
|
||||
private val trustExtension: TrustExtension = Injekt.get(),
|
||||
) {
|
||||
|
||||
var isInitialized = false
|
||||
private set
|
||||
private val _isInitialized = MutableStateFlow(false)
|
||||
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
||||
|
||||
/**
|
||||
* API where all the available extensions can be found.
|
||||
@@ -108,7 +109,7 @@ class ExtensionManager(
|
||||
.filterIsInstance<LoadResult.Untrusted>()
|
||||
.map { it.extension }
|
||||
|
||||
isInitialized = true
|
||||
_isInitialized.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.app.Service
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Process
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
@@ -49,7 +50,8 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
try {
|
||||
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||
service.contentResolver.openInputStream(entry.uri)!!.use {
|
||||
val createCommand = "pm install-create --user current -r -i ${service.packageName} -S $size"
|
||||
val userId = Process.myUserHandle().hashCode()
|
||||
val createCommand = "pm install-create --user $userId -r -i ${service.packageName} -S $size"
|
||||
val createResult = exec(createCommand)
|
||||
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
||||
?: throw RuntimeException("Failed to create install session")
|
||||
|
@@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
@@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
|
||||
import eu.kanade.tachiyomi.util.system.ChildFirstPathClassLoader
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -272,7 +272,7 @@ internal object ExtensionLoader {
|
||||
}
|
||||
|
||||
val classLoader = try {
|
||||
PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
ChildFirstPathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" }
|
||||
return LoadResult.Error
|
||||
|
@@ -9,6 +9,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -28,6 +30,9 @@ class AndroidSourceManager(
|
||||
private val sourceRepository: StubSourceRepository,
|
||||
) : SourceManager {
|
||||
|
||||
private val _isInitialized = MutableStateFlow(false)
|
||||
override val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
||||
|
||||
private val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
@@ -60,6 +65,7 @@ class AndroidSourceManager(
|
||||
}
|
||||
}
|
||||
sourcesMapFlow.value = mutableMap
|
||||
_isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -31,6 +31,7 @@ import androidx.preference.forEach
|
||||
import androidx.preference.getOnBindEditTextListener
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.core.util.ifSourcesLoaded
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -40,6 +41,7 @@ import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@@ -47,6 +49,11 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
if (!ifSourcesLoaded()) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
|
@@ -18,6 +18,7 @@ import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.core.util.ifSourcesLoaded
|
||||
import eu.kanade.presentation.browse.BrowseSourceContent
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
@@ -34,6 +35,7 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
data class SourceSearchScreen(
|
||||
@@ -44,6 +46,11 @@ data class SourceSearchScreen(
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
if (!ifSourcesLoaded()) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -76,7 +83,7 @@ data class SourceSearchScreen(
|
||||
) { paddingValues ->
|
||||
val pagingFlow by screenModel.mangaPagerFlowFlow.collectAsState()
|
||||
val openMigrateDialog: (Manga) -> Unit = {
|
||||
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(it))
|
||||
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(newManga = it, oldManga = oldManga))
|
||||
}
|
||||
BrowseSourceContent(
|
||||
source = screenModel.source,
|
||||
|
@@ -35,6 +35,7 @@ import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.core.util.ifSourcesLoaded
|
||||
import eu.kanade.presentation.browse.BrowseSourceContent
|
||||
import eu.kanade.presentation.browse.MissingSourceScreen
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
|
||||
@@ -46,6 +47,8 @@ import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
@@ -60,6 +63,7 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
data class BrowseSourceScreen(
|
||||
@@ -73,6 +77,11 @@ data class BrowseSourceScreen(
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
if (!ifSourcesLoaded()) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId, listingQuery) }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
@@ -245,6 +254,22 @@ data class BrowseSourceScreen(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||
onMigrate = {
|
||||
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(dialog.manga, dialog.duplicate))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is BrowseSourceScreenModel.Dialog.Migrate -> {
|
||||
MigrateDialog(
|
||||
oldManga = dialog.oldManga,
|
||||
newManga = dialog.newManga,
|
||||
screenModel = MigrateDialogScreenModel(),
|
||||
onDismissRequest = onDismissRequest,
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
||||
onPopScreen = {
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
|
||||
@@ -267,7 +292,6 @@ data class BrowseSourceScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
is BrowseSourceScreenModel.Dialog.Migrate -> {}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
|
@@ -345,7 +345,7 @@ class BrowseSourceScreenModel(
|
||||
val manga: Manga,
|
||||
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
|
||||
) : Dialog
|
||||
data class Migrate(val newManga: Manga) : Dialog
|
||||
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
@@ -10,6 +10,7 @@ import androidx.compose.runtime.setValue
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.core.util.ifSourcesLoaded
|
||||
import eu.kanade.presentation.browse.GlobalSearchScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||
@@ -23,6 +24,11 @@ class GlobalSearchScreen(
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
if (!ifSourcesLoaded()) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val screenModel = rememberScreenModel {
|
||||
|
@@ -28,7 +28,6 @@ import tachiyomi.domain.history.interactor.RemoveHistory
|
||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.LocalDate
|
||||
|
||||
class HistoryScreenModel(
|
||||
private val getHistory: GetHistory = Injekt.get(),
|
||||
@@ -60,12 +59,10 @@ class HistoryScreenModel(
|
||||
private fun List<HistoryWithRelations>.toHistoryUiModels(): List<HistoryUiModel> {
|
||||
return map { HistoryUiModel.Item(it) }
|
||||
.insertSeparators { before, after ->
|
||||
val beforeDate = before?.item?.readAt?.time?.toLocalDate() ?: LocalDate.MIN
|
||||
val afterDate = after?.item?.readAt?.time?.toLocalDate() ?: LocalDate.MIN
|
||||
val beforeDate = before?.item?.readAt?.time?.toLocalDate()
|
||||
val afterDate = after?.item?.readAt?.time?.toLocalDate()
|
||||
when {
|
||||
beforeDate.isAfter(afterDate)
|
||||
or afterDate.equals(LocalDate.MIN)
|
||||
or beforeDate.equals(LocalDate.MIN) -> HistoryUiModel.Header(afterDate)
|
||||
beforeDate != afterDate && afterDate != null -> HistoryUiModel.Header(afterDate)
|
||||
// Return null to avoid adding a separator between two items.
|
||||
else -> null
|
||||
}
|
||||
|
@@ -5,9 +5,9 @@ import android.net.Uri
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import coil3.imageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.size.Size
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.saver.Image
|
||||
@@ -96,7 +96,7 @@ class MangaCoverScreenModel(
|
||||
.build()
|
||||
|
||||
return withIOContext {
|
||||
val result = context.imageLoader.execute(req).drawable
|
||||
val result = context.imageLoader.execute(req).image?.asDrawable(context.resources)
|
||||
|
||||
// TODO: Handle animated cover
|
||||
val bitmap = result?.getBitmapOrNull() ?: return@withIOContext null
|
||||
|
@@ -22,6 +22,7 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.core.util.ifSourcesLoaded
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.domain.manga.model.toSManga
|
||||
import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
||||
@@ -40,6 +41,8 @@ import eu.kanade.presentation.util.isTabletUi
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.isLocalOrStub
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
||||
@@ -73,6 +76,11 @@ class MangaScreen(
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
if (!ifSourcesLoaded()) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val haptic = LocalHapticFeedback.current
|
||||
@@ -185,11 +193,28 @@ class MangaScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
is MangaScreenModel.Dialog.DuplicateManga -> DuplicateMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
|
||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||
)
|
||||
|
||||
is MangaScreenModel.Dialog.DuplicateManga -> {
|
||||
DuplicateMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
|
||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||
onMigrate = {
|
||||
screenModel.showMigrateDialog(dialog.duplicate)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is MangaScreenModel.Dialog.Migrate -> {
|
||||
MigrateDialog(
|
||||
oldManga = dialog.oldManga,
|
||||
newManga = dialog.newManga,
|
||||
screenModel = MigrateDialogScreenModel(),
|
||||
onDismissRequest = onDismissRequest,
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
||||
onPopScreen = { navigator.replace(MangaScreen(dialog.newManga.id)) },
|
||||
)
|
||||
}
|
||||
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
manga = successState.manga,
|
||||
|
@@ -1003,6 +1003,7 @@ class MangaScreenModel(
|
||||
) : Dialog
|
||||
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
||||
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
||||
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
||||
data class SetFetchInterval(val manga: Manga) : Dialog
|
||||
data object SettingsSheet : Dialog
|
||||
data object TrackSheet : Dialog
|
||||
@@ -1029,6 +1030,11 @@ class MangaScreenModel(
|
||||
updateSuccessState { it.copy(dialog = Dialog.FullCover) }
|
||||
}
|
||||
|
||||
fun showMigrateDialog(duplicate: Manga) {
|
||||
val manga = successState?.manga ?: return
|
||||
updateSuccessState { it.copy(dialog = Dialog.Migrate(newManga = manga, oldManga = duplicate)) }
|
||||
}
|
||||
|
||||
fun setExcludedScanlators(excludedScanlators: Set<String>) {
|
||||
screenModelScope.launchIO {
|
||||
setExcludedScanlators.await(mangaId, excludedScanlators)
|
||||
|
@@ -427,6 +427,7 @@ private data class TrackDateSelectorScreen(
|
||||
private val start: Boolean,
|
||||
) : Screen() {
|
||||
|
||||
@Transient
|
||||
private val selectableDates = object : SelectableDates {
|
||||
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
||||
val dateToCheck = Instant.ofEpochMilli(utcTimeMillis)
|
||||
|
@@ -5,7 +5,6 @@ import android.app.Activity
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
@@ -39,7 +38,9 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||
import com.hippo.unifile.UniFile
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.core.util.ifSourcesLoaded
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.presentation.reader.DisplayRefreshHost
|
||||
import eu.kanade.presentation.reader.OrientationSelectDialog
|
||||
@@ -93,6 +94,7 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class ReaderActivity : BaseActivity() {
|
||||
|
||||
@@ -344,6 +346,10 @@ class ReaderActivity : BaseActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
if (!ifSourcesLoaded()) {
|
||||
return@setComposeContent
|
||||
}
|
||||
|
||||
val isHttpSource = viewModel.getSource() is HttpSource
|
||||
val isFullscreen by readerPreferences.fullscreen().collectAsState()
|
||||
val flashOnPageChange by readerPreferences.flashOnPageChange().collectAsState()
|
||||
@@ -814,8 +820,8 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
readerPreferences.trueColor().changes()
|
||||
.onEach(::setTrueColor)
|
||||
preferences.displayProfile().changes()
|
||||
.onEach { setDisplayProfile(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
readerPreferences.cutoutShort().changes()
|
||||
@@ -854,13 +860,19 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 32-bit color mode according to [enabled].
|
||||
* Sets the display profile to [path].
|
||||
*/
|
||||
private fun setTrueColor(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
SubsamplingScaleImageView.setPreferredBitmapConfig(Bitmap.Config.ARGB_8888)
|
||||
} else {
|
||||
SubsamplingScaleImageView.setPreferredBitmapConfig(Bitmap.Config.RGB_565)
|
||||
private fun setDisplayProfile(path: String) {
|
||||
val file = UniFile.fromUri(baseContext, path.toUri())
|
||||
if (file != null && file.exists()) {
|
||||
val inputStream = file.openInputStream()
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
inputStream.use { input ->
|
||||
outputStream.use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
SubsamplingScaleImageView.setDisplayProfile(outputStream.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -264,6 +265,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
try {
|
||||
val manga = getManga.await(mangaId)
|
||||
if (manga != null) {
|
||||
sourceManager.isInitialized.first { it }
|
||||
mutableState.update { it.copy(manga = manga) }
|
||||
if (chapterId == -1L) chapterId = initialChapterId
|
||||
|
||||
|
@@ -4,9 +4,9 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import coil3.imageLoader
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ImageRequest
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
@@ -37,7 +37,7 @@ class SaveImageNotifier(private val context: Context) {
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.size(720, 1280)
|
||||
.target(
|
||||
onSuccess = { showCompleteNotification(uri, it.getBitmapOrNull()) },
|
||||
onSuccess = { showCompleteNotification(uri, it.asDrawable(context.resources).getBitmapOrNull()) },
|
||||
onError = { onError(null) },
|
||||
)
|
||||
.build()
|
||||
|
@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import mihon.core.common.extensions.toZipFile
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import java.nio.channels.SeekableByteChannel
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.nio.channels.SeekableByteChannel
|
||||
*/
|
||||
internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() {
|
||||
|
||||
private val zip = ZipFile(channel)
|
||||
private val zip = channel.toZipFile()
|
||||
|
||||
override var isLocal: Boolean = true
|
||||
|
||||
|
@@ -23,9 +23,6 @@ class ReaderPreferences(
|
||||
|
||||
fun showReadingMode() = preferenceStore.getBoolean("pref_show_reading_mode", true)
|
||||
|
||||
// TODO: default this to true if reader long strip ever goes stable
|
||||
fun trueColor() = preferenceStore.getBoolean("pref_true_color_key", false)
|
||||
|
||||
fun fullscreen() = preferenceStore.getBoolean("fullscreen", true)
|
||||
|
||||
fun cutoutShort() = preferenceStore.getBoolean("cutout_short", true)
|
||||
@@ -72,6 +69,8 @@ class ReaderPreferences(
|
||||
|
||||
fun skipDupe() = preferenceStore.getBoolean("skip_dupe", false)
|
||||
|
||||
fun webtoonDisableZoomOut() = preferenceStore.getBoolean("webtoon_disable_zoom_out", false)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Split two page spread
|
||||
|
@@ -18,10 +18,11 @@ import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.core.view.isVisible
|
||||
import coil.dispose
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import coil3.dispose
|
||||
import coil3.imageLoader
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD
|
||||
@@ -348,8 +349,9 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.target(
|
||||
onSuccess = { result ->
|
||||
setImageDrawable(result)
|
||||
(result as? Animatable)?.start()
|
||||
val drawable = result.asDrawable(context.resources)
|
||||
setImageDrawable(drawable)
|
||||
(drawable as? Animatable)?.start()
|
||||
isVisible = true
|
||||
this@ReaderPageImageView.onImageLoaded()
|
||||
},
|
||||
|
@@ -22,7 +22,6 @@ abstract class ViewerConfig(readerPreferences: ReaderPreferences, private val sc
|
||||
var doubleTapAnimDuration = 500
|
||||
var volumeKeysEnabled = false
|
||||
var volumeKeysInverted = false
|
||||
var trueColor = false
|
||||
var alwaysShowChapterTransition = true
|
||||
var navigationMode = 0
|
||||
protected set
|
||||
@@ -62,9 +61,6 @@ abstract class ViewerConfig(readerPreferences: ReaderPreferences, private val sc
|
||||
readerPreferences.readWithVolumeKeysInverted()
|
||||
.register({ volumeKeysInverted = it })
|
||||
|
||||
readerPreferences.trueColor()
|
||||
.register({ trueColor = it }, { imagePropertyChangedListener?.invoke() })
|
||||
|
||||
readerPreferences.alwaysShowChapterTransition()
|
||||
.register({ alwaysShowChapterTransition = it })
|
||||
|
||||
|
@@ -29,6 +29,11 @@ class WebtoonConfig(
|
||||
var imageCropBorders = false
|
||||
private set
|
||||
|
||||
var zoomOutDisabled = false
|
||||
private set
|
||||
|
||||
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
|
||||
|
||||
var sidePadding = 0
|
||||
private set
|
||||
|
||||
@@ -74,6 +79,12 @@ class WebtoonConfig(
|
||||
{ imagePropertyChangedListener?.invoke() },
|
||||
)
|
||||
|
||||
readerPreferences.webtoonDisableZoomOut()
|
||||
.register(
|
||||
{ zoomOutDisabled = it },
|
||||
{ zoomPropertyChangedListener?.invoke(it) }
|
||||
)
|
||||
|
||||
readerPreferences.webtoonDoubleTapZoomEnabled()
|
||||
.register(
|
||||
{ doubleTapZoom = it },
|
||||
|
@@ -33,6 +33,12 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
|
||||
scaleDetector.isQuickScaleEnabled = value
|
||||
}
|
||||
|
||||
var zoomOutDisabled = false
|
||||
set(value) {
|
||||
field = value
|
||||
recycler?.zoomOutDisabled = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Recycler view added in this frame.
|
||||
*/
|
||||
|
@@ -36,20 +36,21 @@ class WebtoonLayoutManager(context: Context) : LinearLayoutManager(context) {
|
||||
*/
|
||||
fun findLastEndVisibleItemPosition(): Int {
|
||||
ensureLayoutState()
|
||||
@ViewBoundsCheck.ViewBounds val preferredBoundsFlag =
|
||||
(ViewBoundsCheck.FLAG_CVE_LT_PVE or ViewBoundsCheck.FLAG_CVE_EQ_PVE)
|
||||
|
||||
val fromIndex = childCount - 1
|
||||
val toIndex = -1
|
||||
|
||||
val child = if (mOrientation == HORIZONTAL) {
|
||||
val callback = if (mOrientation == HORIZONTAL) {
|
||||
mHorizontalBoundCheck
|
||||
.findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, 0)
|
||||
} else {
|
||||
mVerticalBoundCheck
|
||||
.findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, 0)
|
||||
}.mCallback
|
||||
val start = callback.parentStart
|
||||
val end = callback.parentEnd
|
||||
for (i in childCount - 1 downTo 0) {
|
||||
val child = getChildAt(i)!!
|
||||
val childStart = callback.getChildStart(child)
|
||||
val childEnd = callback.getChildEnd(child)
|
||||
if (childEnd <= end || childStart < start) {
|
||||
return getPosition(child)
|
||||
}
|
||||
}
|
||||
|
||||
return if (child == null) NO_POSITION else getPosition(child)
|
||||
return NO_POSITION
|
||||
}
|
||||
}
|
||||
|
@@ -118,6 +118,7 @@ class WebtoonPageHolder(
|
||||
removeErrorLayout()
|
||||
frame.recycle()
|
||||
progressIndicator.setProgress(0)
|
||||
progressContainer.isVisible = true
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -33,6 +33,15 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
private var firstVisibleItemPosition = 0
|
||||
private var lastVisibleItemPosition = 0
|
||||
private var currentScale = DEFAULT_RATE
|
||||
var zoomOutDisabled = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (value && currentScale < DEFAULT_RATE) {
|
||||
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
|
||||
}
|
||||
}
|
||||
private val minRate
|
||||
get() = if (zoomOutDisabled) DEFAULT_RATE else MIN_RATE
|
||||
|
||||
private val listener = GestureListener()
|
||||
private val detector = Detector()
|
||||
@@ -166,7 +175,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
fun onScale(scaleFactor: Float) {
|
||||
currentScale *= scaleFactor
|
||||
currentScale = currentScale.coerceIn(
|
||||
MIN_RATE,
|
||||
minRate,
|
||||
MAX_SCALE_RATE,
|
||||
)
|
||||
|
||||
@@ -193,8 +202,8 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun onScaleEnd() {
|
||||
if (scaleX < MIN_RATE) {
|
||||
zoom(currentScale, MIN_RATE, x, 0f, y, 0f)
|
||||
if (scaleX < minRate) {
|
||||
zoom(currentScale, minRate, x, 0f, y, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -152,6 +152,10 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
frame.doubleTapZoom = it
|
||||
}
|
||||
|
||||
config.zoomPropertyChangedListener = {
|
||||
frame.zoomOutDisabled = it
|
||||
}
|
||||
|
||||
config.navigationModeChangedListener = {
|
||||
val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay
|
||||
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
|
||||
|
@@ -45,7 +45,6 @@ import tachiyomi.domain.updates.interactor.GetUpdates
|
||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.LocalDate
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class UpdatesScreenModel(
|
||||
@@ -374,12 +373,10 @@ class UpdatesScreenModel(
|
||||
return items
|
||||
.map { UpdatesUiModel.Item(it) }
|
||||
.insertSeparators { before, after ->
|
||||
val beforeDate = before?.item?.update?.dateFetch?.toLocalDate() ?: LocalDate.MIN
|
||||
val afterDate = after?.item?.update?.dateFetch?.toLocalDate() ?: LocalDate.MIN
|
||||
val beforeDate = before?.item?.update?.dateFetch?.toLocalDate()
|
||||
val afterDate = after?.item?.update?.dateFetch?.toLocalDate()
|
||||
when {
|
||||
beforeDate.isAfter(afterDate)
|
||||
or afterDate.equals(LocalDate.MIN)
|
||||
or beforeDate.equals(LocalDate.MIN) -> UpdatesUiModel.Header(afterDate)
|
||||
beforeDate != afterDate && afterDate != null -> UpdatesUiModel.Header(afterDate)
|
||||
// Return null to avoid adding a separator between two items.
|
||||
else -> null
|
||||
}
|
||||
|
@@ -60,14 +60,12 @@ fun LocalDate.toRelativeString(
|
||||
difference.toInt().absoluteValue,
|
||||
difference.toInt().absoluteValue,
|
||||
)
|
||||
|
||||
difference < 1 -> context.stringResource(MR.strings.relative_time_today)
|
||||
difference < 7 -> context.pluralStringResource(
|
||||
MR.plurals.relative_time,
|
||||
difference.toInt(),
|
||||
difference.toInt(),
|
||||
)
|
||||
|
||||
else -> dateFormat.format(this)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,86 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import dalvik.system.PathClassLoader
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.util.Enumeration
|
||||
|
||||
/**
|
||||
* A parent-last class loader that will try in order:
|
||||
* - the system class loader
|
||||
* - the child class loader
|
||||
* - the parent class loader.
|
||||
*/
|
||||
class ChildFirstPathClassLoader(
|
||||
dexPath: String,
|
||||
librarySearchPath: String?,
|
||||
parent: ClassLoader
|
||||
) : PathClassLoader(dexPath, librarySearchPath, parent) {
|
||||
|
||||
private val systemClassLoader: ClassLoader? = getSystemClassLoader()
|
||||
|
||||
override fun loadClass(name: String?, resolve: Boolean): Class<*> {
|
||||
var c = findLoadedClass(name)
|
||||
|
||||
if (c == null && systemClassLoader != null) {
|
||||
try {
|
||||
c = systemClassLoader.loadClass(name)
|
||||
} catch (_: ClassNotFoundException) {}
|
||||
}
|
||||
|
||||
if (c == null) {
|
||||
c = try {
|
||||
findClass(name)
|
||||
} catch (_: ClassNotFoundException) {
|
||||
super.loadClass(name, resolve)
|
||||
}
|
||||
}
|
||||
|
||||
if (resolve) {
|
||||
resolveClass(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
override fun getResource(name: String?): URL? {
|
||||
return systemClassLoader?.getResource(name)
|
||||
?: findResource(name)
|
||||
?: super.getResource(name)
|
||||
}
|
||||
|
||||
override fun getResources(name: String?): Enumeration<URL> {
|
||||
val systemUrls = systemClassLoader?.getResources(name)
|
||||
val localUrls = findResources(name)
|
||||
val parentUrls = parent?.getResources(name)
|
||||
val urls = buildList {
|
||||
while (systemUrls?.hasMoreElements() == true) {
|
||||
add(systemUrls.nextElement())
|
||||
}
|
||||
|
||||
while (localUrls?.hasMoreElements() == true) {
|
||||
add(localUrls.nextElement())
|
||||
}
|
||||
|
||||
while (parentUrls?.hasMoreElements() == true) {
|
||||
add(parentUrls.nextElement())
|
||||
}
|
||||
}
|
||||
|
||||
return object : Enumeration<URL> {
|
||||
val iterator = urls.iterator()
|
||||
|
||||
override fun hasMoreElements() = iterator.hasNext()
|
||||
override fun nextElement() = iterator.next()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getResourceAsStream(name: String?): InputStream? {
|
||||
return try {
|
||||
getResource(name)?.openStream()
|
||||
} catch (_: IOException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.drawable.ScaleDrawable
|
||||
import coil3.gif.ScaleDrawable
|
||||
|
||||
fun Drawable.getBitmapOrNull(): Bitmap? = when (this) {
|
||||
is BitmapDrawable -> bitmap
|
||||
|
@@ -18,5 +18,7 @@ style:
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ReturnCount:
|
||||
excludeGuardClauses: true
|
||||
SerialVersionUIDInSerializableClass:
|
||||
active: false
|
||||
UnusedPrivateMember:
|
||||
ignoreAnnotated: [ 'Preview' ]
|
||||
|
@@ -27,6 +27,7 @@ fun SManga.getComicInfo() = ComicInfo(
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
categories = null,
|
||||
source = null,
|
||||
)
|
||||
|
||||
fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
@@ -81,6 +82,7 @@ data class ComicInfo(
|
||||
val web: Web?,
|
||||
val publishingStatus: PublishingStatusTachiyomi?,
|
||||
val categories: CategoriesTachiyomi?,
|
||||
val source: SourceMihon?,
|
||||
) {
|
||||
@XmlElement(false)
|
||||
@XmlSerialName("xmlns:xsd", "", "")
|
||||
@@ -154,6 +156,10 @@ data class ComicInfo(
|
||||
@Serializable
|
||||
@XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty")
|
||||
data class CategoriesTachiyomi(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("SourceMihon", "http://www.w3.org/2001/XMLSchema", "mh")
|
||||
data class SourceMihon(@XmlValue(true) val value: String = "")
|
||||
}
|
||||
|
||||
enum class ComicInfoPublishingStatus(
|
||||
|
@@ -19,7 +19,7 @@ class NetworkPreferences(
|
||||
fun defaultUserAgent(): Preference<String> {
|
||||
return preferenceStore.getString(
|
||||
"default_user_agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.util.storage
|
||||
|
||||
import mihon.core.common.extensions.toZipFile
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.io.Closeable
|
||||
@@ -17,7 +17,7 @@ class EpubFile(channel: SeekableByteChannel) : Closeable {
|
||||
/**
|
||||
* Zip file of this epub.
|
||||
*/
|
||||
private val zip = ZipFile(channel)
|
||||
private val zip = channel.toZipFile()
|
||||
|
||||
/**
|
||||
* Path separator used by this epub.
|
||||
|
@@ -0,0 +1,8 @@
|
||||
package mihon.core.common.extensions
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import java.nio.channels.SeekableByteChannel
|
||||
|
||||
fun SeekableByteChannel.toZipFile(): ZipFile {
|
||||
return ZipFile.Builder().setSeekableByteChannel(this).get()
|
||||
}
|
@@ -77,19 +77,20 @@ object ImageUtil {
|
||||
}
|
||||
|
||||
fun isAnimatedAndSupported(stream: InputStream): Boolean {
|
||||
try {
|
||||
return try {
|
||||
val type = getImageType(stream) ?: return false
|
||||
return when (type.format) {
|
||||
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats
|
||||
when (type.format) {
|
||||
Format.Gif -> true
|
||||
// Coil supports animated WebP on Android 9.0+
|
||||
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats
|
||||
// Animated WebP on Android 9+
|
||||
Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
// Animated Heif on Android 11+
|
||||
Format.Heif -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
else -> false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
/* Do Nothing */
|
||||
false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getImageType(stream: InputStream): tachiyomi.decoder.ImageType? {
|
||||
@@ -551,7 +552,7 @@ object ImageUtil {
|
||||
imageStream: InputStream,
|
||||
resetAfterExtraction: Boolean = true,
|
||||
): BitmapFactory.Options {
|
||||
imageStream.mark(imageStream.available() + 1)
|
||||
imageStream.mark(Int.MAX_VALUE)
|
||||
|
||||
val imageBytes = imageStream.readBytes()
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
|
@@ -29,6 +29,7 @@ class ChapterRepositoryImpl(
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
chapter.version,
|
||||
)
|
||||
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
|
||||
chapter.copy(id = lastInsertId)
|
||||
@@ -64,6 +65,8 @@ class ChapterRepositoryImpl(
|
||||
dateFetch = chapterUpdate.dateFetch,
|
||||
dateUpload = chapterUpdate.dateUpload,
|
||||
chapterId = chapterUpdate.id,
|
||||
version = chapterUpdate.version,
|
||||
isSyncing = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -124,6 +127,7 @@ class ChapterRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun mapChapter(
|
||||
id: Long,
|
||||
mangaId: Long,
|
||||
@@ -138,6 +142,9 @@ class ChapterRepositoryImpl(
|
||||
dateFetch: Long,
|
||||
dateUpload: Long,
|
||||
lastModifiedAt: Long,
|
||||
version: Long,
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
isSyncing: Long,
|
||||
): Chapter = Chapter(
|
||||
id = id,
|
||||
mangaId = mangaId,
|
||||
@@ -152,5 +159,6 @@ class ChapterRepositoryImpl(
|
||||
chapterNumber = chapterNumber,
|
||||
scanlator = scanlator,
|
||||
lastModifiedAt = lastModifiedAt,
|
||||
version = version,
|
||||
)
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
object MangaMapper {
|
||||
@Suppress("LongParameterList")
|
||||
fun mapManga(
|
||||
id: Long,
|
||||
source: Long,
|
||||
@@ -28,6 +29,9 @@ object MangaMapper {
|
||||
calculateInterval: Long,
|
||||
lastModifiedAt: Long,
|
||||
favoriteModifiedAt: Long?,
|
||||
version: Long,
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
isSyncing: Long,
|
||||
): Manga = Manga(
|
||||
id = id,
|
||||
source = source,
|
||||
@@ -51,8 +55,10 @@ object MangaMapper {
|
||||
initialized = initialized,
|
||||
lastModifiedAt = lastModifiedAt,
|
||||
favoriteModifiedAt = favoriteModifiedAt,
|
||||
version = version,
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun mapLibraryManga(
|
||||
id: Long,
|
||||
source: Long,
|
||||
@@ -76,6 +82,8 @@ object MangaMapper {
|
||||
calculateInterval: Long,
|
||||
lastModifiedAt: Long,
|
||||
favoriteModifiedAt: Long?,
|
||||
version: Long,
|
||||
isSyncing: Long,
|
||||
totalCount: Long,
|
||||
readCount: Double,
|
||||
latestUpload: Long,
|
||||
@@ -107,6 +115,8 @@ object MangaMapper {
|
||||
calculateInterval,
|
||||
lastModifiedAt,
|
||||
favoriteModifiedAt,
|
||||
version,
|
||||
isSyncing,
|
||||
),
|
||||
category = category,
|
||||
totalChapters = totalCount,
|
||||
|
@@ -65,9 +65,9 @@ class MangaRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUpcomingManga(): List<Manga> {
|
||||
override suspend fun getUpcomingManga(statues: Set<Long>): List<Manga> {
|
||||
return handler.awaitList {
|
||||
mangasQueries.getUpcomingManga(MangaMapper::mapManga)
|
||||
mangasQueries.getUpcomingManga(statues, MangaMapper::mapManga)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ class MangaRepositoryImpl(
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
version = manga.version,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
@@ -161,6 +162,8 @@ class MangaRepositoryImpl(
|
||||
dateAdded = value.dateAdded,
|
||||
mangaId = value.id,
|
||||
updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode),
|
||||
version = value.version,
|
||||
isSyncing = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,8 @@ CREATE TABLE chapters(
|
||||
date_fetch INTEGER NOT NULL,
|
||||
date_upload INTEGER NOT NULL,
|
||||
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
is_syncing INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
@@ -30,6 +32,22 @@ BEGIN
|
||||
WHERE _id = new._id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER update_chapter_and_manga_version AFTER UPDATE ON chapters
|
||||
WHEN new.is_syncing = 0 AND (
|
||||
new.read != old.read OR
|
||||
new.bookmark != old.bookmark OR
|
||||
new.last_page_read != old.last_page_read
|
||||
)
|
||||
BEGIN
|
||||
-- Update the chapter version
|
||||
UPDATE chapters SET version = version + 1
|
||||
WHERE _id = new._id;
|
||||
|
||||
-- Update the manga version
|
||||
UPDATE mangas SET version = version + 1
|
||||
WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0;
|
||||
END;
|
||||
|
||||
getChapterById:
|
||||
SELECT *
|
||||
FROM chapters
|
||||
@@ -73,9 +91,14 @@ removeChaptersWithIds:
|
||||
DELETE FROM chapters
|
||||
WHERE _id IN :chapterIds;
|
||||
|
||||
resetIsSyncing:
|
||||
UPDATE chapters
|
||||
SET is_syncing = 0
|
||||
WHERE is_syncing = 1;
|
||||
|
||||
insert:
|
||||
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at)
|
||||
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0);
|
||||
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing)
|
||||
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0);
|
||||
|
||||
update:
|
||||
UPDATE chapters
|
||||
@@ -89,7 +112,9 @@ SET manga_id = coalesce(:mangaId, manga_id),
|
||||
chapter_number = coalesce(:chapterNumber, chapter_number),
|
||||
source_order = coalesce(:sourceOrder, source_order),
|
||||
date_fetch = coalesce(:dateFetch, date_fetch),
|
||||
date_upload = coalesce(:dateUpload, date_upload)
|
||||
date_upload = coalesce(:dateUpload, date_upload),
|
||||
version = coalesce(:version, version),
|
||||
is_syncing = coalesce(:isSyncing, is_syncing)
|
||||
WHERE _id = :chapterId;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
|
@@ -25,7 +25,9 @@ CREATE TABLE mangas(
|
||||
update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0,
|
||||
calculate_interval INTEGER DEFAULT 0 NOT NULL,
|
||||
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
||||
favorite_modified_at INTEGER
|
||||
favorite_modified_at INTEGER,
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
is_syncing INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
|
||||
@@ -48,6 +50,16 @@ BEGIN
|
||||
WHERE _id = new._id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas
|
||||
BEGIN
|
||||
UPDATE mangas SET version = version + 1
|
||||
WHERE _id = new._id AND new.is_syncing = 0 AND (
|
||||
new.url != old.url OR
|
||||
new.description != old.description OR
|
||||
new.favorite != old.favorite
|
||||
);
|
||||
END;
|
||||
|
||||
getMangaById:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
@@ -105,13 +117,18 @@ SELECT *
|
||||
FROM mangas
|
||||
WHERE next_update > 0
|
||||
AND favorite = 1
|
||||
AND (status == 1 OR status == 4)
|
||||
AND status IN ?
|
||||
ORDER BY next_update ASC;
|
||||
|
||||
resetViewerFlags:
|
||||
UPDATE mangas
|
||||
SET viewer = 0;
|
||||
|
||||
resetIsSyncing:
|
||||
UPDATE mangas
|
||||
SET is_syncing = 0
|
||||
WHERE is_syncing = 1;
|
||||
|
||||
getSourceIdsWithNonLibraryManga:
|
||||
SELECT source, COUNT(*) AS manga_count
|
||||
FROM mangas
|
||||
@@ -124,8 +141,8 @@ WHERE favorite = 0
|
||||
AND source IN :sourceIds;
|
||||
|
||||
insert:
|
||||
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at)
|
||||
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0);
|
||||
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version)
|
||||
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version);
|
||||
|
||||
update:
|
||||
UPDATE mangas SET
|
||||
@@ -147,7 +164,9 @@ UPDATE mangas SET
|
||||
cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
|
||||
date_added = coalesce(:dateAdded, date_added),
|
||||
update_strategy = coalesce(:updateStrategy, update_strategy),
|
||||
calculate_interval = coalesce(:calculateInterval, calculate_interval)
|
||||
calculate_interval = coalesce(:calculateInterval, calculate_interval),
|
||||
version = coalesce(:version, version),
|
||||
is_syncing = coalesce(:isSyncing, is_syncing)
|
||||
WHERE _id = :mangaId;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
|
@@ -2,25 +2,22 @@ CREATE TABLE mangas_categories(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(category_id) REFERENCES categories (_id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TRIGGER update_last_modified_at_mangas_categories
|
||||
AFTER UPDATE ON mangas_categories
|
||||
FOR EACH ROW
|
||||
CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories
|
||||
BEGIN
|
||||
UPDATE mangas_categories
|
||||
SET last_modified_at = strftime('%s', 'now')
|
||||
WHERE _id = new._id;
|
||||
UPDATE mangas
|
||||
SET version = version + 1
|
||||
WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0;
|
||||
END;
|
||||
|
||||
insert:
|
||||
INSERT INTO mangas_categories(manga_id, category_id, last_modified_at)
|
||||
VALUES (:mangaId, :categoryId, 0);
|
||||
INSERT INTO mangas_categories(manga_id, category_id)
|
||||
VALUES (:mangaId, :categoryId);
|
||||
|
||||
deleteMangaCategoryByMangaId:
|
||||
DELETE FROM mangas_categories
|
||||
|
46
data/src/main/sqldelight/tachiyomi/migrations/2.sqm
Normal file
46
data/src/main/sqldelight/tachiyomi/migrations/2.sqm
Normal file
@@ -0,0 +1,46 @@
|
||||
-- Mangas table
|
||||
ALTER TABLE mangas ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE mangas ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Chapters table
|
||||
ALTER TABLE chapters ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE chapters ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Mangas triggers
|
||||
DROP TRIGGER IF EXISTS update_manga_version;
|
||||
CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas
|
||||
BEGIN
|
||||
UPDATE mangas SET version = version + 1
|
||||
WHERE _id = new._id AND new.is_syncing = 0 AND (
|
||||
new.url != old.url OR
|
||||
new.description != old.description OR
|
||||
new.favorite != old.favorite
|
||||
);
|
||||
END;
|
||||
|
||||
-- Chapters triggers
|
||||
DROP TRIGGER IF EXISTS update_chapter_and_manga_version;
|
||||
CREATE TRIGGER update_chapter_and_manga_version AFTER UPDATE ON chapters
|
||||
WHEN new.is_syncing = 0 AND (
|
||||
new.read != old.read OR
|
||||
new.bookmark != old.bookmark OR
|
||||
new.last_page_read != old.last_page_read
|
||||
)
|
||||
BEGIN
|
||||
-- Update the chapter version
|
||||
UPDATE chapters SET version = version + 1
|
||||
WHERE _id = new._id;
|
||||
|
||||
-- Update the manga version
|
||||
UPDATE mangas SET version = version + 1
|
||||
WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0;
|
||||
END;
|
||||
|
||||
-- manga_categories table
|
||||
DROP TRIGGER IF EXISTS insert_manga_category_update_version;
|
||||
CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories
|
||||
BEGIN
|
||||
UPDATE mangas
|
||||
SET version = version + 1
|
||||
WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0;
|
||||
END;
|
@@ -14,6 +14,7 @@ data class Chapter(
|
||||
val chapterNumber: Double,
|
||||
val scanlator: String?,
|
||||
val lastModifiedAt: Long,
|
||||
val version: Long,
|
||||
) {
|
||||
val isRecognizedNumber: Boolean
|
||||
get() = chapterNumber >= 0f
|
||||
@@ -43,6 +44,7 @@ data class Chapter(
|
||||
chapterNumber = -1.0,
|
||||
scanlator = null,
|
||||
lastModifiedAt = 0,
|
||||
version = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ data class ChapterUpdate(
|
||||
val dateUpload: Long? = null,
|
||||
val chapterNumber: Double? = null,
|
||||
val scanlator: String? = null,
|
||||
val version: Long? = null,
|
||||
)
|
||||
|
||||
fun Chapter.toChapterUpdate(): ChapterUpdate {
|
||||
@@ -29,5 +30,6 @@ fun Chapter.toChapterUpdate(): ChapterUpdate {
|
||||
dateUpload,
|
||||
chapterNumber,
|
||||
scanlator,
|
||||
version,
|
||||
)
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package tachiyomi.domain.manga.interactor
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
@@ -7,7 +8,12 @@ class GetUpcomingManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
private val includedStatuses = setOf(
|
||||
SManga.ONGOING.toLong(),
|
||||
SManga.PUBLISHING_FINISHED.toLong(),
|
||||
)
|
||||
|
||||
suspend fun await(): List<Manga> {
|
||||
return mangaRepository.getUpcomingManga()
|
||||
return mangaRepository.getUpcomingManga(includedStatuses)
|
||||
}
|
||||
}
|
||||
|
@@ -29,6 +29,7 @@ data class Manga(
|
||||
val initialized: Boolean,
|
||||
val lastModifiedAt: Long,
|
||||
val favoriteModifiedAt: Long?,
|
||||
val version: Long,
|
||||
) : Serializable {
|
||||
|
||||
val expectedNextUpdate: Instant?
|
||||
@@ -122,6 +123,7 @@ data class Manga(
|
||||
initialized = false,
|
||||
lastModifiedAt = 0L,
|
||||
favoriteModifiedAt = null,
|
||||
version = 0L,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ data class MangaUpdate(
|
||||
val thumbnailUrl: String? = null,
|
||||
val updateStrategy: UpdateStrategy? = null,
|
||||
val initialized: Boolean? = null,
|
||||
val version: Long? = null,
|
||||
)
|
||||
|
||||
fun Manga.toMangaUpdate(): MangaUpdate {
|
||||
@@ -47,5 +48,6 @@ fun Manga.toMangaUpdate(): MangaUpdate {
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
updateStrategy = updateStrategy,
|
||||
initialized = initialized,
|
||||
version = version,
|
||||
)
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ interface MangaRepository {
|
||||
|
||||
suspend fun getDuplicateLibraryManga(id: Long, title: String): List<Manga>
|
||||
|
||||
suspend fun getUpcomingManga(): List<Manga>
|
||||
suspend fun getUpcomingManga(statues: Set<Long>): List<Manga>
|
||||
|
||||
suspend fun resetViewerFlags(): Boolean
|
||||
|
||||
|
@@ -4,10 +4,13 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
|
||||
interface SourceManager {
|
||||
|
||||
val isInitialized: StateFlow<Boolean>
|
||||
|
||||
val catalogueSources: Flow<List<CatalogueSource>>
|
||||
|
||||
fun get(sourceKey: Long): Source?
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package tachiyomi.domain.track.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class Track(
|
||||
val id: Long,
|
||||
val mangaId: Long,
|
||||
@@ -14,4 +16,4 @@ data class Track(
|
||||
val remoteUrl: String,
|
||||
val startDate: Long,
|
||||
val finishDate: Long,
|
||||
)
|
||||
) : Serializable
|
||||
|
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp_version = "8.2.2"
|
||||
agp_version = "8.3.1"
|
||||
lifecycle_version = "2.7.0"
|
||||
paging_version = "3.2.1"
|
||||
|
||||
@@ -28,7 +28,7 @@ paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pag
|
||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.3"
|
||||
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha03"
|
||||
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha03"
|
||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-beta01"
|
||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0"
|
||||
|
||||
[bundles]
|
||||
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
|
||||
|
@@ -1,9 +1,11 @@
|
||||
[versions]
|
||||
compiler = "1.5.8"
|
||||
compose-bom = "2024.01.00-alpha03"
|
||||
accompanist = "0.34.0"
|
||||
compiler = "1.5.11"
|
||||
compose-bom = "2024.02.00-alpha02"
|
||||
accompanist = "0.35.0-alpha"
|
||||
|
||||
[libraries]
|
||||
compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compiler" }
|
||||
|
||||
activity = "androidx.activity:activity-compose:1.8.2"
|
||||
bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
foundation = { module = "androidx.compose.foundation:foundation" }
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
kotlin_version = "1.9.22"
|
||||
serialization_version = "1.6.2"
|
||||
kotlin_version = "1.9.23"
|
||||
serialization_version = "1.6.3"
|
||||
xml_serialization_version = "0.86.3"
|
||||
|
||||
[libraries]
|
||||
@@ -9,7 +9,7 @@ gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "
|
||||
|
||||
immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.7" }
|
||||
|
||||
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.3" }
|
||||
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.8.0" }
|
||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
|
||||
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
|
||||
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }
|
||||
|
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
aboutlib_version = "10.10.0"
|
||||
aboutlib_version = "11.1.0"
|
||||
leakcanary = "2.13"
|
||||
moko = "0.23.0"
|
||||
okhttp_version = "5.0.0-alpha.12"
|
||||
@@ -8,13 +8,13 @@ shizuku_version = "12.2.0"
|
||||
sqldelight = "2.0.0"
|
||||
sqlite = "2.4.0"
|
||||
voyager = "1.0.0"
|
||||
detekt = "1.23.1"
|
||||
detektCompose = "0.3.11"
|
||||
detekt = "1.23.5"
|
||||
detektCompose = "0.3.12"
|
||||
|
||||
[libraries]
|
||||
desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
|
||||
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
||||
google-services-gradle = "com.google.gms:google-services:4.4.0"
|
||||
google-services-gradle = "com.google.gms:google-services:4.4.1"
|
||||
|
||||
rxjava = "io.reactivex:rxjava:1.3.8"
|
||||
|
||||
@@ -22,7 +22,7 @@ okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_ve
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
|
||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp_version" }
|
||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
|
||||
okio = "com.squareup.okio:okio:3.7.0"
|
||||
okio = "com.squareup.okio:okio:3.9.0"
|
||||
|
||||
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
|
||||
|
||||
@@ -32,7 +32,7 @@ jsoup = "org.jsoup:jsoup:1.17.2"
|
||||
|
||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||
unifile = "com.github.tachiyomiorg:unifile:7c257e1c64"
|
||||
common-compress = "org.apache.commons:commons-compress:1.25.0"
|
||||
common-compress = "org.apache.commons:commons-compress:1.26.1"
|
||||
junrar = "com.github.junrar:junrar:7.5.5"
|
||||
|
||||
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
||||
@@ -43,13 +43,14 @@ preferencektx = "androidx.preference:preference-ktx:1.2.1"
|
||||
|
||||
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
|
||||
|
||||
coil-bom = { module = "io.coil-kt:coil-bom", version = "2.5.0" }
|
||||
coil-core = { module = "io.coil-kt:coil" }
|
||||
coil-gif = { module = "io.coil-kt:coil-gif" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose" }
|
||||
coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.0-alpha06" }
|
||||
coil-core = { module = "io.coil-kt.coil3:coil" }
|
||||
coil-gif = { module = "io.coil-kt.coil3:coil-gif" }
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose" }
|
||||
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" }
|
||||
|
||||
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:7e57335"
|
||||
image-decoder = "com.github.tachiyomiorg:image-decoder:fbd6601290"
|
||||
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:aeaa170036"
|
||||
image-decoder = "com.github.tachiyomiorg:image-decoder:e08e9be535"
|
||||
|
||||
natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
|
||||
|
||||
@@ -63,14 +64,14 @@ directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
||||
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.2.0"
|
||||
|
||||
swipe = "me.saket.swipe:swipe:1.2.0"
|
||||
swipe = "me.saket.swipe:swipe:1.3.0"
|
||||
|
||||
moko-core = { module = "dev.icerock.moko:resources", version.ref = "moko" }
|
||||
moko-gradle = { module = "dev.icerock.moko:resources-generator", version.ref = "moko" }
|
||||
|
||||
logcat = "com.squareup.logcat:logcat:0.1"
|
||||
|
||||
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.0"
|
||||
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.6.1"
|
||||
|
||||
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
|
||||
aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" }
|
||||
@@ -87,9 +88,9 @@ sqldelight-android-paging = { module = "app.cash.sqldelight:androidx-paging3-ext
|
||||
sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
|
||||
sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "sqldelight" }
|
||||
|
||||
junit = "org.junit.jupiter:junit-jupiter:5.10.1"
|
||||
kotest-assertions = "io.kotest:kotest-assertions-core:5.8.0"
|
||||
mockk = "io.mockk:mockk:1.13.9"
|
||||
junit = "org.junit.jupiter:junit-jupiter:5.10.2"
|
||||
kotest-assertions = "io.kotest:kotest-assertions-core:5.8.1"
|
||||
mockk = "io.mockk:mockk:1.13.10"
|
||||
|
||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
|
||||
@@ -105,9 +106,9 @@ archive = ["common-compress", "junrar"]
|
||||
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]
|
||||
js-engine = ["quickjs-android"]
|
||||
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
|
||||
coil = ["coil-core", "coil-gif", "coil-compose"]
|
||||
coil = ["coil-core", "coil-gif", "coil-compose", "coil-network-okhttp"]
|
||||
shizuku = ["shizuku-api", "shizuku-provider"]
|
||||
sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"]
|
||||
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]
|
||||
richtext = ["richtext-commonmark", "richtext-m3"]
|
||||
test = ["junit", "kotest-assertions", "mockk"]
|
||||
test = ["junit", "kotest-assertions", "mockk"]
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user