chore: merge upstream.

This commit is contained in:
KaiserBh 2023-09-24 20:46:41 +10:00
commit 2eef0dd939
217 changed files with 2175 additions and 1627 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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",
) )
} }
} }

View File

@ -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" />

View File

@ -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()) }

View File

@ -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
} }

View File

@ -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))

View File

@ -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)
}, },
) )
} }

View File

@ -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))
} }
}, },

View File

@ -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 }

View File

@ -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,

View File

@ -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))
} }
}, },

View File

@ -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")
}, },
) )
} }

View File

@ -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

View File

@ -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)

View File

@ -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),

View File

@ -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/") },
) )
} }

View File

@ -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)
}, },

View File

@ -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),
) )
} }
} }

View File

@ -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),
) )
} }
} }

View File

@ -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)

View File

@ -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))
} }
}, },

View File

@ -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")
}, },
) )
} }

View File

@ -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)
} }

View File

@ -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

View File

@ -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"
} }
} }
} }

View File

@ -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
} }
} }

View File

@ -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
} }

View File

@ -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

View File

@ -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"

View File

@ -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()
} }
} }

View File

@ -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()) {

View File

@ -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)!!

View File

@ -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)
} }

View File

@ -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.")

View File

@ -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")
}, },
), ),
), ),

View File

@ -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)
} }

View File

@ -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))
} }
} }

View File

@ -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 {

View File

@ -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)
}, },
) )

View File

@ -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
} }
} }

View File

@ -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") },
), ),
), ),
) )

View File

@ -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,
) )
} }

View File

@ -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!!

View File

@ -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(

View File

@ -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.

View File

@ -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()
} }
} }

View File

@ -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
}

View File

@ -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.).
* *

View File

@ -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
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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-->
<!--===========--> <!--===========-->

View File

@ -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())
} }

View File

@ -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")
} }
} }

View File

@ -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 {

View File

@ -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()

View File

@ -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",
)
} }
} }

View File

@ -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)

View File

@ -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
} }
} }

View File

@ -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)

View File

@ -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))

View File

@ -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()

View File

@ -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)
} }

View File

@ -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 {

View File

@ -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"

View File

@ -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
} }

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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> {

View File

@ -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,

View File

@ -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,
)
}
} }
} }

View File

@ -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,

View File

@ -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(

View File

@ -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> {

View File

@ -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(

View File

@ -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)
} }
} }

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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)
} }
} }

View File

@ -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

View File

@ -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) }

View File

@ -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(),
)
} }

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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
}
} }

View File

@ -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