mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 23:12:48 +01:00
chore: merge upstream.
This commit is contained in:
commit
2eef0dd939
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -5,7 +5,7 @@ I acknowledge that:
|
|||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.14.6)
|
- To the latest version of the app (stable is v0.14.6)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have gone through the FAQ (https://tachiyomi.org/help/faq/) and troubleshooting guide (https://tachiyomi.org/help/guides/troubleshooting/)
|
- I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/)
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
||||||
- I will fill out the title and the information in this template
|
- I will fill out the title and the information in this template
|
||||||
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -4,8 +4,8 @@ contact_links:
|
|||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
||||||
- name: 📦 Tachiyomi extensions
|
- name: 📦 Tachiyomi extensions
|
||||||
url: https://tachiyomi.org/extensions
|
url: https://tachiyomi.org/extensions/
|
||||||
about: List of all available extensions with download links
|
about: List of all available extensions with download links
|
||||||
- name: 🖥️ Tachiyomi website
|
- name: 🖥️ Tachiyomi website
|
||||||
url: https://tachiyomi.org/help/
|
url: https://tachiyomi.org/
|
||||||
about: Guides, troubleshooting, and answers to common questions
|
about: Guides, troubleshooting, and answers to common questions
|
||||||
|
2
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
2
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -96,7 +96,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have gone through the [FAQ](https://tachiyomi.org/help/faq/) and [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
- label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
|
4
.github/workflows/build_pull_request.yml
vendored
4
.github/workflows/build_pull_request.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
@ -36,4 +36,4 @@ jobs:
|
|||||||
- name: Build app and run unit tests
|
- name: Build app and run unit tests
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
|
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
14
.github/workflows/build_push.yml
vendored
14
.github/workflows/build_push.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
@ -31,7 +31,7 @@ jobs:
|
|||||||
- name: Build app and run unit tests
|
- name: Build app and run unit tests
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
|
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
# Sign APK and create release for tags
|
||||||
|
|
||||||
@ -104,3 +104,13 @@ jobs:
|
|||||||
prerelease: false
|
prerelease: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
update-website:
|
||||||
|
needs: [build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
steps:
|
||||||
|
- name: Trigger Netlify build hook
|
||||||
|
run: curl -s -X POST -d {} "https://api.netlify.com/build_hooks/${TOKEN}"
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.NETLIFY_HOOK_RELEASE }}
|
||||||
|
2
.github/workflows/issue_moderator.yml
vendored
2
.github/workflows/issue_moderator.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
|||||||
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
|
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
|
||||||
"ignoreCase": true,
|
"ignoreCase": true,
|
||||||
"labels": ["Cloudflare protected"],
|
"labels": ["Cloudflare protected"],
|
||||||
"message": "Refer to the **Solving Cloudflare issues** section at https://tachiyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
"message": "Refer to the **Solving Cloudflare issues** section at https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
auto-close-ignore-label: do-not-autoclose
|
auto-close-ignore-label: do-not-autoclose
|
||||||
|
@ -30,7 +30,7 @@ Before you start, please note that the ability to use following technologies is
|
|||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/help/contribution/#translation) for more details.
|
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/docs/contribute#translation) for more details.
|
||||||
|
|
||||||
|
|
||||||
# Forks
|
# Forks
|
||||||
|
@ -29,7 +29,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Issues</summary>
|
<details><summary>Issues</summary>
|
||||||
|
|
||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://tachiyomi.org/changelogs/) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
||||||
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
|
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
@ -104,7 +103,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
resources.excludes.addAll(listOf(
|
resources.excludes.addAll(
|
||||||
|
listOf(
|
||||||
"META-INF/DEPENDENCIES",
|
"META-INF/DEPENDENCIES",
|
||||||
"LICENSE.txt",
|
"LICENSE.txt",
|
||||||
"META-INF/LICENSE",
|
"META-INF/LICENSE",
|
||||||
@ -112,7 +112,8 @@ android {
|
|||||||
"META-INF/README.md",
|
"META-INF/README.md",
|
||||||
"META-INF/NOTICE",
|
"META-INF/NOTICE",
|
||||||
"META-INF/*.kotlin_module",
|
"META-INF/*.kotlin_module",
|
||||||
))
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
@ -267,7 +268,9 @@ androidComponents {
|
|||||||
beforeVariants { variantBuilder ->
|
beforeVariants { variantBuilder ->
|
||||||
// Disables standardBenchmark
|
// Disables standardBenchmark
|
||||||
if (variantBuilder.buildType == "benchmark") {
|
if (variantBuilder.buildType == "benchmark") {
|
||||||
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
|
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
|
||||||
|
listOf("default" to "dev"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onVariants(selector().withFlavor("default" to "standard")) {
|
onVariants(selector().withFlavor("default" to "standard")) {
|
||||||
@ -278,10 +281,6 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
withType<LintTask>().configureEach {
|
|
||||||
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||||
withType<KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
@ -306,12 +305,12 @@ tasks {
|
|||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-P",
|
"-P",
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||||
project.buildDir.absolutePath + "/compose_metrics"
|
project.buildDir.absolutePath + "/compose_metrics",
|
||||||
)
|
)
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-P",
|
"-P",
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||||
project.buildDir.absolutePath + "/compose_metrics"
|
project.buildDir.absolutePath + "/compose_metrics",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,20 +155,6 @@
|
|||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="tachiyomi.presentation.widget.UpdatesGridGlanceReceiver"
|
|
||||||
android:enabled="@bool/glance_appwidget_available"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/label_recent_updates">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.appwidget.provider"
|
|
||||||
android:resource="@xml/updates_grid_glance_widget_info" />
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.download.DownloadService"
|
android:name=".data.download.DownloadService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
@ -50,6 +50,7 @@ import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
|||||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||||
import tachiyomi.domain.history.repository.HistoryRepository
|
import tachiyomi.domain.history.repository.HistoryRepository
|
||||||
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
||||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||||
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||||
@ -57,7 +58,6 @@ import tachiyomi.domain.manga.interactor.GetManga
|
|||||||
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||||
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
|
||||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
@ -102,7 +102,7 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetNextChapters(get(), get(), get()) }
|
addFactory { GetNextChapters(get(), get(), get()) }
|
||||||
addFactory { ResetViewerFlags(get()) }
|
addFactory { ResetViewerFlags(get()) }
|
||||||
addFactory { SetMangaChapterFlags(get()) }
|
addFactory { SetMangaChapterFlags(get()) }
|
||||||
addFactory { SetFetchInterval(get()) }
|
addFactory { FetchInterval(get()) }
|
||||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||||
addFactory { SetMangaViewerFlags(get()) }
|
addFactory { SetMangaViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalManga(get()) }
|
addFactory { NetworkToLocalManga(get()) }
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.domain.manga.interactor
|
|||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
@ -15,7 +15,7 @@ import java.util.Date
|
|||||||
|
|
||||||
class UpdateManga(
|
class UpdateManga(
|
||||||
private val mangaRepository: MangaRepository,
|
private val mangaRepository: MangaRepository,
|
||||||
private val setFetchInterval: SetFetchInterval,
|
private val fetchInterval: FetchInterval,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||||
@ -79,9 +79,9 @@ class UpdateManga(
|
|||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
window: Pair<Long, Long> = setFetchInterval.getWindow(dateTime),
|
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return setFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
return fetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||||
?.let { mangaRepository.update(it) }
|
?.let { mangaRepository.update(it) }
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
|
@ -162,7 +162,7 @@ fun CategoryDeleteDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onDelete()
|
onDelete()
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -217,7 +217,7 @@ fun ChangeCategoryDialog(
|
|||||||
tachiyomi.presentation.core.components.material.TextButton(onClick = {
|
tachiyomi.presentation.core.components.material.TextButton(onClick = {
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
onEditCategories()
|
onEditCategories()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_edit))
|
Text(text = stringResource(R.string.action_edit))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
@ -3,8 +3,6 @@ package eu.kanade.presentation.components
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
|
||||||
import tachiyomi.presentation.core.components.ListGroupHeader
|
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@ -15,11 +13,10 @@ fun RelativeDateHeader(
|
|||||||
date: Date,
|
date: Date,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
ListGroupHeader(
|
ListGroupHeader(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = remember {
|
text = remember {
|
||||||
date.toRelativeString(context, dateFormat)
|
dateFormat.format(date)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ fun HistoryDeleteDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onDelete(removeEverything)
|
onDelete(removeEverything)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_remove))
|
Text(text = stringResource(R.string.action_remove))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -90,7 +90,7 @@ fun HistoryDeleteAllDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onDelete()
|
onDelete()
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -63,7 +63,6 @@ import eu.kanade.tachiyomi.data.download.model.Download
|
|||||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||||
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.service.missingChaptersCount
|
import tachiyomi.domain.chapter.service.missingChaptersCount
|
||||||
@ -740,7 +739,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
date = chapterItem.chapter.dateUpload
|
date = chapterItem.chapter.dateUpload
|
||||||
.takeIf { it > 0L }
|
.takeIf { it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
Date(it).toRelativeString(context, dateFormat)
|
dateFormat.format(Date(it))
|
||||||
},
|
},
|
||||||
readProgress = chapterItem.chapter.lastPageRead
|
readProgress = chapterItem.chapter.lastPageRead
|
||||||
.takeIf { !chapterItem.chapter.read && it > 0L }
|
.takeIf { !chapterItem.chapter.read && it > 0L }
|
||||||
|
@ -143,7 +143,7 @@ fun MangaBottomActionMenu(
|
|||||||
if (onMarkPreviousAsReadClicked != null) {
|
if (onMarkPreviousAsReadClicked != null) {
|
||||||
Button(
|
Button(
|
||||||
title = stringResource(R.string.action_mark_previous_as_read),
|
title = stringResource(R.string.action_mark_previous_as_read),
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.ic_done_prev_24dp),
|
icon = ImageVector.vectorResource(R.drawable.ic_done_prev_24dp),
|
||||||
toConfirm = confirm[4],
|
toConfirm = confirm[4],
|
||||||
onLongClick = { onLongClickItem(4) },
|
onLongClick = { onLongClickItem(4) },
|
||||||
onClick = onMarkPreviousAsReadClicked,
|
onClick = onMarkPreviousAsReadClicked,
|
||||||
|
@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.domain.manga.interactor.MAX_FETCH_INTERVAL
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -67,7 +67,7 @@ fun SetIntervalDialog(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||||
val items = (0..MAX_FETCH_INTERVAL).map {
|
val items = (0..FetchInterval.MAX_INTERVAL).map {
|
||||||
if (it == 0) {
|
if (it == 0) {
|
||||||
stringResource(R.string.label_default)
|
stringResource(R.string.label_default)
|
||||||
} else {
|
} else {
|
||||||
@ -91,7 +91,7 @@ fun SetIntervalDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onValueChanged(selectedInterval)
|
onValueChanged(selectedInterval)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -62,7 +62,7 @@ fun MoreScreen(
|
|||||||
WarningBanner(
|
WarningBanner(
|
||||||
textRes = R.string.fdroid_warning,
|
textRes = R.string.fdroid_warning,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
uriHandler.openUri("https://tachiyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version")
|
uriHandler.openUri("https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.more.settings.Preference.PreferenceItem
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import tachiyomi.core.preference.Preference as PreferenceData
|
import tachiyomi.core.preference.Preference as PreferenceData
|
@ -211,7 +211,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
|
|||||||
showCreateDialog = false
|
showCreateDialog = false
|
||||||
flag = it
|
flag = it
|
||||||
try {
|
try {
|
||||||
chooseBackupDir.launch(Backup.getBackupFilename())
|
chooseBackupDir.launch(Backup.getFilename())
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
flag = 0
|
flag = 0
|
||||||
context.toast(R.string.file_picker_error)
|
context.toast(R.string.file_picker_error)
|
||||||
|
@ -70,7 +70,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun RowScope.AppBarAction() {
|
override fun RowScope.AppBarAction() {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/help/guides/tracking/") }) {
|
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.HelpOutline,
|
imageVector = Icons.Outlined.HelpOutline,
|
||||||
contentDescription = stringResource(R.string.tracking_guide),
|
contentDescription = stringResource(R.string.tracking_guide),
|
||||||
|
@ -149,7 +149,7 @@ object AboutScreen : Screen() {
|
|||||||
item {
|
item {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(R.string.help_translate),
|
title = stringResource(R.string.help_translate),
|
||||||
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/help/contribution/#translation") },
|
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/docs/contribute#translation") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ object AboutScreen : Screen() {
|
|||||||
item {
|
item {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(R.string.privacy_policy),
|
title = stringResource(R.string.privacy_policy),
|
||||||
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy") },
|
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy/") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,9 +41,9 @@ class OpenSourceLicensesScreen : Screen() {
|
|||||||
),
|
),
|
||||||
onLibraryClick = {
|
onLibraryClick = {
|
||||||
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
||||||
name = it.name,
|
name = it.library.name,
|
||||||
website = it.website,
|
website = it.library.website,
|
||||||
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||||
)
|
)
|
||||||
navigator.push(libraryLicenseScreen)
|
navigator.push(libraryLicenseScreen)
|
||||||
},
|
},
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
package eu.kanade.presentation.reader
|
package eu.kanade.presentation.reader
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.domain.manga.model.orientationType
|
import eu.kanade.domain.manga.model.orientationType
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
import tachiyomi.presentation.core.components.SettingsIconGrid
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.IconToggleButton
|
||||||
|
|
||||||
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
|
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
|
||||||
|
|
||||||
@ -32,22 +32,20 @@ fun OrientationModeSelectDialog(
|
|||||||
val manga by screenModel.mangaFlow.collectAsState()
|
val manga by screenModel.mangaFlow.collectAsState()
|
||||||
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
|
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||||
onDismissRequest = onDismissRequest,
|
Box(modifier = Modifier.padding(vertical = 16.dp)) {
|
||||||
) {
|
SettingsIconGrid(R.string.rotation_type) {
|
||||||
Row(
|
items(orientationTypeOptions) { (stringRes, mode) ->
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
IconToggleButton(
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
checked = mode == orientationType,
|
||||||
) {
|
onCheckedChange = {
|
||||||
SettingsChipRow(R.string.rotation_type) {
|
screenModel.onChangeOrientation(mode)
|
||||||
orientationTypeOptions.map { (stringRes, it) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = it == orientationType,
|
|
||||||
onClick = {
|
|
||||||
screenModel.onChangeOrientation(it)
|
|
||||||
onChange(stringRes)
|
onChange(stringRes)
|
||||||
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(stringRes)) },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
imageVector = ImageVector.vectorResource(mode.iconRes),
|
||||||
|
title = stringResource(stringRes),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
package eu.kanade.presentation.reader
|
package eu.kanade.presentation.reader
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.res.vectorResource
|
||||||
import eu.kanade.domain.manga.model.readingModeType
|
import eu.kanade.domain.manga.model.readingModeType
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
import tachiyomi.presentation.core.components.SettingsIconGrid
|
||||||
|
import tachiyomi.presentation.core.components.material.IconToggleButton
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
|
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
|
||||||
@ -32,22 +33,20 @@ fun ReadingModeSelectDialog(
|
|||||||
val manga by screenModel.mangaFlow.collectAsState()
|
val manga by screenModel.mangaFlow.collectAsState()
|
||||||
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
|
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||||
onDismissRequest = onDismissRequest,
|
Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) {
|
||||||
) {
|
SettingsIconGrid(R.string.pref_category_reading_mode) {
|
||||||
Row(
|
items(readingModeOptions) { (stringRes, mode) ->
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
IconToggleButton(
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
checked = mode == readingMode,
|
||||||
) {
|
onCheckedChange = {
|
||||||
SettingsChipRow(R.string.pref_category_reading_mode) {
|
screenModel.onChangeReadingMode(mode)
|
||||||
readingModeOptions.map { (stringRes, it) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = it == readingMode,
|
|
||||||
onClick = {
|
|
||||||
screenModel.onChangeReadingMode(it)
|
|
||||||
onChange(stringRes)
|
onChange(stringRes)
|
||||||
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(stringRes)) },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
imageVector = ImageVector.vectorResource(mode.iconRes),
|
||||||
|
title = stringResource(stringRes),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -223,6 +223,7 @@ private fun SearchResultItem(
|
|||||||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 12.dp)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
@ -21,7 +21,7 @@ fun UpdatesDeleteConfirmationDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onConfirm()
|
onConfirm()
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -175,7 +175,7 @@ fun WebViewScreenContent(
|
|||||||
WarningBanner(
|
WarningBanner(
|
||||||
textRes = R.string.information_cloudflare_help,
|
textRes = R.string.information_cloudflare_help,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
uriHandler.openUri("https://tachiyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues")
|
uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -93,15 +93,14 @@ class BackupManager(
|
|||||||
|
|
||||||
// Delete older backups
|
// Delete older backups
|
||||||
val numberOfBackups = backupPreferences.numberOfBackups().get()
|
val numberOfBackups = backupPreferences.numberOfBackups().get()
|
||||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
|
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
|
||||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.sortedByDescending { it.name }
|
.sortedByDescending { it.name }
|
||||||
.drop(numberOfBackups - 1)
|
.drop(numberOfBackups - 1)
|
||||||
.forEach { it.delete() }
|
.forEach { it.delete() }
|
||||||
|
|
||||||
// Create new file to place backup
|
// Create new file to place backup
|
||||||
dir.createFile(Backup.getBackupFilename())
|
dir.createFile(Backup.getFilename())
|
||||||
} else {
|
} else {
|
||||||
UniFile.fromUri(context, uri)
|
UniFile.fromUri(context, uri)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import kotlinx.coroutines.coroutineScope
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -31,10 +31,10 @@ class BackupRestorer(
|
|||||||
) {
|
) {
|
||||||
private val updateManga: UpdateManga = Injekt.get()
|
private val updateManga: UpdateManga = Injekt.get()
|
||||||
private val chapterRepository: ChapterRepository = Injekt.get()
|
private val chapterRepository: ChapterRepository = Injekt.get()
|
||||||
private val setFetchInterval: SetFetchInterval = Injekt.get()
|
private val fetchInterval: FetchInterval = Injekt.get()
|
||||||
|
|
||||||
private var now = ZonedDateTime.now()
|
private var now = ZonedDateTime.now()
|
||||||
private var currentFetchWindow = setFetchInterval.getWindow(now)
|
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||||
|
|
||||||
private var backupManager = BackupManager(context)
|
private var backupManager = BackupManager(context)
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ class BackupRestorer(
|
|||||||
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
||||||
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
||||||
now = ZonedDateTime.now()
|
now = ZonedDateTime.now()
|
||||||
currentFetchWindow = setFetchInterval.getWindow(now)
|
currentFetchWindow = fetchInterval.getWindow(now)
|
||||||
|
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -16,9 +17,11 @@ data class Backup(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getBackupFilename(): String {
|
val filenameRegex = """${BuildConfig.APPLICATION_ID}_\d+-\d+-\d+_\d+-\d+.tachibk""".toRegex()
|
||||||
|
|
||||||
|
fun getFilename(): String {
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
return "tachiyomi_$date.proto.gz"
|
return "${BuildConfig.APPLICATION_ID}_$date.tachibk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,11 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import logcat.LogPriority
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -97,6 +99,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
editor.commit()
|
editor.commit()
|
||||||
editor.abortUnlessCommitted()
|
editor.abortUnlessCommitted()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.WARN, e) { "Failed to put page list to cache" }
|
||||||
// Ignore.
|
// Ignore.
|
||||||
} finally {
|
} finally {
|
||||||
editor?.abortUnlessCommitted()
|
editor?.abortUnlessCommitted()
|
||||||
@ -174,7 +177,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
* @return status of deletion for the file.
|
* @return status of deletion for the file.
|
||||||
*/
|
*/
|
||||||
private fun removeFileFromCache(file: String): Boolean {
|
private fun removeFileFromCache(file: String): Boolean {
|
||||||
// Make sure we don't delete the journal file (keeps track of cache).
|
// Make sure we don't delete the journal file (keeps track of cache)
|
||||||
if (file == "journal" || file.startsWith("journal.")) {
|
if (file == "journal" || file.startsWith("journal.")) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -182,9 +185,10 @@ class ChapterCache(private val context: Context) {
|
|||||||
return try {
|
return try {
|
||||||
// Remove the extension from the file to get the key of the cache
|
// Remove the extension from the file to get the key of the cache
|
||||||
val key = file.substringBeforeLast(".")
|
val key = file.substringBeforeLast(".")
|
||||||
// Remove file from cache.
|
// Remove file from cache
|
||||||
diskCache.remove(key)
|
diskCache.remove(key)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.WARN, e) { "Failed to remove file from cache" }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,6 @@ import nl.adaptivity.xmlutil.serialization.XML
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNow
|
import tachiyomi.core.util.lang.launchNow
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
@ -363,7 +362,7 @@ class Downloader(
|
|||||||
if (page.imageUrl.isNullOrEmpty()) {
|
if (page.imageUrl.isNullOrEmpty()) {
|
||||||
page.status = Page.State.LOAD_PAGE
|
page.status = Page.State.LOAD_PAGE
|
||||||
try {
|
try {
|
||||||
page.imageUrl = download.source.fetchImageUrl(page).awaitSingle()
|
page.imageUrl = download.source.getImageUrl(page)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
page.status = Page.State.ERROR
|
page.status = Page.State.ERROR
|
||||||
}
|
}
|
||||||
|
@ -59,9 +59,9 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U
|
|||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
||||||
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.toMangaUpdate
|
import tachiyomi.domain.manga.model.toMangaUpdate
|
||||||
import tachiyomi.domain.source.model.SourceNotInstalledException
|
import tachiyomi.domain.source.model.SourceNotInstalledException
|
||||||
@ -90,7 +90,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
private val getCategories: GetCategories = Injekt.get()
|
private val getCategories: GetCategories = Injekt.get()
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
||||||
private val refreshTracks: RefreshTracks = Injekt.get()
|
private val refreshTracks: RefreshTracks = Injekt.get()
|
||||||
private val setFetchInterval: SetFetchInterval = Injekt.get()
|
private val fetchInterval: FetchInterval = Injekt.get()
|
||||||
|
|
||||||
private val notifier = LibraryUpdateNotifier(context)
|
private val notifier = LibraryUpdateNotifier(context)
|
||||||
|
|
||||||
@ -216,7 +216,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
|
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
|
||||||
val fetchWindow = setFetchInterval.getWindow(ZonedDateTime.now())
|
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
mangaToUpdate.groupBy { it.manga.source }.values
|
mangaToUpdate.groupBy { it.manga.source }.values
|
||||||
@ -497,7 +497,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
private const val WORK_NAME_AUTO = "LibraryUpdate-auto"
|
private const val WORK_NAME_AUTO = "LibraryUpdate-auto"
|
||||||
private const val WORK_NAME_MANUAL = "LibraryUpdate-manual"
|
private const val WORK_NAME_MANUAL = "LibraryUpdate-manual"
|
||||||
|
|
||||||
private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/help/guides/troubleshooting"
|
private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/docs/guides/troubleshooting/"
|
||||||
|
|
||||||
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
||||||
|
|
||||||
|
@ -329,11 +329,11 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HELP_WARNING_URL = "https://tachiyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads"
|
const val HELP_WARNING_URL = "https://tachiyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val NOTIF_MAX_CHAPTERS = 5
|
private const val NOTIF_MAX_CHAPTERS = 5
|
||||||
private const val NOTIF_TITLE_MAX_LEN = 45
|
private const val NOTIF_TITLE_MAX_LEN = 45
|
||||||
private const val NOTIF_ICON_SIZE = 192
|
private const val NOTIF_ICON_SIZE = 192
|
||||||
private const val HELP_SKIPPED_URL = "https://tachiyomi.org/help/faq/#why-does-global-update-skip-some-entries"
|
private const val HELP_SKIPPED_URL = "https://tachiyomi.org/docs/faq/library#why-is-global-update-skipping-entries"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.saver
|
package eu.kanade.tachiyomi.data.saver
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@ -28,30 +27,59 @@ class ImageSaver(
|
|||||||
val context: Context,
|
val context: Context,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
fun save(image: Image): Uri {
|
fun save(image: Image): Uri {
|
||||||
val data = image.data
|
val data = image.data
|
||||||
|
|
||||||
val type = ImageUtil.findImageType(data) ?: throw Exception("Not an image")
|
val type = ImageUtil.findImageType(data) ?: throw IllegalArgumentException("Not an image")
|
||||||
val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}")
|
val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}")
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
|
||||||
return save(data(), image.location.directory(context), filename)
|
return save(data(), image.location.directory(context), filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return saveApi29(image, type, filename, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
|
||||||
|
directory.mkdirs()
|
||||||
|
|
||||||
|
val destFile = File(directory, filename)
|
||||||
|
|
||||||
|
inputStream.use { input ->
|
||||||
|
destFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DiskUtil.scanMedia(context, destFile.toUri())
|
||||||
|
|
||||||
|
return destFile.getUriCompat(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
private fun saveApi29(
|
||||||
|
image: Image,
|
||||||
|
type: ImageUtil.ImageType,
|
||||||
|
filename: String,
|
||||||
|
data: () -> InputStream,
|
||||||
|
): Uri {
|
||||||
val pictureDir =
|
val pictureDir =
|
||||||
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||||
|
|
||||||
val folderRelativePath = "${Environment.DIRECTORY_PICTURES}/${context.getString(R.string.app_name)}/"
|
|
||||||
val imageLocation = (image.location as Location.Pictures).relativePath
|
val imageLocation = (image.location as Location.Pictures).relativePath
|
||||||
|
val relativePath = listOf(
|
||||||
|
Environment.DIRECTORY_PICTURES,
|
||||||
|
context.getString(R.string.app_name),
|
||||||
|
imageLocation,
|
||||||
|
).joinToString(File.separator)
|
||||||
|
|
||||||
val contentValues = contentValuesOf(
|
val contentValues = contentValuesOf(
|
||||||
|
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
|
||||||
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
||||||
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
||||||
MediaStore.Images.Media.RELATIVE_PATH to folderRelativePath + imageLocation,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val picture = findUriOrDefault(folderRelativePath, "$imageLocation$filename") {
|
val picture = findUriOrDefault(relativePath, filename) {
|
||||||
context.contentResolver.insert(
|
context.contentResolver.insert(
|
||||||
pictureDir,
|
pictureDir,
|
||||||
contentValues,
|
contentValues,
|
||||||
@ -74,49 +102,34 @@ class ImageSaver(
|
|||||||
return picture
|
return picture
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
|
|
||||||
directory.mkdirs()
|
|
||||||
|
|
||||||
val destFile = File(directory, filename)
|
|
||||||
|
|
||||||
inputStream.use { input ->
|
|
||||||
destFile.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DiskUtil.scanMedia(context, destFile.toUri())
|
|
||||||
|
|
||||||
return destFile.getUriCompat(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private fun findUriOrDefault(relativePath: String, imagePath: String, default: () -> Uri): Uri {
|
private fun findUriOrDefault(path: String, filename: String, default: () -> Uri): Uri {
|
||||||
val projection = arrayOf(
|
val projection = arrayOf(
|
||||||
MediaStore.MediaColumns._ID,
|
MediaStore.MediaColumns._ID,
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
MediaStore.Images.Media.MIME_TYPE,
|
|
||||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=? AND ${MediaStore.MediaColumns.DISPLAY_NAME}=?"
|
val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=? AND ${MediaStore.MediaColumns.DISPLAY_NAME}=?"
|
||||||
|
|
||||||
|
// Need to make sure it ends with the separator
|
||||||
|
val normalizedPath = "${path.removeSuffix(File.separator)}${File.separator}"
|
||||||
|
|
||||||
context.contentResolver.query(
|
context.contentResolver.query(
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
projection,
|
projection,
|
||||||
selection,
|
selection,
|
||||||
arrayOf(relativePath, imagePath),
|
arrayOf(normalizedPath, filename),
|
||||||
null,
|
null,
|
||||||
).use { cursor ->
|
).use { cursor ->
|
||||||
if (cursor != null && cursor.count >= 1) {
|
if (cursor != null && cursor.count >= 1) {
|
||||||
cursor.moveToFirst().let {
|
if (cursor.moveToFirst()) {
|
||||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
||||||
|
|
||||||
return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return default()
|
return default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita"
|
|||||||
.reduce(Long::or) and Long.MAX_VALUE
|
.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
val preferences: SharedPreferences by lazy {
|
val preferences: SharedPreferences by lazy {
|
||||||
context.getSharedPreferences("source_$sourceSuffixID", 0x0000)
|
context.getSharedPreferences("source_$sourceSuffixID", Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
val prefApiUrl = preferences.getString("APIURL", "")!!
|
val prefApiUrl = preferences.getString("APIURL", "")!!
|
||||||
if (prefApiUrl.isEmpty()) {
|
if (prefApiUrl.isEmpty()) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
@ -107,7 +108,7 @@ class TachideskApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$tachideskExtensionId", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$tachideskExtensionId", Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
|
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
|
||||||
|
@ -143,7 +143,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.update_check_notification_update_available))
|
setContentTitle(context.getString(R.string.update_check_notification_update_available))
|
||||||
setContentText(context.getString(R.string.update_check_fdroid_migration_info))
|
setContentText(context.getString(R.string.update_check_fdroid_migration_info))
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
setContentIntent(NotificationHandler.openUrl(context, "https://tachiyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version"))
|
setContentIntent(NotificationHandler.openUrl(context, "https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds"))
|
||||||
}
|
}
|
||||||
notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT)
|
notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT)
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ class ExtensionDetailsScreenModel(
|
|||||||
val extension = state.value.extension ?: return ""
|
val extension = state.value.extension ?: return ""
|
||||||
|
|
||||||
if (!extension.hasReadme) {
|
if (!extension.hasReadme) {
|
||||||
return "https://tachiyomi.org/help/faq/#extensions"
|
return "https://tachiyomi.org/docs/faq/browse/extensions"
|
||||||
}
|
}
|
||||||
|
|
||||||
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
||||||
|
@ -31,7 +31,7 @@ fun Screen.migrateSourceTab(): TabContent {
|
|||||||
title = stringResource(R.string.migration_help_guide),
|
title = stringResource(R.string.migration_help_guide),
|
||||||
icon = Icons.Outlined.HelpOutline,
|
icon = Icons.Outlined.HelpOutline,
|
||||||
onClick = {
|
onClick = {
|
||||||
uriHandler.openUri("https://tachiyomi.org/help/guides/source-migration/")
|
uriHandler.openUri("https://tachiyomi.org/docs/guides/source-migration")
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -9,6 +9,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.filter
|
||||||
import androidx.paging.map
|
import androidx.paging.map
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.coroutineScope
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
@ -30,7 +31,6 @@ import eu.kanade.tachiyomi.util.removeCovers
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -113,25 +113,20 @@ class BrowseSourceScreenModel(
|
|||||||
/**
|
/**
|
||||||
* Flow of Pager flow tied to [State.listing]
|
* Flow of Pager flow tied to [State.listing]
|
||||||
*/
|
*/
|
||||||
|
private val hideInLibraryItems = sourcePreferences.hideInLibraryItems().get()
|
||||||
val mangaPagerFlowFlow = state.map { it.listing }
|
val mangaPagerFlowFlow = state.map { it.listing }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.map { listing ->
|
.map { listing ->
|
||||||
Pager(
|
Pager(PagingConfig(pageSize = 25)) {
|
||||||
PagingConfig(pageSize = 25),
|
|
||||||
) {
|
|
||||||
getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters)
|
getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters)
|
||||||
}.flow.map { pagingData ->
|
}.flow.map { pagingData ->
|
||||||
pagingData.map {
|
pagingData.map {
|
||||||
networkToLocalManga.await(it.toDomainManga(sourceId))
|
networkToLocalManga.await(it.toDomainManga(sourceId))
|
||||||
.let { localManga ->
|
.let { localManga -> getManga.subscribe(localManga.url, localManga.source) }
|
||||||
getManga.subscribe(localManga.url, localManga.source)
|
|
||||||
}
|
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.filter { localManga ->
|
|
||||||
!sourcePreferences.hideInLibraryItems().get() || !localManga.favorite
|
|
||||||
}
|
|
||||||
.stateIn(ioCoroutineScope)
|
.stateIn(ioCoroutineScope)
|
||||||
}
|
}
|
||||||
|
.filter { !hideInLibraryItems || !it.value.favorite }
|
||||||
}
|
}
|
||||||
.cachedIn(ioCoroutineScope)
|
.cachedIn(ioCoroutineScope)
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ fun SourceFilterDialog(
|
|||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
onFilter()
|
onFilter()
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(stringResource(R.string.action_filter))
|
Text(stringResource(R.string.action_filter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@ -140,7 +139,7 @@ abstract class SearchScreenModel(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val page = withContext(coroutineDispatcher) {
|
val page = withContext(coroutineDispatcher) {
|
||||||
source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle()
|
source.getSearchManga(1, query, source.getFilterList())
|
||||||
}
|
}
|
||||||
|
|
||||||
val titles = page.mangas.map {
|
val titles = page.mangas.map {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.ui.download
|
package eu.kanade.tachiyomi.ui.download
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
@ -42,7 +42,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
@ -243,6 +242,7 @@ object DownloadQueueScreen : Screen() {
|
|||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
|
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
|
||||||
@ -252,13 +252,13 @@ object DownloadQueueScreen : Screen() {
|
|||||||
|
|
||||||
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
|
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
screenModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context))
|
screenModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context))
|
||||||
screenModel.adapter = DownloadAdapter(screenModel.listener)
|
screenModel.adapter = DownloadAdapter(screenModel.listener)
|
||||||
screenModel.controllerBinding.recycler.adapter = screenModel.adapter
|
screenModel.controllerBinding.root.adapter = screenModel.adapter
|
||||||
screenModel.adapter?.isHandleDragEnabled = true
|
screenModel.adapter?.isHandleDragEnabled = true
|
||||||
screenModel.adapter?.fastScroller = screenModel.controllerBinding.fastScroller
|
screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(context)
|
||||||
screenModel.controllerBinding.recycler.layoutManager = LinearLayoutManager(context)
|
|
||||||
|
|
||||||
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true)
|
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true)
|
||||||
|
|
||||||
@ -274,7 +274,7 @@ object DownloadQueueScreen : Screen() {
|
|||||||
screenModel.controllerBinding.root
|
screenModel.controllerBinding.root
|
||||||
},
|
},
|
||||||
update = {
|
update = {
|
||||||
screenModel.controllerBinding.recycler
|
screenModel.controllerBinding.root
|
||||||
.updatePadding(
|
.updatePadding(
|
||||||
left = left,
|
left = left,
|
||||||
top = top,
|
top = top,
|
||||||
@ -282,14 +282,6 @@ object DownloadQueueScreen : Screen() {
|
|||||||
bottom = bottom,
|
bottom = bottom,
|
||||||
)
|
)
|
||||||
|
|
||||||
screenModel.controllerBinding.fastScroller
|
|
||||||
.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
leftMargin = left
|
|
||||||
topMargin = top
|
|
||||||
rightMargin = right
|
|
||||||
bottomMargin = bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
screenModel.adapter?.updateDataSet(downloadList)
|
screenModel.adapter?.updateDataSet(downloadList)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -84,13 +84,17 @@ class DownloadQueueScreenModel(
|
|||||||
}
|
}
|
||||||
reorder(newDownloads)
|
reorder(newDownloads)
|
||||||
}
|
}
|
||||||
R.id.move_to_top_series -> {
|
R.id.move_to_top_series, R.id.move_to_bottom_series -> {
|
||||||
val (selectedSeries, otherSeries) = adapter?.currentItems
|
val (selectedSeries, otherSeries) = adapter?.currentItems
|
||||||
?.filterIsInstance<DownloadItem>()
|
?.filterIsInstance<DownloadItem>()
|
||||||
?.map(DownloadItem::download)
|
?.map(DownloadItem::download)
|
||||||
?.partition { item.download.manga.id == it.manga.id }
|
?.partition { item.download.manga.id == it.manga.id }
|
||||||
?: Pair(emptyList(), emptyList())
|
?: Pair(emptyList(), emptyList())
|
||||||
|
if (menuItem.itemId == R.id.move_to_top_series) {
|
||||||
reorder(selectedSeries + otherSeries)
|
reorder(selectedSeries + otherSeries)
|
||||||
|
} else {
|
||||||
|
reorder(otherSeries + selectedSeries)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
R.id.cancel_download -> {
|
R.id.cancel_download -> {
|
||||||
cancel(listOf(item.download))
|
cancel(listOf(item.download))
|
||||||
@ -258,6 +262,6 @@ class DownloadQueueScreenModel(
|
|||||||
* @return the holder of the download or null if it's not bound.
|
* @return the holder of the download or null if it's not bound.
|
||||||
*/
|
*/
|
||||||
private fun getHolder(download: Download): DownloadHolder? {
|
private fun getHolder(download: Download): DownloadHolder? {
|
||||||
return controllerBinding.recycler.findViewHolderForItemId(download.chapter.id) as? DownloadHolder
|
return controllerBinding.root.findViewHolderForItemId(download.chapter.id) as? DownloadHolder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ object LibraryTab : Tab {
|
|||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
stringResId = R.string.getting_started_guide,
|
stringResId = R.string.getting_started_guide,
|
||||||
icon = Icons.Outlined.HelpOutline,
|
icon = Icons.Outlined.HelpOutline,
|
||||||
onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
|
onClick = { handler.openUri("https://tachiyomi.org/docs/guides/getting-started") },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -271,7 +271,10 @@ private data class TrackStatusSelectorScreen(
|
|||||||
selection = state.selection,
|
selection = state.selection,
|
||||||
onSelectionChange = sm::setSelection,
|
onSelectionChange = sm::setSelection,
|
||||||
selections = remember { sm.getSelections() },
|
selections = remember { sm.getSelections() },
|
||||||
onConfirm = { sm.setStatus(); navigator.pop() },
|
onConfirm = {
|
||||||
|
sm.setStatus()
|
||||||
|
navigator.pop()
|
||||||
|
},
|
||||||
onDismissRequest = navigator::pop,
|
onDismissRequest = navigator::pop,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -322,7 +325,10 @@ private data class TrackChapterSelectorScreen(
|
|||||||
selection = state.selection,
|
selection = state.selection,
|
||||||
onSelectionChange = sm::setSelection,
|
onSelectionChange = sm::setSelection,
|
||||||
range = remember { sm.getRange() },
|
range = remember { sm.getRange() },
|
||||||
onConfirm = { sm.setChapter(); navigator.pop() },
|
onConfirm = {
|
||||||
|
sm.setChapter()
|
||||||
|
navigator.pop()
|
||||||
|
},
|
||||||
onDismissRequest = navigator::pop,
|
onDismissRequest = navigator::pop,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -378,7 +384,10 @@ private data class TrackScoreSelectorScreen(
|
|||||||
selection = state.selection,
|
selection = state.selection,
|
||||||
onSelectionChange = sm::setSelection,
|
onSelectionChange = sm::setSelection,
|
||||||
selections = remember { sm.getSelections() },
|
selections = remember { sm.getSelections() },
|
||||||
onConfirm = { sm.setScore(); navigator.pop() },
|
onConfirm = {
|
||||||
|
sm.setScore()
|
||||||
|
navigator.pop()
|
||||||
|
},
|
||||||
onDismissRequest = navigator::pop,
|
onDismissRequest = navigator::pop,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -495,7 +504,10 @@ private data class TrackDateSelectorScreen(
|
|||||||
},
|
},
|
||||||
initialSelectedDateMillis = sm.initialSelection,
|
initialSelectedDateMillis = sm.initialSelection,
|
||||||
selectableDates = selectableDates,
|
selectableDates = selectableDates,
|
||||||
onConfirm = { sm.setDate(it); navigator.pop() },
|
onConfirm = {
|
||||||
|
sm.setDate(it)
|
||||||
|
navigator.pop()
|
||||||
|
},
|
||||||
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
|
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
|
||||||
onDismissRequest = navigator::pop,
|
onDismissRequest = navigator::pop,
|
||||||
)
|
)
|
||||||
@ -584,7 +596,10 @@ private data class TrackDateRemoverScreen(
|
|||||||
Text(text = stringResource(android.R.string.cancel))
|
Text(text = stringResource(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = { sm.removeDate(); navigator.popUntil { it is TrackInfoDialogHomeScreen } },
|
onClick = {
|
||||||
|
sm.removeDate()
|
||||||
|
navigator.popUntil { it is TrackInfoDialogHomeScreen }
|
||||||
|
},
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
colors = ButtonDefaults.filledTonalButtonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
@ -646,7 +661,10 @@ data class TrackServiceSearchScreen(
|
|||||||
queryResult = state.queryResult,
|
queryResult = state.queryResult,
|
||||||
selected = state.selected,
|
selected = state.selected,
|
||||||
onSelectedChange = sm::updateSelection,
|
onSelectedChange = sm::updateSelection,
|
||||||
onConfirmSelection = { sm.registerTracking(state.selected!!); navigator.pop() },
|
onConfirmSelection = {
|
||||||
|
sm.registerTracking(state.selected!!)
|
||||||
|
navigator.pop()
|
||||||
|
},
|
||||||
onDismissRequest = navigator::pop,
|
onDismissRequest = navigator::pop,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.filter
|
|||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -170,7 +169,7 @@ internal class HttpPageLoader(
|
|||||||
try {
|
try {
|
||||||
if (page.imageUrl.isNullOrEmpty()) {
|
if (page.imageUrl.isNullOrEmpty()) {
|
||||||
page.status = Page.State.LOAD_PAGE
|
page.status = Page.State.LOAD_PAGE
|
||||||
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
|
page.imageUrl = source.getImageUrl(page)
|
||||||
}
|
}
|
||||||
val imageUrl = page.imageUrl!!
|
val imageUrl = page.imageUrl!!
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class UnlockActivity : BaseActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
startAuthentication(
|
startAuthentication(
|
||||||
getString(R.string.unlock_app),
|
getString(R.string.unlock_app_title, getString(R.string.app_name)),
|
||||||
confirmationRequired = false,
|
confirmationRequired = false,
|
||||||
callback = object : AuthenticatorUtil.AuthenticationCallback() {
|
callback = object : AuthenticatorUtil.AuthenticationCallback() {
|
||||||
override fun onAuthenticationError(
|
override fun onAuthenticationError(
|
||||||
|
@ -20,7 +20,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
@ -384,7 +383,7 @@ class UpdatesScreenModel(
|
|||||||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||||
when {
|
when {
|
||||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
||||||
val text = afterDate.toRelativeString(context, dateFormat)
|
val text = dateFormat.format(afterDate)
|
||||||
UpdatesUiModel.Header(text)
|
UpdatesUiModel.Header(text)
|
||||||
}
|
}
|
||||||
// Return null to avoid adding a separator between two items.
|
// Return null to avoid adding a separator between two items.
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
@ -35,6 +36,7 @@ class CrashLogUtil(private val context: Context) {
|
|||||||
Device name: ${Build.DEVICE}
|
Device name: ${Build.DEVICE}
|
||||||
Device model: ${Build.MODEL}
|
Device model: ${Build.MODEL}
|
||||||
Device product name: ${Build.PRODUCT}
|
Device product name: ${Build.PRODUCT}
|
||||||
|
WebView user agent: ${WebViewUtil.getInferredUserAgent(context)}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.util.lang
|
package eu.kanade.tachiyomi.util.lang
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
fun Date.toDateTimestampString(dateFormatter: DateFormat): String {
|
fun Date.toDateTimestampString(dateFormatter: DateFormat): String {
|
||||||
val date = dateFormatter.format(this)
|
val date = dateFormatter.format(this)
|
||||||
@ -45,101 +42,3 @@ fun Long.toDateKey(): Date {
|
|||||||
cal[Calendar.MILLISECOND] = 0
|
cal[Calendar.MILLISECOND] = 0
|
||||||
return cal.time
|
return cal.time
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert epoch long to Calendar instance
|
|
||||||
*
|
|
||||||
* @return Calendar instance at supplied epoch time. Null if epoch was 0.
|
|
||||||
*/
|
|
||||||
fun Long.toCalendar(): Calendar? {
|
|
||||||
if (this == 0L) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val cal = Calendar.getInstance()
|
|
||||||
cal.timeInMillis = this
|
|
||||||
return cal
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert local time millisecond value to Calendar instance in UTC
|
|
||||||
*
|
|
||||||
* @return UTC Calendar instance at supplied time. Null if time is 0.
|
|
||||||
*/
|
|
||||||
fun Long.toUtcCalendar(): Calendar? {
|
|
||||||
if (this == 0L) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val rawCalendar = Calendar.getInstance().apply {
|
|
||||||
timeInMillis = this@toUtcCalendar
|
|
||||||
}
|
|
||||||
return Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
|
|
||||||
clear()
|
|
||||||
set(
|
|
||||||
rawCalendar.get(Calendar.YEAR),
|
|
||||||
rawCalendar.get(Calendar.MONTH),
|
|
||||||
rawCalendar.get(Calendar.DAY_OF_MONTH),
|
|
||||||
rawCalendar.get(Calendar.HOUR_OF_DAY),
|
|
||||||
rawCalendar.get(Calendar.MINUTE),
|
|
||||||
rawCalendar.get(Calendar.SECOND),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert UTC time millisecond to Calendar instance in local time zone
|
|
||||||
*
|
|
||||||
* @return local Calendar instance at supplied UTC time. Null if time is 0.
|
|
||||||
*/
|
|
||||||
fun Long.toLocalCalendar(): Calendar? {
|
|
||||||
if (this == 0L) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val rawCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
|
|
||||||
timeInMillis = this@toLocalCalendar
|
|
||||||
}
|
|
||||||
return Calendar.getInstance().apply {
|
|
||||||
clear()
|
|
||||||
set(
|
|
||||||
rawCalendar.get(Calendar.YEAR),
|
|
||||||
rawCalendar.get(Calendar.MONTH),
|
|
||||||
rawCalendar.get(Calendar.DAY_OF_MONTH),
|
|
||||||
rawCalendar.get(Calendar.HOUR_OF_DAY),
|
|
||||||
rawCalendar.get(Calendar.MINUTE),
|
|
||||||
rawCalendar.get(Calendar.SECOND),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val MILLISECONDS_IN_DAY = 86_400_000L
|
|
||||||
|
|
||||||
fun Date.toRelativeString(
|
|
||||||
context: Context,
|
|
||||||
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
|
|
||||||
): String {
|
|
||||||
val now = Date()
|
|
||||||
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY)
|
|
||||||
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
|
|
||||||
return when {
|
|
||||||
difference < 0 -> dateFormat.format(this)
|
|
||||||
difference < MILLISECONDS_IN_DAY -> context.getString(R.string.relative_time_today)
|
|
||||||
difference < MILLISECONDS_IN_DAY.times(7) -> context.resources.getQuantityString(
|
|
||||||
R.plurals.relative_time,
|
|
||||||
days,
|
|
||||||
days,
|
|
||||||
)
|
|
||||||
else -> dateFormat.format(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Date.timeWithOffset: Long
|
|
||||||
get() {
|
|
||||||
return Calendar.getInstance().run {
|
|
||||||
time = this@timeWithOffset
|
|
||||||
val dstOffset = get(Calendar.DST_OFFSET)
|
|
||||||
this@timeWithOffset.time + timeZone.rawOffset + dstOffset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Long.floorNearest(to: Long): Long {
|
|
||||||
return this.floorDiv(to) * to
|
|
||||||
}
|
|
||||||
|
@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.util.system
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.View
|
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.domain.ui.model.TabletUiMode
|
import eu.kanade.domain.ui.model.TabletUiMode
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -64,18 +62,6 @@ fun Context.isNightMode(): Boolean {
|
|||||||
return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||||
}
|
}
|
||||||
|
|
||||||
val Resources.isLTR
|
|
||||||
get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts to px and takes into account LTR/RTL layout.
|
|
||||||
*/
|
|
||||||
val Float.dpToPxEnd: Float
|
|
||||||
get() = (
|
|
||||||
this * Resources.getSystem().displayMetrics.density *
|
|
||||||
if (Resources.getSystem().isLTR) 1 else -1
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.).
|
* Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.).
|
||||||
*
|
*
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.widget
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
|
||||||
import eu.davidea.fastscroller.FastScroller
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.util.system.dpToPxEnd
|
|
||||||
import eu.kanade.tachiyomi.util.system.isLTR
|
|
||||||
|
|
||||||
class MaterialFastScroll @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
|
||||||
FastScroller(context, attrs) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
setViewsToUse(
|
|
||||||
R.layout.material_fastscroll,
|
|
||||||
R.id.fast_scroller_bubble,
|
|
||||||
R.id.fast_scroller_handle,
|
|
||||||
)
|
|
||||||
autoHideEnabled = true
|
|
||||||
ignoreTouchesOutsideHandle = true
|
|
||||||
|
|
||||||
applyInsetter {
|
|
||||||
type(navigationBars = true) {
|
|
||||||
margin()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overridden to handle RTL
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
if (recyclerView.computeVerticalScrollRange() <= recyclerView.computeVerticalScrollExtent()) {
|
|
||||||
return super.onTouchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
when (event.action) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
// start: handle RTL differently
|
|
||||||
if (
|
|
||||||
if (context.resources.isLTR) {
|
|
||||||
event.x < handle.x - ViewCompat.getPaddingStart(handle)
|
|
||||||
} else {
|
|
||||||
event.x > handle.width + ViewCompat.getPaddingStart(handle)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// end
|
|
||||||
|
|
||||||
if (ignoreTouchesOutsideHandle &&
|
|
||||||
(event.y < handle.y || event.y > handle.y + handle.height)
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
handle.isSelected = true
|
|
||||||
notifyScrollStateChange(true)
|
|
||||||
showBubble()
|
|
||||||
showScrollbar()
|
|
||||||
val y = event.y
|
|
||||||
setBubbleAndHandlePosition(y)
|
|
||||||
setRecyclerViewPosition(y)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
|
||||||
val y = event.y
|
|
||||||
setBubbleAndHandlePosition(y)
|
|
||||||
setRecyclerViewPosition(y)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
||||||
handle.isSelected = false
|
|
||||||
notifyScrollStateChange(false)
|
|
||||||
hideBubble()
|
|
||||||
if (autoHideEnabled) hideScrollbar()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onTouchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setBubbleAndHandlePosition(y: Float) {
|
|
||||||
super.setBubbleAndHandlePosition(y)
|
|
||||||
if (bubbleEnabled) {
|
|
||||||
bubble.y = handle.y - bubble.height / 2f + handle.height / 2f
|
|
||||||
bubble.translationX = (-45f).dpToPxEnd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_selected="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<solid android:color="?attr/colorAccent" />
|
|
||||||
<size android:width="6dp" android:height="54dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<solid android:color="@color/fast_scroller_handle_idle" />
|
|
||||||
<size android:width="6dp" android:height="54dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</selector>
|
|
@ -1,24 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/frame_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
tools:listitem="@layout/download_item" />
|
tools:listitem="@layout/download_item" />
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
|
||||||
android:id="@+id/fast_scroller"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
app:fastScrollerBubbleEnabled="false"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/fast_scroller_bar"
|
|
||||||
android:layout_width="7dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:background="@null" />
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="end">
|
|
||||||
|
|
||||||
<!-- No margin, use padding at the handle -->
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/fast_scroller_bubble"
|
|
||||||
style="@style/FloatingTextView"
|
|
||||||
android:layout_gravity="end|center_vertical"
|
|
||||||
android:layout_toStartOf="@+id/fast_scroller_handle"
|
|
||||||
android:gravity="center"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="A"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<!-- Padding is here to have better grab -->
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/fast_scroller_handle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:paddingStart="6dp"
|
|
||||||
android:paddingEnd="4dp"
|
|
||||||
android:src="@drawable/material_thumb_drawable" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</merge>
|
|
@ -13,6 +13,10 @@
|
|||||||
android:id="@+id/move_to_bottom"
|
android:id="@+id/move_to_bottom"
|
||||||
android:title="@string/action_move_to_bottom" />
|
android:title="@string/action_move_to_bottom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/move_to_bottom_series"
|
||||||
|
android:title="@string/action_move_to_bottom_all_for_series" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/cancel_download"
|
android:id="@+id/cancel_download"
|
||||||
android:title="@string/action_cancel" />
|
android:title="@string/action_cancel" />
|
||||||
|
@ -56,21 +56,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<!--============-->
|
|
||||||
<!--FastScroller-->
|
|
||||||
<!--============-->
|
|
||||||
<style name="FloatingTextView" parent="TextAppearance.AppCompat">
|
|
||||||
<item name="android:layout_height">wrap_content</item>
|
|
||||||
<item name="android:layout_width">wrap_content</item>
|
|
||||||
<item name="android:elevation">5dp</item>
|
|
||||||
<item name="android:paddingStart">12dp</item>
|
|
||||||
<item name="android:paddingEnd">12dp</item>
|
|
||||||
<item name="android:paddingTop">8dp</item>
|
|
||||||
<item name="android:paddingBottom">8dp</item>
|
|
||||||
<item name="android:textColor">?attr/colorOnPrimary</item>
|
|
||||||
<item name="android:textSize">15sp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!--===========-->
|
<!--===========-->
|
||||||
<!--Preferences-->
|
<!--Preferences-->
|
||||||
<!--===========-->
|
<!--===========-->
|
||||||
|
@ -5,7 +5,7 @@ plugins {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(androidxLibs.gradle)
|
implementation(androidxLibs.gradle)
|
||||||
implementation(kotlinLibs.gradle)
|
implementation(kotlinLibs.gradle)
|
||||||
implementation(libs.kotlinter)
|
implementation(libs.ktlint)
|
||||||
implementation(gradleApi())
|
implementation(gradleApi())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
import org.jmailen.gradle.kotlinter.KotlinterExtension
|
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
||||||
import org.jmailen.gradle.kotlinter.KotlinterPlugin
|
import org.jlleitschuh.gradle.ktlint.KtlintPlugin
|
||||||
|
|
||||||
apply<KotlinterPlugin>()
|
apply<KtlintPlugin>()
|
||||||
|
|
||||||
extensions.configure<KotlinterExtension>("kotlinter") {
|
extensions.configure<KtlintExtension>("ktlint") {
|
||||||
experimentalRules = true
|
version.set("0.50.0")
|
||||||
|
android.set(true)
|
||||||
|
enableExperimentalRules.set(true)
|
||||||
|
|
||||||
disabledRules = arrayOf(
|
filter {
|
||||||
"experimental:argument-list-wrapping", // Doesn't play well with Android Studio
|
exclude("**/generated/**")
|
||||||
"filename", // Often broken to give a more general name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
|
||||||
named<DefaultTask>("preBuild").configure {
|
|
||||||
if (!System.getenv("CI").toBoolean())
|
|
||||||
dependsOn("formatKotlin")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ android {
|
|||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
consumerProguardFiles("consumer-rules.pro")
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -39,7 +39,9 @@ class NetworkHelper(
|
|||||||
builder.addNetworkInterceptor(httpLoggingInterceptor)
|
builder.addNetworkInterceptor(httpLoggingInterceptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.addInterceptor(CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider))
|
builder.addInterceptor(
|
||||||
|
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
|
||||||
|
)
|
||||||
|
|
||||||
when (preferences.dohProvider().get()) {
|
when (preferences.dohProvider().get()) {
|
||||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||||
|
@ -17,6 +17,9 @@ class NetworkPreferences(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun defaultUserAgent(): Preference<String> {
|
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/114.0")
|
return preferenceStore.getString(
|
||||||
|
"default_user_agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,15 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Call.asObservableSuccess(): Observable<Response> {
|
||||||
|
return asObservable().doOnNext { response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
response.close()
|
||||||
|
throw HttpException(response.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||||
@ -95,6 +104,9 @@ suspend fun Call.await(): Response {
|
|||||||
return await(callStack)
|
return await(callStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
*/
|
||||||
suspend fun Call.awaitSuccess(): Response {
|
suspend fun Call.awaitSuccess(): Response {
|
||||||
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
||||||
val response = await(callStack)
|
val response = await(callStack)
|
||||||
@ -105,15 +117,6 @@ suspend fun Call.awaitSuccess(): Response {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Call.asObservableSuccess(): Observable<Response> {
|
|
||||||
return asObservable().doOnNext { response ->
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
response.close()
|
|
||||||
throw HttpException(response.code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
|
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
val progressClient = newBuilder()
|
val progressClient = newBuilder()
|
||||||
.cache(null)
|
.cache(null)
|
||||||
|
@ -9,7 +9,10 @@ import okio.Source
|
|||||||
import okio.buffer
|
import okio.buffer
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
class ProgressResponseBody(
|
||||||
|
private val responseBody: ResponseBody,
|
||||||
|
private val progressListener: ProgressListener,
|
||||||
|
) : ResponseBody() {
|
||||||
|
|
||||||
private val bufferedSource: BufferedSource by lazy {
|
private val bufferedSource: BufferedSource by lazy {
|
||||||
source(responseBody.source()).buffer()
|
source(responseBody.source()).buffer()
|
||||||
@ -36,7 +39,11 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
|
|||||||
val bytesRead = super.read(sink, byteCount)
|
val bytesRead = super.read(sink, byteCount)
|
||||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
progressListener.update(
|
||||||
|
totalBytesRead,
|
||||||
|
responseBody.contentLength(),
|
||||||
|
bytesRead == -1L,
|
||||||
|
)
|
||||||
return bytesRead
|
return bytesRead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,11 @@ class CloudflareInterceptor(
|
|||||||
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
|
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
|
override fun intercept(
|
||||||
|
chain: Interceptor.Chain,
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
): Response {
|
||||||
try {
|
try {
|
||||||
response.close()
|
response.close()
|
||||||
cookieManager.remove(request.url, COOKIE_NAMES, 0)
|
cookieManager.remove(request.url, COOKIE_NAMES, 0)
|
||||||
|
@ -33,7 +33,9 @@ fun OkHttpClient.Builder.rateLimitHost(
|
|||||||
permits: Int,
|
permits: Int,
|
||||||
period: Long = 1,
|
period: Long = 1,
|
||||||
unit: TimeUnit = TimeUnit.SECONDS,
|
unit: TimeUnit = TimeUnit.SECONDS,
|
||||||
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())))
|
) = addInterceptor(
|
||||||
|
RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())),
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An OkHttp interceptor that handles given url host's rate limiting.
|
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||||
@ -69,8 +71,5 @@ fun OkHttpClient.Builder.rateLimitHost(
|
|||||||
* @param permits [Int] Number of requests allowed within a period of units.
|
* @param permits [Int] Number of requests allowed within a period of units.
|
||||||
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
|
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
|
||||||
*/
|
*/
|
||||||
fun OkHttpClient.Builder.rateLimitHost(
|
fun OkHttpClient.Builder.rateLimitHost(url: String, permits: Int, period: Duration = 1.seconds) =
|
||||||
url: String,
|
addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
|
||||||
permits: Int,
|
|
||||||
period: Duration = 1.seconds,
|
|
||||||
) = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
|
|
||||||
|
@ -10,7 +10,11 @@ import androidx.annotation.StringRes
|
|||||||
* @param resource the text resource.
|
* @param resource the text resource.
|
||||||
* @param duration the duration of the toast. Defaults to short.
|
* @param duration the duration of the toast. Defaults to short.
|
||||||
*/
|
*/
|
||||||
fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
|
fun Context.toast(
|
||||||
|
@StringRes resource: Int,
|
||||||
|
duration: Int = Toast.LENGTH_SHORT,
|
||||||
|
block: (Toast) -> Unit = {},
|
||||||
|
): Toast {
|
||||||
return toast(getString(resource), duration, block)
|
return toast(getString(resource), duration, block)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,7 +24,11 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT,
|
|||||||
* @param text the text to display.
|
* @param text the text to display.
|
||||||
* @param duration the duration of the toast. Defaults to short.
|
* @param duration the duration of the toast. Defaults to short.
|
||||||
*/
|
*/
|
||||||
fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
|
fun Context.toast(
|
||||||
|
text: String?,
|
||||||
|
duration: Int = Toast.LENGTH_SHORT,
|
||||||
|
block: (Toast) -> Unit = {},
|
||||||
|
): Toast {
|
||||||
return Toast.makeText(applicationContext, text.orEmpty(), duration).also {
|
return Toast.makeText(applicationContext, text.orEmpty(), duration).also {
|
||||||
block(it)
|
block(it)
|
||||||
it.show()
|
it.show()
|
||||||
|
@ -47,10 +47,7 @@ abstract class WebViewClientCompat : WebViewClient() {
|
|||||||
return shouldInterceptRequestCompat(view, request.url.toString())
|
return shouldInterceptRequestCompat(view, request.url.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
final override fun shouldInterceptRequest(
|
final override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
|
||||||
view: WebView,
|
|
||||||
url: String,
|
|
||||||
): WebResourceResponse? {
|
|
||||||
return shouldInterceptRequestCompat(view, url)
|
return shouldInterceptRequestCompat(view, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,24 @@ import kotlin.coroutines.resume
|
|||||||
object WebViewUtil {
|
object WebViewUtil {
|
||||||
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
||||||
|
|
||||||
const val MINIMUM_WEBVIEW_VERSION = 111
|
const val MINIMUM_WEBVIEW_VERSION = 114
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the WebView's user agent string to create something similar to what Chrome on Android
|
||||||
|
* would return.
|
||||||
|
*
|
||||||
|
* Example of WebView user agent string:
|
||||||
|
* Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.0.0 Mobile Safari/537.36
|
||||||
|
*
|
||||||
|
* Example of Chrome on Android:
|
||||||
|
* Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.3
|
||||||
|
*/
|
||||||
|
fun getInferredUserAgent(context: Context): String {
|
||||||
|
return WebView(context)
|
||||||
|
.getDefaultUserAgentString()
|
||||||
|
.replace("; Android .*?\\)".toRegex(), "; Android 10; K)")
|
||||||
|
.replace("Version/.* Chrome/".toRegex(), "Chrome/")
|
||||||
|
}
|
||||||
|
|
||||||
fun supportsWebView(context: Context): Boolean {
|
fun supportsWebView(context: Context): Boolean {
|
||||||
try {
|
try {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package tachiyomi.core
|
package tachiyomi.core
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val URL_HELP = "https://tachiyomi.org/help/"
|
const val URL_HELP = "https://tachiyomi.org/docs/guides/troubleshooting/"
|
||||||
|
|
||||||
const val MANGA_EXTRA = "manga"
|
const val MANGA_EXTRA = "manga"
|
||||||
|
|
||||||
|
@ -68,7 +68,11 @@ sealed class AndroidPreference<T>(
|
|||||||
key: String,
|
key: String,
|
||||||
defaultValue: String,
|
defaultValue: String,
|
||||||
) : AndroidPreference<String>(preferences, keyFlow, key, defaultValue) {
|
) : AndroidPreference<String>(preferences, keyFlow, key, defaultValue) {
|
||||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: String): String {
|
override fun read(
|
||||||
|
preferences: SharedPreferences,
|
||||||
|
key: String,
|
||||||
|
defaultValue: String,
|
||||||
|
): String {
|
||||||
return preferences.getString(key, defaultValue) ?: defaultValue
|
return preferences.getString(key, defaultValue) ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +132,11 @@ sealed class AndroidPreference<T>(
|
|||||||
key: String,
|
key: String,
|
||||||
defaultValue: Boolean,
|
defaultValue: Boolean,
|
||||||
) : AndroidPreference<Boolean>(preferences, keyFlow, key, defaultValue) {
|
) : AndroidPreference<Boolean>(preferences, keyFlow, key, defaultValue) {
|
||||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: Boolean): Boolean {
|
override fun read(
|
||||||
|
preferences: SharedPreferences,
|
||||||
|
key: String,
|
||||||
|
defaultValue: Boolean,
|
||||||
|
): Boolean {
|
||||||
return preferences.getBoolean(key, defaultValue)
|
return preferences.getBoolean(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +151,11 @@ sealed class AndroidPreference<T>(
|
|||||||
key: String,
|
key: String,
|
||||||
defaultValue: Set<String>,
|
defaultValue: Set<String>,
|
||||||
) : AndroidPreference<Set<String>>(preferences, keyFlow, key, defaultValue) {
|
) : AndroidPreference<Set<String>>(preferences, keyFlow, key, defaultValue) {
|
||||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: Set<String>): Set<String> {
|
override fun read(
|
||||||
|
preferences: SharedPreferences,
|
||||||
|
key: String,
|
||||||
|
defaultValue: Set<String>,
|
||||||
|
): Set<String> {
|
||||||
return preferences.getStringSet(key, defaultValue) ?: defaultValue
|
return preferences.getStringSet(key, defaultValue) ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,11 @@ class AndroidPreferenceStore(
|
|||||||
|
|
||||||
private val SharedPreferences.keyFlow
|
private val SharedPreferences.keyFlow
|
||||||
get() = callbackFlow {
|
get() = callbackFlow {
|
||||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> trySend(key) }
|
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? ->
|
||||||
|
trySend(
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
}
|
||||||
registerOnSharedPreferenceChangeListener(listener)
|
registerOnSharedPreferenceChangeListener(listener)
|
||||||
awaitClose {
|
awaitClose {
|
||||||
unregisterOnSharedPreferenceChangeListener(listener)
|
unregisterOnSharedPreferenceChangeListener(listener)
|
||||||
|
@ -23,7 +23,9 @@ interface Preference<T> {
|
|||||||
fun stateIn(scope: CoroutineScope): StateFlow<T>
|
fun stateIn(scope: CoroutineScope): StateFlow<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(block(get()))
|
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(
|
||||||
|
block(get()),
|
||||||
|
)
|
||||||
|
|
||||||
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
|
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
|
||||||
set(get() + item)
|
set(get() + item)
|
||||||
|
@ -52,9 +52,15 @@ fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
|||||||
fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job =
|
fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job =
|
||||||
launchIO { withContext(NonCancellable, block) }
|
launchIO { withContext(NonCancellable, block) }
|
||||||
|
|
||||||
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(
|
||||||
|
Dispatchers.Main,
|
||||||
|
block,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
|
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(
|
||||||
|
Dispatchers.IO,
|
||||||
|
block,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) =
|
suspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) =
|
||||||
withContext(NonCancellable, block)
|
withContext(NonCancellable, block)
|
||||||
|
@ -11,13 +11,14 @@ object DateColumnAdapter : ColumnAdapter<Date, Long> {
|
|||||||
|
|
||||||
private const val LIST_OF_STRINGS_SEPARATOR = ", "
|
private const val LIST_OF_STRINGS_SEPARATOR = ", "
|
||||||
object StringListColumnAdapter : ColumnAdapter<List<String>, String> {
|
object StringListColumnAdapter : ColumnAdapter<List<String>, String> {
|
||||||
override fun decode(databaseValue: String) =
|
override fun decode(databaseValue: String) = if (databaseValue.isEmpty()) {
|
||||||
if (databaseValue.isEmpty()) {
|
|
||||||
emptyList()
|
emptyList()
|
||||||
} else {
|
} else {
|
||||||
databaseValue.split(LIST_OF_STRINGS_SEPARATOR)
|
databaseValue.split(LIST_OF_STRINGS_SEPARATOR)
|
||||||
}
|
}
|
||||||
override fun encode(value: List<String>) = value.joinToString(separator = LIST_OF_STRINGS_SEPARATOR)
|
override fun encode(value: List<String>) = value.joinToString(
|
||||||
|
separator = LIST_OF_STRINGS_SEPARATOR,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object UpdateStrategyColumnAdapter : ColumnAdapter<UpdateStrategy, Long> {
|
object UpdateStrategyColumnAdapter : ColumnAdapter<UpdateStrategy, Long> {
|
||||||
|
@ -2,7 +2,21 @@ package tachiyomi.data.chapter
|
|||||||
|
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
|
||||||
val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Double, Long, Long, Long, Long) -> Chapter =
|
val chapterMapper: (
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
String?,
|
||||||
|
Boolean,
|
||||||
|
Boolean,
|
||||||
|
Long,
|
||||||
|
Double,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
) -> Chapter =
|
||||||
{ id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload, lastModifiedAt ->
|
{ id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload, lastModifiedAt ->
|
||||||
Chapter(
|
Chapter(
|
||||||
id = id,
|
id = id,
|
||||||
|
@ -81,7 +81,12 @@ class ChapterRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
|
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
|
||||||
return handler.awaitList { chaptersQueries.getBookmarkedChaptersByMangaId(mangaId, chapterMapper) }
|
return handler.awaitList {
|
||||||
|
chaptersQueries.getBookmarkedChaptersByMangaId(
|
||||||
|
mangaId,
|
||||||
|
chapterMapper,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapterById(id: Long): Chapter? {
|
override suspend fun getChapterById(id: Long): Chapter? {
|
||||||
@ -89,10 +94,21 @@ class ChapterRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>> {
|
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>> {
|
||||||
return handler.subscribeToList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) }
|
return handler.subscribeToList {
|
||||||
|
chaptersQueries.getChaptersByMangaId(
|
||||||
|
mangaId,
|
||||||
|
chapterMapper,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
|
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
|
||||||
return handler.awaitOneOrNull { chaptersQueries.getChapterByUrlAndMangaId(url, mangaId, chapterMapper) }
|
return handler.awaitOneOrNull {
|
||||||
|
chaptersQueries.getChapterByUrlAndMangaId(
|
||||||
|
url,
|
||||||
|
mangaId,
|
||||||
|
chapterMapper,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,19 @@ val historyMapper: (Long, Long, Date?, Long) -> History = { id, chapterId, readA
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val historyWithRelationsMapper: (Long, Long, Long, String, String?, Long, Boolean, Long, Double, Date?, Long) -> HistoryWithRelations = {
|
val historyWithRelationsMapper: (
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
String,
|
||||||
|
String?,
|
||||||
|
Long,
|
||||||
|
Boolean,
|
||||||
|
Long,
|
||||||
|
Double,
|
||||||
|
Date?,
|
||||||
|
Long,
|
||||||
|
) -> HistoryWithRelations = {
|
||||||
historyId, mangaId, chapterId, title, thumbnailUrl, sourceId, isFavorite, coverLastModified, chapterNumber, readAt, readDuration ->
|
historyId, mangaId, chapterId, title, thumbnailUrl, sourceId, isFavorite, coverLastModified, chapterNumber, readAt, readDuration ->
|
||||||
HistoryWithRelations(
|
HistoryWithRelations(
|
||||||
id = historyId,
|
id = historyId,
|
||||||
|
@ -4,7 +4,30 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
|||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
|
||||||
val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, UpdateStrategy, Long, Long, Long?) -> Manga =
|
val mangaMapper: (
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
String,
|
||||||
|
String?,
|
||||||
|
String?,
|
||||||
|
String?,
|
||||||
|
List<String>?,
|
||||||
|
String,
|
||||||
|
Long,
|
||||||
|
String?,
|
||||||
|
Boolean,
|
||||||
|
Long?,
|
||||||
|
Long?,
|
||||||
|
Boolean,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
UpdateStrategy,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long?,
|
||||||
|
) -> Manga =
|
||||||
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt ->
|
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt ->
|
||||||
Manga(
|
Manga(
|
||||||
id = id,
|
id = id,
|
||||||
@ -32,7 +55,37 @@ val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val libraryManga: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, UpdateStrategy, Long, Long, Long?, Long, Double, Long, Long, Long, Double, Long) -> LibraryManga =
|
val libraryManga: (
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
String,
|
||||||
|
String?,
|
||||||
|
String?,
|
||||||
|
String?,
|
||||||
|
List<String>?,
|
||||||
|
String,
|
||||||
|
Long,
|
||||||
|
String?,
|
||||||
|
Boolean,
|
||||||
|
Long?,
|
||||||
|
Long?,
|
||||||
|
Boolean,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
UpdateStrategy,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long?,
|
||||||
|
Long,
|
||||||
|
Double,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Double,
|
||||||
|
Long,
|
||||||
|
) -> LibraryManga =
|
||||||
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt, totalCount, readCount, latestUpload, chapterFetchedAt, lastRead, bookmarkCount, category ->
|
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt, totalCount, readCount, latestUpload, chapterFetchedAt, lastRead, bookmarkCount, category ->
|
||||||
LibraryManga(
|
LibraryManga(
|
||||||
manga = mangaMapper(
|
manga = mangaMapper(
|
||||||
|
@ -24,11 +24,23 @@ class MangaRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
|
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
|
||||||
return handler.awaitOneOrNull(inTransaction = true) { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
|
return handler.awaitOneOrNull(inTransaction = true) {
|
||||||
|
mangasQueries.getMangaByUrlAndSource(
|
||||||
|
url,
|
||||||
|
sourceId,
|
||||||
|
mangaMapper,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
|
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
|
||||||
return handler.subscribeToOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
|
return handler.subscribeToOneOrNull {
|
||||||
|
mangasQueries.getMangaByUrlAndSource(
|
||||||
|
url,
|
||||||
|
sourceId,
|
||||||
|
mangaMapper,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getFavorites(): List<Manga> {
|
override suspend fun getFavorites(): List<Manga> {
|
||||||
|
@ -32,7 +32,9 @@ data class GitHubAssets(@SerialName("browser_download_url") val downloadLink: St
|
|||||||
* Reference: https://stackoverflow.com/a/30281147
|
* Reference: https://stackoverflow.com/a/30281147
|
||||||
*/
|
*/
|
||||||
val gitHubUsernameMentionRegex =
|
val gitHubUsernameMentionRegex =
|
||||||
"""\B@([a-z0-9](?:-(?=[a-z0-9])|[a-z0-9]){0,38}(?<=[a-z0-9]))""".toRegex(RegexOption.IGNORE_CASE)
|
"""\B@([a-z0-9](?:-(?=[a-z0-9])|[a-z0-9]){0,38}(?<=[a-z0-9]))""".toRegex(
|
||||||
|
RegexOption.IGNORE_CASE,
|
||||||
|
)
|
||||||
|
|
||||||
val releaseMapper: (GithubRelease) -> Release = {
|
val releaseMapper: (GithubRelease) -> Release = {
|
||||||
Release(
|
Release(
|
||||||
|
@ -5,25 +5,26 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.domain.source.repository.SourcePagingSourceType
|
import tachiyomi.domain.source.repository.SourcePagingSourceType
|
||||||
|
|
||||||
class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource(source) {
|
class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource(
|
||||||
|
source,
|
||||||
|
) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||||
return source.fetchSearchManga(currentPage, query, filters).awaitSingle()
|
return source.getSearchManga(currentPage, query, filters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||||
return source.fetchPopularManga(currentPage).awaitSingle()
|
return source.getPopularManga(currentPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||||
return source.fetchLatestUpdates(currentPage).awaitSingle()
|
return source.getLatestUpdates(currentPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,21 @@ package tachiyomi.data.track
|
|||||||
|
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
|
|
||||||
val trackMapper: (Long, Long, Long, Long, Long?, String, Double, Long, Long, Double, String, Long, Long) -> Track =
|
val trackMapper: (
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long?,
|
||||||
|
String,
|
||||||
|
Double,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Double,
|
||||||
|
String,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
) -> Track =
|
||||||
{ id, mangaId, syncId, remoteId, libraryId, title, lastChapterRead, totalChapters, status, score, remoteUrl, startDate, finishDate ->
|
{ id, mangaId, syncId, remoteId, libraryId, title, lastChapterRead, totalChapters, status, score, remoteUrl, startDate, finishDate ->
|
||||||
Track(
|
Track(
|
||||||
id = id,
|
id = id,
|
||||||
|
@ -3,7 +3,22 @@ package tachiyomi.data.updates
|
|||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||||
|
|
||||||
val updateWithRelationMapper: (Long, String, Long, String, String?, Boolean, Boolean, Long, Long, Boolean, String?, Long, Long, Long) -> UpdatesWithRelations = {
|
val updateWithRelationMapper: (
|
||||||
|
Long,
|
||||||
|
String,
|
||||||
|
Long,
|
||||||
|
String,
|
||||||
|
String?,
|
||||||
|
Boolean,
|
||||||
|
Boolean,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Boolean,
|
||||||
|
String?,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
Long,
|
||||||
|
) -> UpdatesWithRelations = {
|
||||||
mangaId, mangaTitle, chapterId, chapterName, scanlator, read, bookmark, lastPageRead, sourceId, favorite, thumbnailUrl, coverLastModified, _, dateFetch ->
|
mangaId, mangaTitle, chapterId, chapterName, scanlator, read, bookmark, lastPageRead, sourceId, favorite, thumbnailUrl, coverLastModified, _, dateFetch ->
|
||||||
UpdatesWithRelations(
|
UpdatesWithRelations(
|
||||||
mangaId = mangaId,
|
mangaId = mangaId,
|
||||||
|
@ -9,7 +9,11 @@ class UpdatesRepositoryImpl(
|
|||||||
private val databaseHandler: DatabaseHandler,
|
private val databaseHandler: DatabaseHandler,
|
||||||
) : UpdatesRepository {
|
) : UpdatesRepository {
|
||||||
|
|
||||||
override suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List<UpdatesWithRelations> {
|
override suspend fun awaitWithRead(
|
||||||
|
read: Boolean,
|
||||||
|
after: Long,
|
||||||
|
limit: Long,
|
||||||
|
): List<UpdatesWithRelations> {
|
||||||
return databaseHandler.awaitList {
|
return databaseHandler.awaitList {
|
||||||
updatesViewQueries.getUpdatesByReadStatus(
|
updatesViewQueries.getUpdatesByReadStatus(
|
||||||
read = read,
|
read = read,
|
||||||
@ -26,7 +30,11 @@ class UpdatesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow<List<UpdatesWithRelations>> {
|
override fun subscribeWithRead(
|
||||||
|
read: Boolean,
|
||||||
|
after: Long,
|
||||||
|
limit: Long,
|
||||||
|
): Flow<List<UpdatesWithRelations>> {
|
||||||
return databaseHandler.subscribeToList {
|
return databaseHandler.subscribeToList {
|
||||||
updatesViewQueries.getUpdatesByReadStatus(
|
updatesViewQueries.getUpdatesByReadStatus(
|
||||||
read = read,
|
read = read,
|
||||||
|
@ -16,11 +16,9 @@ class ReorderCategory(
|
|||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
suspend fun moveUp(category: Category): Result =
|
suspend fun moveUp(category: Category): Result = await(category, MoveTo.UP)
|
||||||
await(category, MoveTo.UP)
|
|
||||||
|
|
||||||
suspend fun moveDown(category: Category): Result =
|
suspend fun moveDown(category: Category): Result = await(category, MoveTo.DOWN)
|
||||||
await(category, MoveTo.DOWN)
|
|
||||||
|
|
||||||
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
|
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
|
@ -28,7 +28,11 @@ class SetSortModeForCategory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun await(category: Category?, type: LibrarySort.Type, direction: LibrarySort.Direction) {
|
suspend fun await(
|
||||||
|
category: Category?,
|
||||||
|
type: LibrarySort.Type,
|
||||||
|
direction: LibrarySort.Direction,
|
||||||
|
) {
|
||||||
await(category?.id, type, direction)
|
await(category?.id, type, direction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,11 @@ object ChapterRecognition {
|
|||||||
*/
|
*/
|
||||||
private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""")
|
private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""")
|
||||||
|
|
||||||
fun parseChapterNumber(mangaTitle: String, chapterName: String, chapterNumber: Double? = null): Double {
|
fun parseChapterNumber(
|
||||||
|
mangaTitle: String,
|
||||||
|
chapterName: String,
|
||||||
|
chapterNumber: Double? = null,
|
||||||
|
): Double {
|
||||||
// If chapter number is known return.
|
// If chapter number is known return.
|
||||||
if (chapterNumber != null && (chapterNumber == -2.0 || chapterNumber > -1.0)) {
|
if (chapterNumber != null && (chapterNumber == -2.0 || chapterNumber > -1.0)) {
|
||||||
return chapterNumber
|
return chapterNumber
|
||||||
|
@ -3,7 +3,13 @@ package tachiyomi.domain.chapter.service
|
|||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
|
||||||
fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending()): (Chapter, Chapter) -> Int {
|
fun getChapterSort(
|
||||||
|
manga: Manga,
|
||||||
|
sortDescending: Boolean = manga.sortDescending(),
|
||||||
|
): (
|
||||||
|
Chapter,
|
||||||
|
Chapter,
|
||||||
|
) -> Int {
|
||||||
return when (manga.sorting) {
|
return when (manga.sorting) {
|
||||||
Manga.CHAPTER_SORTING_SOURCE -> when (sortDescending) {
|
Manga.CHAPTER_SORTING_SOURCE -> when (sortDescending) {
|
||||||
true -> { c1, c2 -> c1.sourceOrder.compareTo(c2.sourceOrder) }
|
true -> { c1, c2 -> c1.sourceOrder.compareTo(c2.sourceOrder) }
|
||||||
|
@ -8,9 +8,15 @@ class DownloadPreferences(
|
|||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun downloadsDirectory() = preferenceStore.getString("download_directory", folderProvider.path())
|
fun downloadsDirectory() = preferenceStore.getString(
|
||||||
|
"download_directory",
|
||||||
|
folderProvider.path(),
|
||||||
|
)
|
||||||
|
|
||||||
fun downloadOnlyOverWifi() = preferenceStore.getBoolean("pref_download_only_over_wifi_key", true)
|
fun downloadOnlyOverWifi() = preferenceStore.getBoolean(
|
||||||
|
"pref_download_only_over_wifi_key",
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
fun saveChaptersAsCBZ() = preferenceStore.getBoolean("save_chapter_as_cbz", true)
|
fun saveChaptersAsCBZ() = preferenceStore.getBoolean("save_chapter_as_cbz", true)
|
||||||
|
|
||||||
@ -20,15 +26,27 @@ class DownloadPreferences(
|
|||||||
|
|
||||||
fun removeAfterReadSlots() = preferenceStore.getInt("remove_after_read_slots", -1)
|
fun removeAfterReadSlots() = preferenceStore.getInt("remove_after_read_slots", -1)
|
||||||
|
|
||||||
fun removeAfterMarkedAsRead() = preferenceStore.getBoolean("pref_remove_after_marked_as_read_key", false)
|
fun removeAfterMarkedAsRead() = preferenceStore.getBoolean(
|
||||||
|
"pref_remove_after_marked_as_read_key",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
fun removeBookmarkedChapters() = preferenceStore.getBoolean("pref_remove_bookmarked", false)
|
fun removeBookmarkedChapters() = preferenceStore.getBoolean("pref_remove_bookmarked", false)
|
||||||
|
|
||||||
fun removeExcludeCategories() = preferenceStore.getStringSet("remove_exclude_categories", emptySet())
|
fun removeExcludeCategories() = preferenceStore.getStringSet(
|
||||||
|
"remove_exclude_categories",
|
||||||
|
emptySet(),
|
||||||
|
)
|
||||||
|
|
||||||
fun downloadNewChapters() = preferenceStore.getBoolean("download_new", false)
|
fun downloadNewChapters() = preferenceStore.getBoolean("download_new", false)
|
||||||
|
|
||||||
fun downloadNewChapterCategories() = preferenceStore.getStringSet("download_new_categories", emptySet())
|
fun downloadNewChapterCategories() = preferenceStore.getStringSet(
|
||||||
|
"download_new_categories",
|
||||||
|
emptySet(),
|
||||||
|
)
|
||||||
|
|
||||||
fun downloadNewChapterCategoriesExclude() = preferenceStore.getStringSet("download_new_categories_exclude", emptySet())
|
fun downloadNewChapterCategoriesExclude() = preferenceStore.getStringSet(
|
||||||
|
"download_new_categories_exclude",
|
||||||
|
emptySet(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,11 @@ class GetNextChapters(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun await(mangaId: Long, fromChapterId: Long, onlyUnread: Boolean = true): List<Chapter> {
|
suspend fun await(
|
||||||
|
mangaId: Long,
|
||||||
|
fromChapterId: Long,
|
||||||
|
onlyUnread: Boolean = true,
|
||||||
|
): List<Chapter> {
|
||||||
val chapters = await(mangaId, onlyUnread)
|
val chapters = await(mangaId, onlyUnread)
|
||||||
val currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId }
|
val currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId }
|
||||||
val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size)
|
val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size)
|
||||||
|
@ -65,7 +65,18 @@ data class LibrarySort(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val types by lazy { setOf(Type.Alphabetical, Type.LastRead, Type.LastUpdate, Type.UnreadCount, Type.TotalChapters, Type.LatestChapter, Type.ChapterFetchDate, Type.DateAdded) }
|
val types by lazy {
|
||||||
|
setOf(
|
||||||
|
Type.Alphabetical,
|
||||||
|
Type.LastRead,
|
||||||
|
Type.LastUpdate,
|
||||||
|
Type.UnreadCount,
|
||||||
|
Type.TotalChapters,
|
||||||
|
Type.LatestChapter,
|
||||||
|
Type.ChapterFetchDate,
|
||||||
|
Type.DateAdded,
|
||||||
|
)
|
||||||
|
}
|
||||||
val directions by lazy { setOf(Direction.Ascending, Direction.Descending) }
|
val directions by lazy { setOf(Direction.Ascending, Direction.Descending) }
|
||||||
val default = LibrarySort(Type.Alphabetical, Direction.Ascending)
|
val default = LibrarySort(Type.Alphabetical, Direction.Ascending)
|
||||||
|
|
||||||
|
@ -11,9 +11,19 @@ class LibraryPreferences(
|
|||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun displayMode() = preferenceStore.getObject("pref_display_mode_library", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
|
fun displayMode() = preferenceStore.getObject(
|
||||||
|
"pref_display_mode_library",
|
||||||
|
LibraryDisplayMode.default,
|
||||||
|
LibraryDisplayMode.Serializer::serialize,
|
||||||
|
LibraryDisplayMode.Serializer::deserialize,
|
||||||
|
)
|
||||||
|
|
||||||
fun sortingMode() = preferenceStore.getObject("library_sorting_mode", LibrarySort.default, LibrarySort.Serializer::serialize, LibrarySort.Serializer::deserialize)
|
fun sortingMode() = preferenceStore.getObject(
|
||||||
|
"library_sorting_mode",
|
||||||
|
LibrarySort.default,
|
||||||
|
LibrarySort.Serializer::serialize,
|
||||||
|
LibrarySort.Serializer::deserialize,
|
||||||
|
)
|
||||||
|
|
||||||
fun portraitColumns() = preferenceStore.getInt("pref_library_columns_portrait_key", 0)
|
fun portraitColumns() = preferenceStore.getInt("pref_library_columns_portrait_key", 0)
|
||||||
|
|
||||||
@ -42,31 +52,64 @@ class LibraryPreferences(
|
|||||||
|
|
||||||
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
||||||
|
|
||||||
fun showContinueReadingButton() = preferenceStore.getBoolean("display_continue_reading_button", false)
|
fun showContinueReadingButton() = preferenceStore.getBoolean(
|
||||||
|
"display_continue_reading_button",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
// region Filter
|
// region Filter
|
||||||
|
|
||||||
fun filterDownloaded() = preferenceStore.getEnum("pref_filter_library_downloaded_v2", TriState.DISABLED)
|
fun filterDownloaded() = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_downloaded_v2",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterUnread() = preferenceStore.getEnum("pref_filter_library_unread_v2", TriState.DISABLED)
|
fun filterUnread() = preferenceStore.getEnum("pref_filter_library_unread_v2", TriState.DISABLED)
|
||||||
|
|
||||||
fun filterStarted() = preferenceStore.getEnum("pref_filter_library_started_v2", TriState.DISABLED)
|
fun filterStarted() = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_started_v2",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterBookmarked() = preferenceStore.getEnum("pref_filter_library_bookmarked_v2", TriState.DISABLED)
|
fun filterBookmarked() = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_bookmarked_v2",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterCompleted() = preferenceStore.getEnum("pref_filter_library_completed_v2", TriState.DISABLED)
|
fun filterCompleted() = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_completed_v2",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterIntervalCustom() = preferenceStore.getEnum("pref_filter_library_interval_custom", TriState.DISABLED)
|
fun filterIntervalCustom() = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_interval_custom",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterIntervalLong() = preferenceStore.getEnum("pref_filter_library_interval_long", TriState.DISABLED)
|
fun filterIntervalLong() = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_interval_long",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterIntervalLate() = preferenceStore.getEnum("pref_filter_library_interval_late", TriState.DISABLED)
|
fun filterIntervalLate() = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_interval_late",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterIntervalDropped() = preferenceStore.getEnum("pref_filter_library_interval_dropped", TriState.DISABLED)
|
fun filterIntervalDropped() = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_interval_dropped",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterIntervalPassed() = preferenceStore.getEnum("pref_filter_library_interval_passed", TriState.DISABLED)
|
fun filterIntervalPassed() = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_interval_passed",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterTracking(id: Int) = preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriState.DISABLED)
|
fun filterTracking(id: Int) = preferenceStore.getEnum(
|
||||||
|
"pref_filter_library_tracked_${id}_v2",
|
||||||
|
TriState.DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -97,24 +140,45 @@ class LibraryPreferences(
|
|||||||
|
|
||||||
fun updateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet())
|
fun updateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet())
|
||||||
|
|
||||||
fun updateCategoriesExclude() = preferenceStore.getStringSet("library_update_categories_exclude", emptySet())
|
fun updateCategoriesExclude() = preferenceStore.getStringSet(
|
||||||
|
"library_update_categories_exclude",
|
||||||
|
emptySet(),
|
||||||
|
)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Chapter
|
// region Chapter
|
||||||
|
|
||||||
fun filterChapterByRead() = preferenceStore.getLong("default_chapter_filter_by_read", Manga.SHOW_ALL)
|
fun filterChapterByRead() = preferenceStore.getLong(
|
||||||
|
"default_chapter_filter_by_read",
|
||||||
|
Manga.SHOW_ALL,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterChapterByDownloaded() = preferenceStore.getLong("default_chapter_filter_by_downloaded", Manga.SHOW_ALL)
|
fun filterChapterByDownloaded() = preferenceStore.getLong(
|
||||||
|
"default_chapter_filter_by_downloaded",
|
||||||
|
Manga.SHOW_ALL,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterChapterByBookmarked() = preferenceStore.getLong("default_chapter_filter_by_bookmarked", Manga.SHOW_ALL)
|
fun filterChapterByBookmarked() = preferenceStore.getLong(
|
||||||
|
"default_chapter_filter_by_bookmarked",
|
||||||
|
Manga.SHOW_ALL,
|
||||||
|
)
|
||||||
|
|
||||||
// and upload date
|
// and upload date
|
||||||
fun sortChapterBySourceOrNumber() = preferenceStore.getLong("default_chapter_sort_by_source_or_number", Manga.CHAPTER_SORTING_SOURCE)
|
fun sortChapterBySourceOrNumber() = preferenceStore.getLong(
|
||||||
|
"default_chapter_sort_by_source_or_number",
|
||||||
|
Manga.CHAPTER_SORTING_SOURCE,
|
||||||
|
)
|
||||||
|
|
||||||
fun displayChapterByNameOrNumber() = preferenceStore.getLong("default_chapter_display_by_name_or_number", Manga.CHAPTER_DISPLAY_NAME)
|
fun displayChapterByNameOrNumber() = preferenceStore.getLong(
|
||||||
|
"default_chapter_display_by_name_or_number",
|
||||||
|
Manga.CHAPTER_DISPLAY_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
fun sortChapterByAscendingOrDescending() = preferenceStore.getLong("default_chapter_sort_by_ascending_or_descending", Manga.CHAPTER_SORT_DESC)
|
fun sortChapterByAscendingOrDescending() = preferenceStore.getLong(
|
||||||
|
"default_chapter_sort_by_ascending_or_descending",
|
||||||
|
Manga.CHAPTER_SORT_DESC,
|
||||||
|
)
|
||||||
|
|
||||||
fun setChapterSettingsDefault(manga: Manga) {
|
fun setChapterSettingsDefault(manga: Manga) {
|
||||||
filterChapterByRead().set(manga.unreadFilterRaw)
|
filterChapterByRead().set(manga.unreadFilterRaw)
|
||||||
@ -122,7 +186,9 @@ class LibraryPreferences(
|
|||||||
filterChapterByBookmarked().set(manga.bookmarkedFilterRaw)
|
filterChapterByBookmarked().set(manga.bookmarkedFilterRaw)
|
||||||
sortChapterBySourceOrNumber().set(manga.sorting)
|
sortChapterBySourceOrNumber().set(manga.sorting)
|
||||||
displayChapterByNameOrNumber().set(manga.displayMode)
|
displayChapterByNameOrNumber().set(manga.displayMode)
|
||||||
sortChapterByAscendingOrDescending().set(if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
sortChapterByAscendingOrDescending().set(
|
||||||
|
if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun autoClearChapterCache() = preferenceStore.getBoolean("auto_clear_chapter_cache", false)
|
fun autoClearChapterCache() = preferenceStore.getBoolean("auto_clear_chapter_cache", false)
|
||||||
@ -131,9 +197,15 @@ class LibraryPreferences(
|
|||||||
|
|
||||||
// region Swipe Actions
|
// region Swipe Actions
|
||||||
|
|
||||||
fun swipeToStartAction() = preferenceStore.getEnum("pref_chapter_swipe_end_action", ChapterSwipeAction.ToggleBookmark)
|
fun swipeToStartAction() = preferenceStore.getEnum(
|
||||||
|
"pref_chapter_swipe_end_action",
|
||||||
|
ChapterSwipeAction.ToggleBookmark,
|
||||||
|
)
|
||||||
|
|
||||||
fun swipeToEndAction() = preferenceStore.getEnum("pref_chapter_swipe_start_action", ChapterSwipeAction.ToggleRead)
|
fun swipeToEndAction() = preferenceStore.getEnum(
|
||||||
|
"pref_chapter_swipe_start_action",
|
||||||
|
ChapterSwipeAction.ToggleRead,
|
||||||
|
)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
@ -5,14 +5,12 @@ import tachiyomi.domain.chapter.model.Chapter
|
|||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val MAX_FETCH_INTERVAL = 28
|
class FetchInterval(
|
||||||
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
|
|
||||||
|
|
||||||
class SetFetchInterval(
|
|
||||||
private val getChapterByMangaId: GetChapterByMangaId,
|
private val getChapterByMangaId: GetChapterByMangaId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -27,7 +25,10 @@ class SetFetchInterval(
|
|||||||
window
|
window
|
||||||
}
|
}
|
||||||
val chapters = getChapterByMangaId.await(manga.id)
|
val chapters = getChapterByMangaId.await(manga.id)
|
||||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, dateTime)
|
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
||||||
|
chapters,
|
||||||
|
dateTime.zone,
|
||||||
|
)
|
||||||
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
||||||
|
|
||||||
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
||||||
@ -39,31 +40,34 @@ class SetFetchInterval(
|
|||||||
|
|
||||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||||
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||||
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
val lowerBound = today.minusDays(GRACE_PERIOD)
|
||||||
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
val upperBound = today.plusDays(GRACE_PERIOD)
|
||||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
internal fun calculateInterval(chapters: List<Chapter>, zone: ZoneId): Int {
|
||||||
val sortedChapters = chapters
|
val uploadDates = chapters.asSequence()
|
||||||
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
|
|
||||||
.take(50)
|
|
||||||
|
|
||||||
val uploadDates = sortedChapters
|
|
||||||
.filter { it.dateUpload > 0L }
|
.filter { it.dateUpload > 0L }
|
||||||
|
.sortedByDescending { it.dateUpload }
|
||||||
.map {
|
.map {
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
.atStartOfDay()
|
.atStartOfDay()
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
val fetchDates = sortedChapters
|
.take(10)
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val fetchDates = chapters.asSequence()
|
||||||
|
.sortedByDescending { it.dateFetch }
|
||||||
.map {
|
.map {
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
.atStartOfDay()
|
.atStartOfDay()
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
|
.take(10)
|
||||||
|
.toList()
|
||||||
|
|
||||||
val interval = when {
|
val interval = when {
|
||||||
// Enough upload date from source
|
// Enough upload date from source
|
||||||
@ -82,7 +86,7 @@ class SetFetchInterval(
|
|||||||
else -> 7
|
else -> 7
|
||||||
}
|
}
|
||||||
|
|
||||||
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
return interval.coerceIn(1, MAX_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateNextUpdate(
|
private fun calculateNextUpdate(
|
||||||
@ -95,7 +99,10 @@ class SetFetchInterval(
|
|||||||
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
|
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
|
||||||
manga.fetchInterval == 0
|
manga.fetchInterval == 0
|
||||||
) {
|
) {
|
||||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone)
|
val latestDate = ZonedDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(manga.lastUpdate),
|
||||||
|
dateTime.zone,
|
||||||
|
)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
.atStartOfDay()
|
.atStartOfDay()
|
||||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
||||||
@ -110,7 +117,7 @@ class SetFetchInterval(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
||||||
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
|
if (delta >= MAX_INTERVAL) return MAX_INTERVAL
|
||||||
|
|
||||||
// double delta again if missed more than 9 check in new delta
|
// double delta again if missed more than 9 check in new delta
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
@ -120,4 +127,10 @@ class SetFetchInterval(
|
|||||||
delta
|
delta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_INTERVAL = 28
|
||||||
|
|
||||||
|
private const val GRACE_PERIOD = 1L
|
||||||
|
}
|
||||||
}
|
}
|
@ -20,7 +20,10 @@ class GetApplicationRelease(
|
|||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
|
|
||||||
// Limit checks to once every 3 days at most
|
// Limit checks to once every 3 days at most
|
||||||
if (arguments.forceCheck.not() && now.isBefore(Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS))) {
|
if (arguments.forceCheck.not() && now.isBefore(
|
||||||
|
Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS),
|
||||||
|
)
|
||||||
|
) {
|
||||||
return Result.NoNewUpdate
|
return Result.NoNewUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +32,12 @@ class GetApplicationRelease(
|
|||||||
lastChecked.set(now.toEpochMilli())
|
lastChecked.set(now.toEpochMilli())
|
||||||
|
|
||||||
// Check if latest version is different from current version
|
// Check if latest version is different from current version
|
||||||
val isNewVersion = isNewVersion(arguments.isPreview, arguments.commitCount, arguments.versionName, release.version)
|
val isNewVersion = isNewVersion(
|
||||||
|
arguments.isPreview,
|
||||||
|
arguments.commitCount,
|
||||||
|
arguments.versionName,
|
||||||
|
release.version,
|
||||||
|
)
|
||||||
return when {
|
return when {
|
||||||
isNewVersion && arguments.isThirdParty -> Result.ThirdPartyInstallation
|
isNewVersion && arguments.isThirdParty -> Result.ThirdPartyInstallation
|
||||||
isNewVersion -> Result.NewUpdate(release)
|
isNewVersion -> Result.NewUpdate(release)
|
||||||
@ -37,7 +45,12 @@ class GetApplicationRelease(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isNewVersion(isPreview: Boolean, commitCount: Int, versionName: String, versionTag: String): Boolean {
|
private fun isNewVersion(
|
||||||
|
isPreview: Boolean,
|
||||||
|
commitCount: Int,
|
||||||
|
versionName: String,
|
||||||
|
versionTag: String,
|
||||||
|
): Boolean {
|
||||||
// Removes prefixes like "r" or "v"
|
// Removes prefixes like "r" or "v"
|
||||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||||
return if (isPreview) {
|
return if (isPreview) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user