chore: merge upstream.

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

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

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/appwidget_updates_description"
android:previewImage="@drawable/updates_grid_widget_preview"
android:initialLayout="@layout/appwidget_loading"
android:minWidth="240dp"
android:minHeight="80dp"
android:minResizeWidth="80dp"
android:minResizeHeight="110dp"
android:maxResizeWidth="600dp"
android:maxResizeHeight="600dp"
android:targetCellWidth="4"
android:targetCellHeight="2"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen" />