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

@ -3,5 +3,5 @@ indent_size=4
insert_final_newline=true
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_name_count_to_use_star_import=2147483647
ij_kotlin_name_count_to_use_star_import_for_members=2147483647

View File

@ -5,7 +5,7 @@ I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v0.14.6)
- 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
- 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

View File

@ -4,8 +4,8 @@ contact_links:
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
- name: 📦 Tachiyomi extensions
url: https://tachiyomi.org/extensions
url: https://tachiyomi.org/extensions/
about: List of all available extensions with download links
- name: 🖥️ Tachiyomi website
url: https://tachiyomi.org/help/
url: https://tachiyomi.org/
about: Guides, troubleshooting, and answers to common questions

View File

@ -96,7 +96,7 @@ body:
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).
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
- label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true

View File

@ -19,7 +19,7 @@ jobs:
steps:
- name: Clone repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
@ -36,4 +36,4 @@ jobs:
- name: Build app and run unit tests
uses: gradle/gradle-command-action@v2
with:
arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest

View File

@ -17,7 +17,7 @@ jobs:
steps:
- name: Clone repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
@ -31,7 +31,7 @@ jobs:
- name: Build app and run unit tests
uses: gradle/gradle-command-action@v2
with:
arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
# Sign APK and create release for tags
@ -104,3 +104,13 @@ jobs:
prerelease: false
env:
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.*",
"ignoreCase": true,
"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

View File

@ -30,7 +30,7 @@ Before you start, please note that the ability to use following technologies is
# 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

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>
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)
</details>

View File

@ -1,5 +1,4 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins {
id("com.android.application")
@ -104,15 +103,17 @@ android {
}
packaging {
resources.excludes.addAll(listOf(
"META-INF/DEPENDENCIES",
"LICENSE.txt",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/README.md",
"META-INF/NOTICE",
"META-INF/*.kotlin_module",
))
resources.excludes.addAll(
listOf(
"META-INF/DEPENDENCIES",
"LICENSE.txt",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/README.md",
"META-INF/NOTICE",
"META-INF/*.kotlin_module",
),
)
}
dependenciesInfo {
@ -267,7 +268,9 @@ androidComponents {
beforeVariants { variantBuilder ->
// Disables standardBenchmark
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")) {
@ -278,10 +281,6 @@ androidComponents {
}
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)
withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
@ -306,12 +305,12 @@ tasks {
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
project.buildDir.absolutePath + "/compose_metrics",
)
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"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: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
android:name=".data.download.DownloadService"
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.UpsertHistory
import tachiyomi.domain.history.repository.HistoryRepository
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetFavorites
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.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.manga.interactor.SetFetchInterval
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease
@ -102,7 +102,7 @@ class DomainModule : InjektModule {
addFactory { GetNextChapters(get(), get(), get()) }
addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
addFactory { SetFetchInterval(get()) }
addFactory { FetchInterval(get()) }
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
addFactory { SetMangaViewerFlags(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.tachiyomi.data.cache.CoverCache
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.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
@ -15,7 +15,7 @@ import java.util.Date
class UpdateManga(
private val mangaRepository: MangaRepository,
private val setFetchInterval: SetFetchInterval,
private val fetchInterval: FetchInterval,
) {
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
@ -79,9 +79,9 @@ class UpdateManga(
suspend fun awaitUpdateFetchInterval(
manga: Manga,
dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = setFetchInterval.getWindow(dateTime),
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
): Boolean {
return setFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
return fetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
?.let { mangaRepository.update(it) }
?: false
}

View File

@ -162,7 +162,7 @@ fun CategoryDeleteDialog(
TextButton(onClick = {
onDelete()
onDismissRequest()
},) {
}) {
Text(text = stringResource(R.string.action_ok))
}
},
@ -217,7 +217,7 @@ fun ChangeCategoryDialog(
tachiyomi.presentation.core.components.material.TextButton(onClick = {
onDismissRequest()
onEditCategories()
},) {
}) {
Text(text = stringResource(R.string.action_edit))
}
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.remember
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 java.text.DateFormat
import java.util.Date
@ -15,11 +13,10 @@ fun RelativeDateHeader(
date: Date,
dateFormat: DateFormat,
) {
val context = LocalContext.current
ListGroupHeader(
modifier = modifier,
text = remember {
date.toRelativeString(context, dateFormat)
dateFormat.format(date)
},
)
}

View File

@ -61,7 +61,7 @@ fun HistoryDeleteDialog(
TextButton(onClick = {
onDelete(removeEverything)
onDismissRequest()
},) {
}) {
Text(text = stringResource(R.string.action_remove))
}
},
@ -90,7 +90,7 @@ fun HistoryDeleteAllDialog(
TextButton(onClick = {
onDelete()
onDismissRequest()
},) {
}) {
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.ui.manga.ChapterItem
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.missingChaptersCount
@ -740,7 +739,7 @@ private fun LazyListScope.sharedChapterItems(
date = chapterItem.chapter.dateUpload
.takeIf { it > 0L }
?.let {
Date(it).toRelativeString(context, dateFormat)
dateFormat.format(Date(it))
},
readProgress = chapterItem.chapter.lastPageRead
.takeIf { !chapterItem.chapter.read && it > 0L }

View File

@ -143,7 +143,7 @@ fun MangaBottomActionMenu(
if (onMarkPreviousAsReadClicked != null) {
Button(
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],
onLongClick = { onLongClickItem(4) },
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.dp
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
@Composable
@ -67,7 +67,7 @@ fun SetIntervalDialog(
contentAlignment = Alignment.Center,
) {
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) {
stringResource(R.string.label_default)
} else {
@ -91,7 +91,7 @@ fun SetIntervalDialog(
TextButton(onClick = {
onValueChanged(selectedInterval)
onDismissRequest()
},) {
}) {
Text(text = stringResource(R.string.action_ok))
}
},

View File

@ -62,7 +62,7 @@ fun MoreScreen(
WarningBanner(
textRes = R.string.fdroid_warning,
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")
},
)
}
@ -108,11 +108,11 @@ fun MoreScreen(
stringResource(R.string.paused)
} else {
"${stringResource(R.string.paused)}${
pluralStringResource(
id = R.plurals.download_queue_summary,
count = pending,
pending,
)
pluralStringResource(
id = R.plurals.download_queue_summary,
count = pending,
pending,
)
}"
}
}

View File

@ -4,7 +4,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.more.settings.Preference.PreferenceItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackService
import tachiyomi.core.preference.Preference as PreferenceData

View File

@ -211,7 +211,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
showCreateDialog = false
flag = it
try {
chooseBackupDir.launch(Backup.getBackupFilename())
chooseBackupDir.launch(Backup.getFilename())
} catch (e: ActivityNotFoundException) {
flag = 0
context.toast(R.string.file_picker_error)

View File

@ -70,7 +70,7 @@ object SettingsTrackingScreen : SearchableSettings {
@Composable
override fun RowScope.AppBarAction() {
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(
imageVector = Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.tracking_guide),

View File

@ -149,7 +149,7 @@ object AboutScreen : Screen() {
item {
TextPreferenceWidget(
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 {
TextPreferenceWidget(
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 = {
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
name = it.name,
website = it.website,
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
name = it.library.name,
website = it.library.website,
license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
)
navigator.push(libraryLicenseScreen)
},

View File

@ -1,25 +1,25 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.orientationType
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.SettingsIconGrid
import tachiyomi.presentation.core.components.material.IconToggleButton
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
@ -32,22 +32,20 @@ fun OrientationModeSelectDialog(
val manga by screenModel.mangaFlow.collectAsState()
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
SettingsChipRow(R.string.rotation_type) {
orientationTypeOptions.map { (stringRes, it) ->
FilterChip(
selected = it == orientationType,
onClick = {
screenModel.onChangeOrientation(it)
AdaptiveSheet(onDismissRequest = onDismissRequest) {
Box(modifier = Modifier.padding(vertical = 16.dp)) {
SettingsIconGrid(R.string.rotation_type) {
items(orientationTypeOptions) { (stringRes, mode) ->
IconToggleButton(
checked = mode == orientationType,
onCheckedChange = {
screenModel.onChangeOrientation(mode)
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
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
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.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
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
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
@ -32,22 +33,20 @@ fun ReadingModeSelectDialog(
val manga by screenModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
SettingsChipRow(R.string.pref_category_reading_mode) {
readingModeOptions.map { (stringRes, it) ->
FilterChip(
selected = it == readingMode,
onClick = {
screenModel.onChangeReadingMode(it)
AdaptiveSheet(onDismissRequest = onDismissRequest) {
Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) {
SettingsIconGrid(R.string.pref_category_reading_mode) {
items(readingModeOptions) { (stringRes, mode) ->
IconToggleButton(
checked = mode == readingMode,
onCheckedChange = {
screenModel.onChangeReadingMode(mode)
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
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.clip(shape)
.background(MaterialTheme.colorScheme.surface)

View File

@ -21,7 +21,7 @@ fun UpdatesDeleteConfirmationDialog(
TextButton(onClick = {
onConfirm()
onDismissRequest()
},) {
}) {
Text(text = stringResource(R.string.action_ok))
}
},

View File

@ -175,7 +175,7 @@ fun WebViewScreenContent(
WarningBanner(
textRes = R.string.information_cloudflare_help,
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
val numberOfBackups = backupPreferences.numberOfBackups().get()
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
dir.listFiles { _, filename -> backupRegex.matches(filename) }
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(numberOfBackups - 1)
.forEach { it.delete() }
// Create new file to place backup
dir.createFile(Backup.getBackupFilename())
dir.createFile(Backup.getFilename())
} else {
UniFile.fromUri(context, uri)
}

View File

@ -14,7 +14,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import tachiyomi.domain.chapter.model.Chapter
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.track.model.Track
import uy.kohesive.injekt.Injekt
@ -31,10 +31,10 @@ class BackupRestorer(
) {
private val updateManga: UpdateManga = 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 currentFetchWindow = setFetchInterval.getWindow(now)
private var currentFetchWindow = fetchInterval.getWindow(now)
private var backupManager = BackupManager(context)
@ -103,7 +103,7 @@ class BackupRestorer(
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
sourceMapping = backupMaps.associate { it.sourceId to it.name }
now = ZonedDateTime.now()
currentFetchWindow = setFetchInterval.getWindow(now)
currentFetchWindow = fetchInterval.getWindow(now)
return coroutineScope {
// Restore individual manga

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.backup.models
import eu.kanade.tachiyomi.BuildConfig
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import java.text.SimpleDateFormat
@ -16,9 +17,11 @@ data class Backup(
) {
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())
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 kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import logcat.LogPriority
import okhttp3.Response
import okio.buffer
import okio.sink
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter
import uy.kohesive.injekt.injectLazy
import java.io.File
@ -97,6 +99,7 @@ class ChapterCache(private val context: Context) {
editor.commit()
editor.abortUnlessCommitted()
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Failed to put page list to cache" }
// Ignore.
} finally {
editor?.abortUnlessCommitted()
@ -174,7 +177,7 @@ class ChapterCache(private val context: Context) {
* @return status of deletion for the file.
*/
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.")) {
return false
}
@ -182,9 +185,10 @@ class ChapterCache(private val context: Context) {
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
// Remove file from cache
diskCache.remove(key)
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Failed to remove file from cache" }
false
}
}

View File

@ -43,7 +43,6 @@ import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.util.lang.awaitSingle
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withIOContext
@ -363,7 +362,7 @@ class Downloader(
if (page.imageUrl.isNullOrEmpty()) {
page.status = Page.State.LOAD_PAGE
try {
page.imageUrl = download.source.fetchImageUrl(page).awaitSingle()
page.imageUrl = download.source.getImageUrl(page)
} catch (e: Throwable) {
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_READ
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.GetManga
import tachiyomi.domain.manga.interactor.SetFetchInterval
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
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 syncChaptersWithSource: SyncChaptersWithSource = 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)
@ -216,7 +216,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val hasDownloads = AtomicBoolean(false)
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
val fetchWindow = setFetchInterval.getWindow(ZonedDateTime.now())
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
coroutineScope {
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_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

View File

@ -329,11 +329,11 @@ class LibraryUpdateNotifier(private val context: Context) {
}
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_TITLE_MAX_LEN = 45
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
import android.annotation.SuppressLint
import android.content.ContentUris
import android.content.Context
import android.graphics.Bitmap
@ -28,30 +27,59 @@ class ImageSaver(
val context: Context,
) {
@SuppressLint("InlinedApi")
fun save(image: Image): Uri {
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}")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
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 =
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 relativePath = listOf(
Environment.DIRECTORY_PICTURES,
context.getString(R.string.app_name),
imageLocation,
).joinToString(File.separator)
val contentValues = contentValuesOf(
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
MediaStore.Images.Media.DISPLAY_NAME to image.name,
MediaStore.Images.Media.MIME_TYPE to type.mime,
MediaStore.Images.Media.RELATIVE_PATH to folderRelativePath + imageLocation,
)
val picture = findUriOrDefault(folderRelativePath, "$imageLocation$filename") {
val picture = findUriOrDefault(relativePath, filename) {
context.contentResolver.insert(
pictureDir,
contentValues,
@ -74,49 +102,34 @@ class ImageSaver(
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)
private fun findUriOrDefault(relativePath: String, imagePath: String, default: () -> Uri): Uri {
private fun findUriOrDefault(path: String, filename: String, default: () -> Uri): Uri {
val projection = arrayOf(
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.MediaColumns.RELATIVE_PATH,
MediaStore.MediaColumns.DATE_MODIFIED,
)
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(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
arrayOf(relativePath, imagePath),
arrayOf(normalizedPath, filename),
null,
).use { cursor ->
if (cursor != null && cursor.count >= 1) {
cursor.moveToFirst().let {
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
}
}
}
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
}
val preferences: SharedPreferences by lazy {
context.getSharedPreferences("source_$sourceSuffixID", 0x0000)
context.getSharedPreferences("source_$sourceSuffixID", Context.MODE_PRIVATE)
}
val prefApiUrl = preferences.getString("APIURL", "")!!
if (prefApiUrl.isEmpty()) {

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.track.suwayomi
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
@ -107,7 +108,7 @@ class TachideskApi {
}
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)!!

View File

@ -143,7 +143,7 @@ internal class AppUpdateNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.update_check_notification_update_available))
setContentText(context.getString(R.string.update_check_fdroid_migration_info))
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)
}

View File

@ -102,7 +102,7 @@ class ExtensionDetailsScreenModel(
val extension = state.value.extension ?: return ""
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.")

View File

@ -31,7 +31,7 @@ fun Screen.migrateSourceTab(): TabContent {
title = stringResource(R.string.migration_help_guide),
icon = Icons.Outlined.HelpOutline,
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.PagingConfig
import androidx.paging.cachedIn
import androidx.paging.filter
import androidx.paging.map
import cafe.adriel.voyager.core.model.StateScreenModel
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.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
@ -113,25 +113,20 @@ class BrowseSourceScreenModel(
/**
* Flow of Pager flow tied to [State.listing]
*/
private val hideInLibraryItems = sourcePreferences.hideInLibraryItems().get()
val mangaPagerFlowFlow = state.map { it.listing }
.distinctUntilChanged()
.map { listing ->
Pager(
PagingConfig(pageSize = 25),
) {
Pager(PagingConfig(pageSize = 25)) {
getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters)
}.flow.map { pagingData ->
pagingData.map {
networkToLocalManga.await(it.toDomainManga(sourceId))
.let { localManga ->
getManga.subscribe(localManga.url, localManga.source)
}
.let { localManga -> getManga.subscribe(localManga.url, localManga.source) }
.filterNotNull()
.filter { localManga ->
!sourcePreferences.hideInLibraryItems().get() || !localManga.favorite
}
.stateIn(ioCoroutineScope)
}
.filter { !hideInLibraryItems || !it.value.favorite }
}
.cachedIn(ioCoroutineScope)
}

View File

@ -64,7 +64,7 @@ fun SourceFilterDialog(
Button(onClick = {
onFilter()
onDismissRequest()
},) {
}) {
Text(stringResource(R.string.action_filter))
}
}

View File

@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.awaitSingle
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga
@ -140,7 +139,7 @@ abstract class SearchScreenModel(
try {
val page = withContext(coroutineDispatcher) {
source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle()
source.getSearchManga(1, query, source.getFilterList())
}
val titles = page.mangas.map {

View File

@ -1,13 +1,13 @@
package eu.kanade.tachiyomi.ui.download
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.isSystemInDarkTheme
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.material.icons.Icons
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.viewinterop.AndroidView
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.core.model.rememberScreenModel
@ -243,6 +242,7 @@ object DownloadQueueScreen : Screen() {
)
return@Scaffold
}
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
@ -252,13 +252,13 @@ object DownloadQueueScreen : Screen() {
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
screenModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context))
screenModel.adapter = DownloadAdapter(screenModel.listener)
screenModel.controllerBinding.recycler.adapter = screenModel.adapter
screenModel.controllerBinding.root.adapter = screenModel.adapter
screenModel.adapter?.isHandleDragEnabled = true
screenModel.adapter?.fastScroller = screenModel.controllerBinding.fastScroller
screenModel.controllerBinding.recycler.layoutManager = LinearLayoutManager(context)
screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(context)
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true)
@ -274,7 +274,7 @@ object DownloadQueueScreen : Screen() {
screenModel.controllerBinding.root
},
update = {
screenModel.controllerBinding.recycler
screenModel.controllerBinding.root
.updatePadding(
left = left,
top = top,
@ -282,14 +282,6 @@ object DownloadQueueScreen : Screen() {
bottom = bottom,
)
screenModel.controllerBinding.fastScroller
.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = left
topMargin = top
rightMargin = right
bottomMargin = bottom
}
screenModel.adapter?.updateDataSet(downloadList)
},
)

View File

@ -84,13 +84,17 @@ class DownloadQueueScreenModel(
}
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
?.filterIsInstance<DownloadItem>()
?.map(DownloadItem::download)
?.partition { item.download.manga.id == it.manga.id }
?: Pair(emptyList(), emptyList())
reorder(selectedSeries + otherSeries)
if (menuItem.itemId == R.id.move_to_top_series) {
reorder(selectedSeries + otherSeries)
} else {
reorder(otherSeries + selectedSeries)
}
}
R.id.cancel_download -> {
cancel(listOf(item.download))
@ -258,6 +262,6 @@ class DownloadQueueScreenModel(
* @return the holder of the download or null if it's not bound.
*/
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(
stringResId = R.string.getting_started_guide,
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,
onSelectionChange = sm::setSelection,
selections = remember { sm.getSelections() },
onConfirm = { sm.setStatus(); navigator.pop() },
onConfirm = {
sm.setStatus()
navigator.pop()
},
onDismissRequest = navigator::pop,
)
}
@ -322,7 +325,10 @@ private data class TrackChapterSelectorScreen(
selection = state.selection,
onSelectionChange = sm::setSelection,
range = remember { sm.getRange() },
onConfirm = { sm.setChapter(); navigator.pop() },
onConfirm = {
sm.setChapter()
navigator.pop()
},
onDismissRequest = navigator::pop,
)
}
@ -378,7 +384,10 @@ private data class TrackScoreSelectorScreen(
selection = state.selection,
onSelectionChange = sm::setSelection,
selections = remember { sm.getSelections() },
onConfirm = { sm.setScore(); navigator.pop() },
onConfirm = {
sm.setScore()
navigator.pop()
},
onDismissRequest = navigator::pop,
)
}
@ -495,7 +504,10 @@ private data class TrackDateSelectorScreen(
},
initialSelectedDateMillis = sm.initialSelection,
selectableDates = selectableDates,
onConfirm = { sm.setDate(it); navigator.pop() },
onConfirm = {
sm.setDate(it)
navigator.pop()
},
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
onDismissRequest = navigator::pop,
)
@ -584,7 +596,10 @@ private data class TrackDateRemoverScreen(
Text(text = stringResource(android.R.string.cancel))
}
FilledTonalButton(
onClick = { sm.removeDate(); navigator.popUntil { it is TrackInfoDialogHomeScreen } },
onClick = {
sm.removeDate()
navigator.popUntil { it is TrackInfoDialogHomeScreen }
},
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
@ -646,7 +661,10 @@ data class TrackServiceSearchScreen(
queryResult = state.queryResult,
selected = state.selected,
onSelectedChange = sm::updateSelection,
onConfirmSelection = { sm.registerTracking(state.selected!!); navigator.pop() },
onConfirmSelection = {
sm.registerTracking(state.selected!!)
navigator.pop()
},
onDismissRequest = navigator::pop,
)
}

View File

@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine
import tachiyomi.core.util.lang.awaitSingle
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.Injekt
@ -170,7 +169,7 @@ internal class HttpPageLoader(
try {
if (page.imageUrl.isNullOrEmpty()) {
page.status = Page.State.LOAD_PAGE
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
page.imageUrl = source.getImageUrl(page)
}
val imageUrl = page.imageUrl!!

View File

@ -19,7 +19,7 @@ class UnlockActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startAuthentication(
getString(R.string.unlock_app),
getString(R.string.unlock_app_title, getString(R.string.app_name)),
confirmationRequired = false,
callback = object : AuthenticatorUtil.AuthenticationCallback() {
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.library.LibraryUpdateJob
import eu.kanade.tachiyomi.util.lang.toDateKey
import eu.kanade.tachiyomi.util.lang.toRelativeString
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
@ -384,7 +383,7 @@ class UpdatesScreenModel(
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
when {
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
val text = afterDate.toRelativeString(context, dateFormat)
val text = dateFormat.format(afterDate)
UpdatesUiModel.Header(text)
}
// 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 eu.kanade.tachiyomi.BuildConfig
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.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
@ -35,6 +36,7 @@ class CrashLogUtil(private val context: Context) {
Device name: ${Build.DEVICE}
Device model: ${Build.MODEL}
Device product name: ${Build.PRODUCT}
WebView user agent: ${WebViewUtil.getInferredUserAgent(context)}
""".trimIndent()
}
}

View File

@ -1,14 +1,11 @@
package eu.kanade.tachiyomi.util.lang
import android.content.Context
import eu.kanade.tachiyomi.R
import java.text.DateFormat
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.Calendar
import java.util.Date
import java.util.TimeZone
fun Date.toDateTimestampString(dateFormatter: DateFormat): String {
val date = dateFormatter.format(this)
@ -45,101 +42,3 @@ fun Long.toDateKey(): Date {
cal[Calendar.MILLISECOND] = 0
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.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.view.View
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode
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
}
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.).
*

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"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
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_height="match_parent"
android:clipToPadding="false"
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>
android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@layout/download_item" />

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: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
android:id="@+id/cancel_download"
android:title="@string/action_cancel" />

View File

@ -56,21 +56,6 @@
</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-->
<!--===========-->

View File

@ -5,7 +5,7 @@ plugins {
dependencies {
implementation(androidxLibs.gradle)
implementation(kotlinLibs.gradle)
implementation(libs.kotlinter)
implementation(libs.ktlint)
implementation(gradleApi())
}

View File

@ -1,20 +1,14 @@
import org.jmailen.gradle.kotlinter.KotlinterExtension
import org.jmailen.gradle.kotlinter.KotlinterPlugin
import org.jlleitschuh.gradle.ktlint.KtlintExtension
import org.jlleitschuh.gradle.ktlint.KtlintPlugin
apply<KotlinterPlugin>()
apply<KtlintPlugin>()
extensions.configure<KotlinterExtension>("kotlinter") {
experimentalRules = true
extensions.configure<KtlintExtension>("ktlint") {
version.set("0.50.0")
android.set(true)
enableExperimentalRules.set(true)
disabledRules = arrayOf(
"experimental:argument-list-wrapping", // Doesn't play well with Android Studio
"filename", // Often broken to give a more general name
)
}
tasks {
named<DefaultTask>("preBuild").configure {
if (!System.getenv("CI").toBoolean())
dependsOn("formatKotlin")
filter {
exclude("**/generated/**")
}
}

View File

@ -11,7 +11,6 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
}
dependencies {

View File

@ -39,7 +39,9 @@ class NetworkHelper(
builder.addNetworkInterceptor(httpLoggingInterceptor)
}
builder.addInterceptor(CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider))
builder.addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
)
when (preferences.dohProvider().get()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()

View File

@ -17,6 +17,9 @@ class NetworkPreferences(
}
fun defaultUserAgent(): Preference<String> {
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/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
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
@ -95,6 +104,9 @@ suspend fun Call.await(): Response {
return await(callStack)
}
/**
* @since extensions-lib 1.5
*/
suspend fun Call.awaitSuccess(): Response {
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
val response = await(callStack)
@ -105,15 +117,6 @@ suspend fun Call.awaitSuccess(): 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 {
val progressClient = newBuilder()
.cache(null)

View File

@ -9,7 +9,10 @@ import okio.Source
import okio.buffer
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 {
source(responseBody.source()).buffer()
@ -36,7 +39,11 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
val bytesRead = super.read(sink, byteCount)
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
progressListener.update(
totalBytesRead,
responseBody.contentLength(),
bytesRead == -1L,
)
return bytesRead
}
}

View File

@ -31,7 +31,11 @@ class CloudflareInterceptor(
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 {
response.close()
cookieManager.remove(request.url, COOKIE_NAMES, 0)

View File

@ -33,7 +33,9 @@ fun OkHttpClient.Builder.rateLimitHost(
permits: Int,
period: Long = 1,
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.
@ -69,8 +71,5 @@ fun OkHttpClient.Builder.rateLimitHost(
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
url: String,
permits: Int,
period: Duration = 1.seconds,
) = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
fun OkHttpClient.Builder.rateLimitHost(url: String, 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 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)
}
@ -20,7 +24,11 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT,
* @param text the text to display.
* @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 {
block(it)
it.show()

View File

@ -47,10 +47,7 @@ abstract class WebViewClientCompat : WebViewClient() {
return shouldInterceptRequestCompat(view, request.url.toString())
}
final override fun shouldInterceptRequest(
view: WebView,
url: String,
): WebResourceResponse? {
final override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
return shouldInterceptRequestCompat(view, url)
}

View File

@ -14,7 +14,24 @@ import kotlin.coroutines.resume
object WebViewUtil {
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 {
try {

View File

@ -1,7 +1,7 @@
package tachiyomi.core
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"

View File

@ -68,7 +68,11 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: String,
) : 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
}
@ -128,7 +132,11 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: Boolean,
) : 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)
}
@ -143,7 +151,11 @@ sealed class AndroidPreference<T>(
key: String,
defaultValue: Set<String>,
) : 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
}

View File

@ -64,7 +64,11 @@ class AndroidPreferenceStore(
private val SharedPreferences.keyFlow
get() = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> trySend(key) }
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? ->
trySend(
key,
)
}
registerOnSharedPreferenceChangeListener(listener)
awaitClose {
unregisterOnSharedPreferenceChangeListener(listener)

View File

@ -23,7 +23,9 @@ interface Preference<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) {
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 =
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) =
withContext(NonCancellable, block)

View File

@ -11,13 +11,14 @@ object DateColumnAdapter : ColumnAdapter<Date, Long> {
private const val LIST_OF_STRINGS_SEPARATOR = ", "
object StringListColumnAdapter : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) =
if (databaseValue.isEmpty()) {
emptyList()
} else {
databaseValue.split(LIST_OF_STRINGS_SEPARATOR)
}
override fun encode(value: List<String>) = value.joinToString(separator = LIST_OF_STRINGS_SEPARATOR)
override fun decode(databaseValue: String) = if (databaseValue.isEmpty()) {
emptyList()
} else {
databaseValue.split(LIST_OF_STRINGS_SEPARATOR)
}
override fun encode(value: List<String>) = value.joinToString(
separator = LIST_OF_STRINGS_SEPARATOR,
)
}
object UpdateStrategyColumnAdapter : ColumnAdapter<UpdateStrategy, Long> {

View File

@ -2,7 +2,21 @@ package tachiyomi.data.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 ->
Chapter(
id = id,

View File

@ -81,7 +81,12 @@ class ChapterRepositoryImpl(
}
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? {
@ -89,10 +94,21 @@ class ChapterRepositoryImpl(
}
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? {
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 ->
HistoryWithRelations(
id = historyId,

View File

@ -4,7 +4,30 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
import tachiyomi.domain.library.model.LibraryManga
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 ->
Manga(
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 ->
LibraryManga(
manga = mangaMapper(

View File

@ -24,11 +24,23 @@ class MangaRepositoryImpl(
}
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?> {
return handler.subscribeToOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
return handler.subscribeToOneOrNull {
mangasQueries.getMangaByUrlAndSource(
url,
sourceId,
mangaMapper,
)
}
}
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
*/
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 = {
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.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.core.util.lang.awaitSingle
import tachiyomi.core.util.lang.withIOContext
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 {
return source.fetchSearchManga(currentPage, query, filters).awaitSingle()
return source.getSearchManga(currentPage, query, filters)
}
}
class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchPopularManga(currentPage).awaitSingle()
return source.getPopularManga(currentPage)
}
}
class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
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
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 ->
Track(
id = id,

View File

@ -3,7 +3,22 @@ package tachiyomi.data.updates
import tachiyomi.domain.manga.model.MangaCover
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 ->
UpdatesWithRelations(
mangaId = mangaId,

View File

@ -9,7 +9,11 @@ class UpdatesRepositoryImpl(
private val databaseHandler: DatabaseHandler,
) : 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 {
updatesViewQueries.getUpdatesByReadStatus(
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 {
updatesViewQueries.getUpdatesByReadStatus(
read = read,

View File

@ -16,11 +16,9 @@ class ReorderCategory(
private val mutex = Mutex()
suspend fun moveUp(category: Category): Result =
await(category, MoveTo.UP)
suspend fun moveUp(category: Category): Result = await(category, MoveTo.UP)
suspend fun moveDown(category: Category): Result =
await(category, MoveTo.DOWN)
suspend fun moveDown(category: Category): Result = await(category, MoveTo.DOWN)
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
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)
}
}

View File

@ -30,7 +30,11 @@ object ChapterRecognition {
*/
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 (chapterNumber != null && (chapterNumber == -2.0 || chapterNumber > -1.0)) {
return chapterNumber

View File

@ -3,7 +3,13 @@ package tachiyomi.domain.chapter.service
import tachiyomi.domain.chapter.model.Chapter
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) {
Manga.CHAPTER_SORTING_SOURCE -> when (sortDescending) {
true -> { c1, c2 -> c1.sourceOrder.compareTo(c2.sourceOrder) }

View File

@ -8,9 +8,15 @@ class DownloadPreferences(
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)
@ -20,15 +26,27 @@ class DownloadPreferences(
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 removeExcludeCategories() = preferenceStore.getStringSet("remove_exclude_categories", emptySet())
fun removeExcludeCategories() = preferenceStore.getStringSet(
"remove_exclude_categories",
emptySet(),
)
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 currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId }
val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size)

View File

@ -65,7 +65,18 @@ data class LibrarySort(
}
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 default = LibrarySort(Type.Alphabetical, Direction.Ascending)

View File

@ -11,9 +11,19 @@ class LibraryPreferences(
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)
@ -42,31 +52,64 @@ class LibraryPreferences(
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
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 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
@ -97,24 +140,45 @@ class LibraryPreferences(
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
// 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
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) {
filterChapterByRead().set(manga.unreadFilterRaw)
@ -122,7 +186,9 @@ class LibraryPreferences(
filterChapterByBookmarked().set(manga.bookmarkedFilterRaw)
sortChapterBySourceOrNumber().set(manga.sorting)
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)
@ -131,9 +197,15 @@ class LibraryPreferences(
// 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

View File

@ -5,14 +5,12 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
const val MAX_FETCH_INTERVAL = 28
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
class SetFetchInterval(
class FetchInterval(
private val getChapterByMangaId: GetChapterByMangaId,
) {
@ -27,7 +25,10 @@ class SetFetchInterval(
window
}
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)
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
@ -39,31 +40,34 @@ class SetFetchInterval(
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
val lowerBound = today.minusDays(GRACE_PERIOD)
val upperBound = today.plusDays(GRACE_PERIOD)
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
}
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
val sortedChapters = chapters
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
.take(50)
val uploadDates = sortedChapters
internal fun calculateInterval(chapters: List<Chapter>, zone: ZoneId): Int {
val uploadDates = chapters.asSequence()
.filter { it.dateUpload > 0L }
.sortedByDescending { it.dateUpload }
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
.toLocalDate()
.atStartOfDay()
}
.distinct()
val fetchDates = sortedChapters
.take(10)
.toList()
val fetchDates = chapters.asSequence()
.sortedByDescending { it.dateFetch }
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
.toLocalDate()
.atStartOfDay()
}
.distinct()
.take(10)
.toList()
val interval = when {
// Enough upload date from source
@ -82,7 +86,7 @@ class SetFetchInterval(
else -> 7
}
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
return interval.coerceIn(1, MAX_INTERVAL)
}
private fun calculateNextUpdate(
@ -95,7 +99,10 @@ class SetFetchInterval(
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
manga.fetchInterval == 0
) {
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone)
val latestDate = ZonedDateTime.ofInstant(
Instant.ofEpochMilli(manga.lastUpdate),
dateTime.zone,
)
.toLocalDate()
.atStartOfDay()
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
@ -110,7 +117,7 @@ class SetFetchInterval(
}
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
val cycle = timeSinceLatest.floorDiv(delta) + 1
@ -120,4 +127,10 @@ class SetFetchInterval(
delta
}
}
companion object {
const val MAX_INTERVAL = 28
private const val GRACE_PERIOD = 1L
}
}

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