chore: merge upstream.

This commit is contained in:
KaiserBh 2023-11-09 23:37:56 +11:00
commit d0eaf5e3cb
275 changed files with 4294 additions and 2587 deletions

View File

@ -1,7 +1,8 @@
[*.{kt,kts}]
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
max_line_length = 120
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

View File

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

View File

@ -53,7 +53,7 @@ body:
label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**.
placeholder: |
Example: "0.14.6"
Example: "0.14.7"
validations:
required: true
@ -98,7 +98,7 @@ body:
required: true
- 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)**.
- label: I have updated the app to version **[0.14.7](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true
- label: I have updated all installed extensions.
required: true

View File

@ -33,7 +33,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 updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
- label: I have updated the app to version **[0.14.7](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -22,8 +22,8 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
versionCode = 107
versionName = "0.14.6"
versionCode = 109
versionName = "0.14.7"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -304,12 +304,12 @@ tasks {
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics",
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
)
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_metrics",
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
)
}
}

View File

@ -159,10 +159,6 @@
android:name=".data.download.DownloadService"
android:exported="false" />
<service
android:name=".data.updater.AppUpdateService"
android:exported="false" />
<service
android:name=".extension.util.ExtensionInstallService"
android:exported="false" />

View File

@ -1,11 +1,14 @@
package eu.kanade.domain
import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.download.interactor.DeleteDownload
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetEnabledSources
@ -40,8 +43,8 @@ import tachiyomi.domain.category.interactor.SetSortModeForCategory
import tachiyomi.domain.category.interactor.UpdateCategory
import tachiyomi.domain.category.repository.CategoryRepository
import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.chapter.interactor.UpdateChapter
@ -112,13 +115,15 @@ class DomainModule : InjektModule {
addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get(), get()) }
addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) }
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { TrackChapter(get(), get(), get(), get()) }
addFactory { AddTracks(get(), get(), get()) }
addFactory { AddTracks(get(), get(), get(), get()) }
addFactory { RefreshTracks(get(), get(), get(), get()) }
addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) }
@ -128,12 +133,13 @@ class DomainModule : InjektModule {
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) }
addFactory { GetChaptersByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) }

View File

@ -5,6 +5,7 @@ import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
class BasePreferences(
@ -12,9 +13,12 @@ class BasePreferences(
private val preferenceStore: PreferenceStore,
) {
fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false)
fun downloadedOnly() = preferenceStore.getBoolean(
Preference.appStateKey("pref_downloaded_only"),
false,
)
fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false)
fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)

View File

@ -0,0 +1,24 @@
package eu.kanade.domain.chapter.interactor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.domain.chapter.repository.ChapterRepository
class GetAvailableScanlators(
private val repository: ChapterRepository,
) {
private fun List<String>.cleanupAvailableScanlators(): Set<String> {
return mapNotNull { it.ifBlank { null } }.toSet()
}
suspend fun await(mangaId: Long): Set<String> {
return repository.getScanlatorsByMangaId(mangaId)
.cleanupAvailableScanlators()
}
fun subscribe(mangaId: Long): Flow<Set<String>> {
return repository.getScanlatorsByMangaIdAsFlow(mangaId)
.map { it.cleanupAvailableScanlators() }
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.copyFromSChapter
import eu.kanade.domain.chapter.model.toSChapter
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.download.DownloadManager
@ -10,7 +11,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource
import tachiyomi.data.chapter.ChapterSanitizer
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.Chapter
@ -32,7 +33,8 @@ class SyncChaptersWithSource(
private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
private val updateManga: UpdateManga,
private val updateChapter: UpdateChapter,
private val getChapterByMangaId: GetChapterByMangaId,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators,
) {
/**
@ -66,7 +68,7 @@ class SyncChaptersWithSource(
}
// Chapters from db.
val dbChapters = getChapterByMangaId.await(manga.id)
val dbChapters = getChaptersByMangaId.await(manga.id)
// Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>()
@ -116,7 +118,9 @@ class SyncChaptersWithSource(
} else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
downloadManager.isChapterDownloaded(
dbChapter.name, dbChapter.scanlator, manga.title, manga.source,
)
if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter)
@ -206,6 +210,10 @@ class SyncChaptersWithSource(
val reAddedUrls = reAdded.map { it.url }.toHashSet()
return updatedToAdd.filterNot { it.url in reAddedUrls }
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
return updatedToAdd.filterNot {
it.url in reAddedUrls || it.scanlator in excludedScanlators
}
}
}

View File

@ -2,7 +2,6 @@ package eu.kanade.domain.chapter.model
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.source.model.SChapter
import tachiyomi.data.Chapters
import tachiyomi.domain.chapter.model.Chapter
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
@ -23,18 +22,7 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
url = sChapter.url,
dateUpload = sChapter.date_upload,
chapterNumber = sChapter.chapter_number.toDouble(),
scanlator = sChapter.scanlator?.ifBlank { null },
)
}
fun Chapter.copyFrom(other: Chapters): Chapter {
return copy(
name = other.name,
url = other.url,
dateUpload = other.date_upload,
chapterNumber = other.chapter_number,
scanlator = other.scanlator?.ifBlank { null },
lastModifiedAt = other.last_modified_at,
scanlator = sChapter.scanlator?.ifBlank { null }?.trim(),
)
}

View File

@ -2,7 +2,7 @@ package eu.kanade.domain.chapter.model
import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.ui.manga.ChapterItem
import eu.kanade.tachiyomi.ui.manga.ChapterList
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.getChapterSort
import tachiyomi.domain.manga.model.Manga
@ -23,7 +23,12 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
.filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } }
.filter { chapter ->
applyFilter(downloadedFilter) {
val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
val downloaded = downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
manga.title,
manga.source,
)
downloaded || isLocalManga
}
}
@ -34,7 +39,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
* Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted.
*/
fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> {
val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter

View File

@ -0,0 +1,24 @@
package eu.kanade.domain.manga.interactor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.DatabaseHandler
class GetExcludedScanlators(
private val handler: DatabaseHandler,
) {
suspend fun await(mangaId: Long): Set<String> {
return handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
.toSet()
}
fun subscribe(mangaId: Long): Flow<Set<String>> {
return handler.subscribeToList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
.map { it.toSet() }
}
}

View File

@ -0,0 +1,22 @@
package eu.kanade.domain.manga.interactor
import tachiyomi.data.DatabaseHandler
class SetExcludedScanlators(
private val handler: DatabaseHandler,
) {
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
handler.await(inTransaction = true) {
val currentExcluded = handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}.toSet()
val toAdd = excludedScanlators.minus(currentExcluded)
for (scanlator in toAdd) {
excluded_scanlatorsQueries.insert(mangaId, scanlator)
}
val toRemove = currentExcluded.minus(excludedScanlators)
excluded_scanlatorsQueries.remove(mangaId, toRemove)
}
}
}

View File

@ -1,7 +1,7 @@
package eu.kanade.domain.manga.interactor
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
@ -9,22 +9,22 @@ class SetMangaViewerFlags(
private val mangaRepository: MangaRepository,
) {
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) {
suspend fun awaitSetReadingMode(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id)
mangaRepository.update(
MangaUpdate(
id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingModeType.MASK.toLong()),
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()),
),
)
}
suspend fun awaitSetOrientationType(id: Long, flag: Long) {
suspend fun awaitSetOrientation(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id)
mangaRepository.update(
MangaUpdate(
id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, OrientationType.MASK.toLong()),
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()),
),
)
}

View File

@ -3,8 +3,8 @@ package eu.kanade.domain.manga.model
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
import tachiyomi.core.preference.TriState
@ -14,11 +14,11 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
// TODO: move these into the domain model
val Manga.readingModeType: Long
get() = viewerFlags and ReadingModeType.MASK.toLong()
val Manga.readingMode: Long
get() = viewerFlags and ReadingMode.MASK.toLong()
val Manga.orientationType: Long
get() = viewerFlags and OrientationType.MASK.toLong()
val Manga.readerOrientation: Long
get() = viewerFlags and ReaderOrientation.MASK.toLong()
val Manga.downloadedFilter: TriState
get() {

View File

@ -3,12 +3,11 @@ package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import tachiyomi.core.util.lang.compareToWithCollator
import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.repository.SourceRepository
import tachiyomi.source.local.isLocal
import java.text.Collator
import java.util.Collections
import java.util.Locale
class GetSourcesWithFavoriteCount(
private val repository: SourceRepository,
@ -31,17 +30,13 @@ class GetSourcesWithFavoriteCount(
direction: SetMigrateSorting.Direction,
sorting: SetMigrateSorting.Mode,
): java.util.Comparator<Pair<Source, Long>> {
val locale = Locale.getDefault()
val collator = Collator.getInstance(locale).apply {
strength = Collator.PRIMARY
}
val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b ->
when (sorting) {
SetMigrateSorting.Mode.ALPHABETICAL -> {
when {
a.first.isStub && b.first.isStub.not() -> -1
b.first.isStub && a.first.isStub.not() -> 1
else -> collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale))
else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
}
}
SetMigrateSorting.Mode.TOTAL -> {

View File

@ -2,6 +2,7 @@ package eu.kanade.domain.source.service
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.getEnum
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -10,7 +11,12 @@ class SourcePreferences(
private val preferenceStore: PreferenceStore,
) {
fun sourceDisplayMode() = preferenceStore.getObject("pref_display_mode_catalogue", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
fun sourceDisplayMode() = preferenceStore.getObject(
"pref_display_mode_catalogue",
LibraryDisplayMode.default,
LibraryDisplayMode.Serializer::serialize,
LibraryDisplayMode.Serializer::deserialize,
)
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
@ -18,17 +24,23 @@ class SourcePreferences(
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedSource() = preferenceStore.getLong("last_catalogue_source", -1)
fun lastUsedSource() = preferenceStore.getLong(
Preference.appStateKey("last_catalogue_source"),
-1,
)
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
fun migrationSortingDirection() = preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING)
fun migrationSortingDirection() = preferenceStore.getEnum(
"pref_migration_direction",
SetMigrateSorting.Direction.ASCENDING,
)
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
}

View File

@ -1,23 +1,84 @@
package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZoneOffset
class AddTracks(
private val getTracks: GetTracks,
private val insertTrack: InsertTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
private val getChaptersByMangaId: GetChaptersByMangaId,
) {
suspend fun bindEnhancedTracks(manga: Manga, source: Source) = withNonCancellableContext {
// TODO: update all trackers based on common data
suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withNonCancellableContext {
withIOContext {
val allChapters = getChaptersByMangaId.await(mangaId)
val hasReadChapters = allChapters.any { it.read }
tracker.bind(item, hasReadChapters)
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
insertTrack.await(track)
// TODO: merge into [SyncChapterProgressWithTrack]?
// Update chapter progress if newer chapters marked read locally
if (hasReadChapters) {
val latestLocalReadChapterNumber = allChapters
.sortedBy { it.chapterNumber }
.takeWhile { it.read }
.lastOrNull()
?.chapterNumber ?: -1.0
if (latestLocalReadChapterNumber > track.lastChapterRead) {
track = track.copy(
lastChapterRead = latestLocalReadChapterNumber,
)
tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
}
if (track.startDate <= 0) {
val firstReadChapterDate = Injekt.get<GetHistory>().await(mangaId)
.sortedBy { it.readAt }
.firstOrNull()
?.readAt
firstReadChapterDate?.let {
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
ZoneOffset.systemDefault(),
ZoneOffset.UTC,
)
track = track.copy(
startDate = startDate,
)
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
}
}
}
syncChapterProgressWithTrack.await(mangaId, track, tracker)
}
}
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
withIOContext {
getTracks.await(manga.id)
.filterIsInstance<EnhancedTracker>()
.filter { it.accept(source) }
@ -42,4 +103,5 @@ class AddTracks(
}
}
}
}
}

View File

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.track.interactor.InsertTrack
@ -14,7 +14,7 @@ import tachiyomi.domain.track.model.Track
class SyncChapterProgressWithTrack(
private val updateChapter: UpdateChapter,
private val insertTrack: InsertTrack,
private val getChapterByMangaId: GetChapterByMangaId,
private val getChaptersByMangaId: GetChaptersByMangaId,
) {
suspend fun await(
@ -26,7 +26,7 @@ class SyncChapterProgressWithTrack(
return
}
val sortedChapters = getChapterByMangaId.await(mangaId)
val sortedChapters = getChaptersByMangaId.await(mangaId)
.sortedBy { it.chapterNumber }
.filter { it.isRecognizedNumber }

View File

@ -43,7 +43,9 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
}
.forEach { track ->
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" }
logcat(LogPriority.DEBUG) {
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
}
trackChapter.await(context, track.mangaId, track.lastChapterRead)
}
}

View File

@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.SnackbarDuration
@ -79,7 +79,7 @@ fun BrowseSourceContent(
listOf(
EmptyScreenAction(
stringResId = R.string.local_source_help_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick,
),
)
@ -97,7 +97,7 @@ fun BrowseSourceContent(
),
EmptyScreenAction(
stringResId = R.string.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onHelpClick,
),
)

View File

@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
@ -92,7 +92,7 @@ fun ExtensionDetailsScreen(
add(
AppBar.Action(
title = stringResource(R.string.action_faq_and_guides),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onClickReadme,
),
)

View File

@ -74,7 +74,9 @@ internal fun GlobalSearchContent(
items.forEach { (source, result) ->
item(key = source.id) {
GlobalSearchResultItem(
title = fromSourceId?.let { "${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
title = fromSourceId?.let {
"${source.name}".takeIf { source.id == fromSourceId }
} ?: source.name,
subtitle = LocaleHelper.getDisplayName(source.lang),
onClick = { onClickSource(source) },
) {

View File

@ -102,14 +102,26 @@ private fun MigrateSourceList(
IconButton(onClick = onToggleSortingMode) {
when (sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(Icons.Outlined.SortByAlpha, contentDescription = stringResource(R.string.action_sort_alpha))
SetMigrateSorting.Mode.TOTAL -> Icon(Icons.Outlined.Numbers, contentDescription = stringResource(R.string.action_sort_count))
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
Icons.Outlined.SortByAlpha,
contentDescription = stringResource(R.string.action_sort_alpha),
)
SetMigrateSorting.Mode.TOTAL -> Icon(
Icons.Outlined.Numbers,
contentDescription = stringResource(R.string.action_sort_count),
)
}
}
IconButton(onClick = onToggleSortingDirection) {
when (sortingDirection) {
SetMigrateSorting.Direction.ASCENDING -> Icon(Icons.Outlined.ArrowUpward, contentDescription = stringResource(R.string.action_asc))
SetMigrateSorting.Direction.DESCENDING -> Icon(Icons.Outlined.ArrowDownward, contentDescription = stringResource(R.string.action_desc))
SetMigrateSorting.Direction.ASCENDING -> Icon(
Icons.Outlined.ArrowUpward,
contentDescription = stringResource(R.string.action_asc),
)
SetMigrateSorting.Direction.DESCENDING -> Icon(
Icons.Outlined.ArrowDownward,
contentDescription = stringResource(R.string.action_desc),
)
}
}
}

View File

@ -144,7 +144,13 @@ private fun SourcePinButton(
onClick: () -> Unit,
) {
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground.copy(alpha = SecondaryItemAlpha)
val tint = if (isPinned) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onBackground.copy(
alpha = SecondaryItemAlpha,
)
}
val description = if (isPinned) R.string.action_unpin else R.string.action_pin
IconButton(onClick = onClick) {
Icon(

View File

@ -25,7 +25,9 @@ fun BaseSourceItem(
action: @Composable RowScope.(Source) -> Unit = {},
content: @Composable RowScope.(Source, String?) -> Unit = defaultContent,
) {
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { showLanguageInContent }
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf {
showLanguageInContent
}
BaseBrowseItem(
modifier = modifier,
onClickItem = onClickItem,

View File

@ -1,7 +1,7 @@
package eu.kanade.presentation.browse.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ViewList
import androidx.compose.material.icons.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
@ -56,7 +56,11 @@ fun BrowseSourceToolbar(
actions = listOfNotNull(
AppBar.Action(
title = stringResource(R.string.action_display_mode),
icon = if (displayMode == LibraryDisplayMode.List) Icons.AutoMirrored.Filled.ViewList else Icons.Filled.ViewModule,
icon = if (displayMode == LibraryDisplayMode.List) {
Icons.Filled.ViewList
} else {
Icons.Filled.ViewModule
},
onClick = { selectingDisplayMode = true },
),
if (isLocalSource) {

View File

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Error
import androidx.compose.material3.CircularProgressIndicator
@ -55,7 +54,7 @@ fun GlobalSearchResultItem(
Text(text = subtitle)
}
IconButton(onClick = onClick) {
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
}
}
content()

View File

@ -58,7 +58,7 @@ fun GlobalSearchToolbar(
)
if (progress in 1..<total) {
LinearProgressIndicator(
progress = { progress / total.toFloat() },
progress = progress / total.toFloat(),
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(),

View File

@ -75,7 +75,9 @@ fun CategoryScreen(
CategoryContent(
categories = state.categories,
lazyListState = lazyListState,
paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium),
paddingValues = paddingValues +
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
onClickRename = onClickRename,
onClickDelete = onClickDelete,
onMoveUp = onClickMoveUp,

View File

@ -74,7 +74,11 @@ fun CategoryCreateDialog(
onValueChange = { name = it },
label = { Text(text = stringResource(R.string.name)) },
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
R.string.error_category_exists
} else {
R.string.information_required_plain
}
Text(text = stringResource(msgRes))
},
isError = name.isNotEmpty() && nameAlreadyExists,
@ -134,7 +138,11 @@ fun CategoryRenameDialog(
},
label = { Text(text = stringResource(R.string.name)) },
supportingText = {
val msgRes = if (valueHasChanged && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
val msgRes = if (valueHasChanged && nameAlreadyExists) {
R.string.error_category_exists
} else {
R.string.information_required_plain
}
Text(text = stringResource(msgRes))
},
isError = valueHasChanged && nameAlreadyExists,
@ -257,8 +265,12 @@ fun ChangeCategoryDialog(
onClick = {
onDismissRequest()
onConfirm(
selection.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }.map { it.value.id },
selection.filter { it is CheckboxState.State.None || it is CheckboxState.TriState.None }.map { it.value.id },
selection
.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }
.map { it.value.id },
selection
.filter { it is CheckboxState.State.None || it is CheckboxState.TriState.None }
.map { it.value.id },
)
},
) {

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.ArrowDropUp
import androidx.compose.material.icons.outlined.Delete
@ -50,7 +49,7 @@ fun CategoryListItem(
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
Text(
text = category.name,
modifier = Modifier
@ -72,7 +71,10 @@ fun CategoryListItem(
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) {
Icon(imageVector = Icons.Outlined.Edit, contentDescription = stringResource(R.string.action_rename_category))
Icon(
imageVector = Icons.Outlined.Edit,
contentDescription = stringResource(R.string.action_rename_category),
)
}
IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(R.string.action_delete))

View File

@ -10,7 +10,8 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Search
@ -39,12 +40,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.R
@ -367,7 +370,11 @@ fun SearchToolbar(
@Composable
fun UpIcon(navigationIcon: ImageVector? = null) {
val icon = navigationIcon
?: Icons.AutoMirrored.Outlined.ArrowBack
?: if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
Icons.Outlined.ArrowBack
} else {
Icons.Outlined.ArrowForward
}
Icon(
imageVector = icon,
contentDescription = stringResource(R.string.abc_action_bar_up_description),

View File

@ -3,7 +3,8 @@ package eu.kanade.presentation.components
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.ArrowLeft
import androidx.compose.material.icons.outlined.ArrowRight
import androidx.compose.material.icons.outlined.RadioButtonChecked
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
import androidx.compose.material3.DropdownMenuItem
@ -15,8 +16,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import eu.kanade.tachiyomi.R
@ -74,13 +77,14 @@ fun NestedMenuItem(
) {
var nestedExpanded by remember { mutableStateOf(false) }
val closeMenu = { nestedExpanded = false }
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
DropdownMenuItem(
text = text,
onClick = { nestedExpanded = true },
trailingIcon = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
imageVector = if (isLtr) Icons.Outlined.ArrowRight else Icons.Outlined.ArrowLeft,
contentDescription = null,
)
},

View File

@ -1,18 +1,17 @@
package eu.kanade.presentation.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.EmptyScreenAction
import tachiyomi.presentation.core.util.ThemePreviews
@ThemePreviews
@PreviewLightDark
@Composable
private fun NoActionPreview() {
TachiyomiTheme {
@ -24,7 +23,7 @@ private fun NoActionPreview() {
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun WithActionPreview() {
TachiyomiTheme {
@ -39,7 +38,7 @@ private fun WithActionPreview() {
),
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = {},
),
),

View File

@ -14,9 +14,8 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -32,6 +31,7 @@ import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.TabIndicator
import tachiyomi.presentation.core.components.material.TabText
object TabbedDialogPaddings {
val Horizontal = 24.dp
@ -55,27 +55,18 @@ fun TabbedDialog(
Column {
Row {
PrimaryTabRow(
TabRow(
modifier = Modifier.weight(1f),
selectedTabIndex = pagerState.currentPage,
indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },
divider = {},
) {
tabTitles.fastForEachIndexed { i, tab ->
val selected = pagerState.currentPage == i
tabTitles.fastForEachIndexed { index, tab ->
Tab(
selected = selected,
onClick = { scope.launch { pagerState.animateScrollToPage(i) } },
text = {
Text(
text = tab,
color = if (selected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
)
},
selected = pagerState.currentPage == index,
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
text = { TabText(text = tab) },
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
)
}
}

View File

@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@ -67,7 +67,7 @@ fun TabbedScreen(
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
),
) {
PrimaryTabRow(
TabRow(
selectedTabIndex = state.currentPage,
indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) },
) {

View File

@ -14,13 +14,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.CrashLogUtil
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.screens.InfoScreen
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
fun CrashScreen(
@ -60,7 +60,7 @@ fun CrashScreen(
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun CrashScreenPreview() {
TachiyomiTheme {

View File

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar
@ -28,7 +29,6 @@ import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@ -148,7 +148,7 @@ sealed interface HistoryUiModel {
data class Item(val item: HistoryWithRelations) : HistoryUiModel
}
@ThemePreviews
@PreviewLightDark
@Composable
internal fun HistoryScreenPreviews(
@PreviewParameter(HistoryScreenModelStateProvider::class)

View File

@ -11,11 +11,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
fun HistoryDeleteDialog(
@ -87,7 +87,7 @@ fun HistoryDeleteAllDialog(
)
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun HistoryDeleteDialogPreview() {
TachiyomiTheme {

View File

@ -11,6 +11,7 @@ import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -19,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover
@ -28,7 +30,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.toTimestampString
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
private val HistoryItemHeight = 96.dp
@ -91,13 +92,14 @@ fun HistoryItem(
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun HistoryItemPreviews(
@PreviewParameter(HistoryWithRelationsProvider::class)
historyWithRelations: HistoryWithRelations,
) {
TachiyomiTheme {
Surface {
HistoryItem(
history = historyWithRelations,
onClickCover = {},
@ -105,4 +107,5 @@ private fun HistoryItemPreviews(
onClickDelete = {},
)
}
}
}

View File

@ -144,6 +144,13 @@ private fun ColumnScope.SortPage(
val sortingMode = category.sort.type
val sortDescending = !category.sort.isAscending
val trackerSortOption =
if (screenModel.trackers.isEmpty()) {
emptyList()
} else {
listOf(R.string.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
}
listOf(
R.string.action_sort_alpha to LibrarySort.Type.Alphabetical,
R.string.action_sort_total to LibrarySort.Type.TotalChapters,
@ -153,15 +160,23 @@ private fun ColumnScope.SortPage(
R.string.action_sort_latest_chapter to LibrarySort.Type.LatestChapter,
R.string.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
R.string.action_sort_date_added to LibrarySort.Type.DateAdded,
).map { (titleRes, mode) ->
).plus(trackerSortOption).map { (titleRes, mode) ->
SortItem(
label = stringResource(titleRes),
sortDescending = sortDescending.takeIf { sortingMode == mode },
onClick = {
val isTogglingDirection = sortingMode == mode
val direction = when {
isTogglingDirection -> if (sortDescending) LibrarySort.Direction.Ascending else LibrarySort.Direction.Descending
else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending
isTogglingDirection -> if (sortDescending) {
LibrarySort.Direction.Ascending
} else {
LibrarySort.Direction.Descending
}
else -> if (sortDescending) {
LibrarySort.Direction.Descending
} else {
LibrarySort.Direction.Ascending
}
}
screenModel.setSort(category, mode, direction)
},

View File

@ -5,9 +5,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
internal fun DownloadsBadge(count: Long) {
@ -47,7 +47,7 @@ internal fun LanguageBadge(
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun BadgePreview() {
TachiyomiTheme {

View File

@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
@ -21,7 +21,7 @@ internal fun LibraryTabs(
onTabItemClick: (Int) -> Unit,
) {
Column {
PrimaryScrollableTabRow(
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp,
indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },

View File

@ -1,13 +1,21 @@
package eu.kanade.presentation.manga
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PeopleAlt
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -15,6 +23,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -29,6 +38,7 @@ import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.theme.active
@Composable
fun ChapterSettingsDialog(
@ -37,9 +47,12 @@ fun ChapterSettingsDialog(
onDownloadFilterChanged: (TriState) -> Unit,
onUnreadFilterChanged: (TriState) -> Unit,
onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
onSortModeChanged: (Long) -> Unit,
onDisplayModeChanged: (Long) -> Unit,
onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
onResetToDefault: () -> Unit,
) {
var showSetAsDefaultDialog by rememberSaveable { mutableStateOf(false) }
if (showSetAsDefaultDialog) {
@ -64,6 +77,13 @@ fun ChapterSettingsDialog(
closeMenu()
},
)
DropdownMenuItem(
text = { Text(stringResource(R.string.action_reset)) },
onClick = {
onResetToDefault()
closeMenu()
},
)
},
) { page ->
Column(
@ -75,11 +95,14 @@ fun ChapterSettingsDialog(
0 -> {
FilterPage(
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { manga?.forceDownloaded() == true },
onDownloadFilterChanged = onDownloadFilterChanged
.takeUnless { manga?.forceDownloaded() == true },
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
onUnreadFilterChanged = onUnreadFilterChanged,
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
onBookmarkedFilterChanged = onBookmarkedFilterChanged,
scanlatorFilterActive = scanlatorFilterActive,
onScanlatorFilterClicked = onScanlatorFilterClicked,
)
}
1 -> {
@ -108,6 +131,8 @@ private fun ColumnScope.FilterPage(
onUnreadFilterChanged: (TriState) -> Unit,
bookmarkedFilter: TriState,
onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
) {
TriStateItem(
label = stringResource(R.string.label_downloaded),
@ -124,6 +149,39 @@ private fun ColumnScope.FilterPage(
state = bookmarkedFilter,
onClick = onBookmarkedFilterChanged,
)
ScanlatorFilterItem(
active = scanlatorFilterActive,
onClick = onScanlatorFilterClicked,
)
}
@Composable
fun ScanlatorFilterItem(
active: Boolean,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.clickable(onClick = onClick)
.fillMaxWidth()
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
Icon(
imageVector = Icons.Outlined.PeopleAlt,
contentDescription = null,
tint = if (active) {
MaterialTheme.colorScheme.active
} else {
LocalContentColor.current
},
)
Text(
text = stringResource(R.string.scanlator),
style = MaterialTheme.typography.bodyMedium,
)
}
}
@Composable
@ -136,6 +194,7 @@ private fun ColumnScope.SortPage(
R.string.sort_by_source to Manga.CHAPTER_SORTING_SOURCE,
R.string.sort_by_number to Manga.CHAPTER_SORTING_NUMBER,
R.string.sort_by_upload_date to Manga.CHAPTER_SORTING_UPLOAD_DATE,
R.string.action_sort_alpha to Manga.CHAPTER_SORTING_ALPHABET,
).map { (titleRes, mode) ->
SortItem(
label = stringResource(titleRes),

View File

@ -48,7 +48,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import eu.kanade.domain.manga.model.chaptersFiltered
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
@ -57,11 +56,12 @@ import eu.kanade.presentation.manga.components.MangaBottomActionMenu
import eu.kanade.presentation.manga.components.MangaChapterListItem
import eu.kanade.presentation.manga.components.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaToolbar
import eu.kanade.presentation.manga.components.MissingChapterCountListItem
import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R
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.ChapterList
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard
@ -92,7 +92,7 @@ fun MangaScreen(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
@ -123,10 +123,10 @@ fun MangaScreen(
onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For chapter swipe
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
@ -225,7 +225,7 @@ private fun MangaScreenSmallImpl(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
@ -257,16 +257,17 @@ private fun MangaScreenSmallImpl(
onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For chapter swipe
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
val chapterListState = rememberLazyListState()
val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember {
derivedStateOf {
@ -306,7 +307,7 @@ private fun MangaScreenSmallImpl(
title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.manga.chaptersFiltered(),
hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterClicked,
onClickShare = onShareClicked,
@ -447,7 +448,8 @@ private fun MangaScreenSmallImpl(
sharedChapterItems(
manga = state.manga,
chapters = chapters,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
@ -474,7 +476,7 @@ fun MangaScreenLargeImpl(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
@ -506,10 +508,10 @@ fun MangaScreenLargeImpl(
onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For swipe actions
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
@ -517,6 +519,7 @@ fun MangaScreenLargeImpl(
val density = LocalDensity.current
val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember {
derivedStateOf {
@ -557,7 +560,7 @@ fun MangaScreenLargeImpl(
title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.manga.chaptersFiltered(),
hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked,
@ -604,7 +607,9 @@ fun MangaScreenLargeImpl(
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
Text(
text = stringResource(if (isReading) R.string.action_resume else R.string.action_start),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
@ -688,7 +693,8 @@ fun MangaScreenLargeImpl(
sharedChapterItems(
manga = state.manga,
chapters = chapters,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
@ -708,12 +714,12 @@ fun MangaScreenLargeImpl(
@Composable
private fun SharedMangaBottomActionMenu(
selected: List<ChapterItem>,
selected: List<ChapterList.Item>,
modifier: Modifier = Modifier,
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onMultiDeleteClicked: (List<Chapter>) -> Unit,
fillFraction: Float,
) {
@ -750,34 +756,45 @@ private fun SharedMangaBottomActionMenu(
private fun LazyListScope.sharedChapterItems(
manga: Manga,
chapters: List<ChapterItem>,
chapters: List<ChapterList>,
isAnyChapterSelected: Boolean,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
) {
items(
items = chapters,
key = { "chapter-${it.chapter.id}" },
key = { item ->
when (item) {
is ChapterList.MissingCount -> "missing-count-${item.id}"
is ChapterList.Item -> "chapter-${item.id}"
}
},
contentType = { MangaScreenItem.CHAPTER },
) { chapterItem ->
) { item ->
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
when (item) {
is ChapterList.MissingCount -> {
MissingChapterCountListItem(count = item.count)
}
is ChapterList.Item -> {
MangaChapterListItem(
title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {
stringResource(
R.string.display_mode_chapter,
formatChapterNumber(chapterItem.chapter.chapterNumber),
formatChapterNumber(item.chapter.chapterNumber),
)
} else {
chapterItem.chapter.name
item.chapter.name
},
date = chapterItem.chapter.dateUpload
date = item.chapter.dateUpload
.takeIf { it > 0L }
?.let {
Date(it).toRelativeString(
@ -786,56 +803,58 @@ private fun LazyListScope.sharedChapterItems(
dateFormat,
)
},
readProgress = chapterItem.chapter.lastPageRead
.takeIf { !chapterItem.chapter.read && it > 0L }
readProgress = item.chapter.lastPageRead
.takeIf { !item.chapter.read && it > 0L }
?.let {
stringResource(
R.string.chapter_progress,
it + 1,
)
},
scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() },
read = chapterItem.chapter.read,
bookmark = chapterItem.chapter.bookmark,
selected = chapterItem.selected,
downloadIndicatorEnabled = chapters.fastAll { !it.selected },
downloadStateProvider = { chapterItem.downloadState },
downloadProgressProvider = { chapterItem.downloadProgress },
scanlator = item.chapter.scanlator.takeIf { !it.isNullOrBlank() },
read = item.chapter.read,
bookmark = item.chapter.bookmark,
selected = item.selected,
downloadIndicatorEnabled = !isAnyChapterSelected,
downloadStateProvider = { item.downloadState },
downloadProgressProvider = { item.downloadProgress },
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onLongClick = {
onChapterSelected(chapterItem, !chapterItem.selected, true, true)
onChapterSelected(item, !item.selected, true, true)
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = {
onChapterItemClick(
chapterItem = chapterItem,
chapters = chapters,
onToggleSelection = { onChapterSelected(chapterItem, !chapterItem.selected, true, false) },
chapterItem = item,
isAnyChapterSelected = isAnyChapterSelected,
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
onChapterClicked = onChapterClicked,
)
},
onDownloadClick = if (onDownloadChapter != null) {
{ onDownloadChapter(listOf(chapterItem), it) }
{ onDownloadChapter(listOf(item), it) }
} else {
null
},
onChapterSwipe = {
onChapterSwipe(chapterItem, it)
onChapterSwipe(item, it)
},
)
}
}
}
}
private fun onChapterItemClick(
chapterItem: ChapterItem,
chapters: List<ChapterItem>,
chapterItem: ChapterList.Item,
isAnyChapterSelected: Boolean,
onToggleSelection: (Boolean) -> Unit,
onChapterClicked: (Chapter) -> Unit,
) {
when {
chapterItem.selected -> onToggleSelection(false)
chapters.fastAny { it.selected } -> onToggleSelection(true)
isAnyChapterSelected -> onToggleSelection(true)
else -> onChapterClicked(chapterItem.chapter)
}
}

View File

@ -148,7 +148,7 @@ private fun DownloadingIndicator(
MaterialTheme.colorScheme.background
}
CircularProgressIndicator(
progress = { animatedProgress },
progress = animatedProgress,
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorSize / 2,

View File

@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.BookmarkAdd
import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete
@ -259,7 +258,7 @@ fun LibraryBottomActionMenu(
) {
Button(
title = stringResource(R.string.action_move_category),
icon = Icons.AutoMirrored.Outlined.Label,
icon = Icons.Outlined.Label,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked,

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.manga.components
import android.content.Context
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.graphics.res.animatedVectorResource
@ -24,8 +23,10 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Brush
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.HourglassEmpty
import androidx.compose.material.icons.filled.PersonOutline
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.AttachMoney
import androidx.compose.material.icons.outlined.Block
@ -41,6 +42,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SuggestionChip
@ -132,7 +134,6 @@ fun MangaInfoBox(
coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick,
title = title,
context = LocalContext.current,
doSearch = doSearch,
author = author,
artist = artist,
@ -146,7 +147,6 @@ fun MangaInfoBox(
coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick,
title = title,
context = LocalContext.current,
doSearch = doSearch,
author = author,
artist = artist,
@ -189,7 +189,11 @@ fun MangaActionRow(
)
if (onEditIntervalClicked != null && fetchInterval != null) {
MangaActionButton(
title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
title = pluralStringResource(
id = R.plurals.day,
count = fetchInterval.absoluteValue,
fetchInterval.absoluteValue,
),
icon = Icons.Default.HourglassEmpty,
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onEditIntervalClicked,
@ -321,7 +325,6 @@ private fun MangaAndSourceTitlesLarge(
coverDataProvider: () -> Manga,
onCoverClick: () -> Unit,
title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
artist: String?,
@ -342,102 +345,16 @@ private fun MangaAndSourceTitlesLarge(
onClick = onCoverClick,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = title.ifBlank { stringResource(R.string.unknown_title) },
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.clickableNoIndication(
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
onClick = { if (title.isNotBlank()) doSearch(title, true) },
),
MangaContentInfo(
title = title,
doSearch = doSearch,
author = author,
artist = artist,
status = status,
sourceName = sourceName,
isStubSource = isStubSource,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = author?.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown_author),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.secondaryItemAlpha()
.padding(top = 2.dp)
.clickableNoIndication(
onLongClick = {
if (!author.isNullOrBlank()) {
context.copyToClipboard(
author,
author,
)
}
},
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
),
textAlign = TextAlign.Center,
)
if (!artist.isNullOrBlank() && author != artist) {
Text(
text = artist,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.secondaryItemAlpha()
.padding(top = 2.dp)
.clickableNoIndication(
onLongClick = { context.copyToClipboard(artist, artist) },
onClick = { doSearch(artist, true) },
),
textAlign = TextAlign.Center,
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.secondaryItemAlpha(),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = when (status) {
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
else -> Icons.Outlined.Block
},
contentDescription = null,
modifier = Modifier
.padding(end = 4.dp)
.size(16.dp),
)
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
Text(
text = when (status) {
SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished)
SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
else -> stringResource(R.string.unknown)
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
DotSeparatorText()
if (isStubSource) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
modifier = Modifier
.padding(end = 4.dp)
.size(16.dp),
tint = MaterialTheme.colorScheme.error,
)
}
Text(
text = sourceName,
modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) },
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
}
@ -447,7 +364,6 @@ private fun MangaAndSourceTitlesSmall(
coverDataProvider: () -> Manga,
onCoverClick: () -> Unit,
title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
artist: String?,
@ -459,6 +375,7 @@ private fun MangaAndSourceTitlesSmall(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MangaCover.Book(
@ -469,7 +386,34 @@ private fun MangaAndSourceTitlesSmall(
contentDescription = stringResource(R.string.manga_cover),
onClick = onCoverClick,
)
Column(modifier = Modifier.padding(start = 16.dp)) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
MangaContentInfo(
title = title,
doSearch = doSearch,
author = author,
artist = artist,
status = status,
sourceName = sourceName,
isStubSource = isStubSource,
)
}
}
}
@Composable
private fun MangaContentInfo(
title: String,
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
artist: String?,
status: Long,
sourceName: String,
isStubSource: Boolean,
) {
val context = LocalContext.current
Text(
text = title.ifBlank { stringResource(R.string.unknown_title) },
style = MaterialTheme.typography.titleLarge,
@ -484,15 +428,26 @@ private fun MangaAndSourceTitlesSmall(
},
onClick = { if (title.isNotBlank()) doSearch(title, true) },
),
textAlign = textAlign,
)
Spacer(modifier = Modifier.height(2.dp))
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Filled.PersonOutline,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
Text(
text = author?.takeIf { it.isNotBlank() }
?: stringResource(R.string.unknown_author),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.secondaryItemAlpha()
.padding(top = 2.dp)
.clickableNoIndication(
onLongClick = {
if (!author.isNullOrBlank()) {
@ -504,21 +459,36 @@ private fun MangaAndSourceTitlesSmall(
},
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
),
textAlign = textAlign,
)
}
if (!artist.isNullOrBlank() && author != artist) {
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Filled.Brush,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
Text(
text = artist,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.secondaryItemAlpha()
.padding(top = 2.dp)
.clickableNoIndication(
onLongClick = { context.copyToClipboard(artist, artist) },
onClick = { doSearch(artist, true) },
),
textAlign = textAlign,
)
}
Spacer(modifier = Modifier.height(4.dp))
}
Spacer(modifier = Modifier.height(2.dp))
Row(
modifier = Modifier.secondaryItemAlpha(),
verticalAlignment = Alignment.CenterVertically,
@ -576,8 +546,6 @@ private fun MangaAndSourceTitlesSmall(
)
}
}
}
}
}
@Composable
@ -623,7 +591,9 @@ private fun MangaSummary(
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon(
painter = rememberAnimatedVectorPainter(image, !expanded),
contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
contentDescription = stringResource(
if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand,
),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
)

View File

@ -0,0 +1,52 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
fun MissingChapterCountListItem(
count: Int,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
)
.secondaryItemAlpha(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
HorizontalDivider(modifier = Modifier.weight(1f))
Text(
text = pluralStringResource(id = R.plurals.missing_chapters, count = count, count),
style = MaterialTheme.typography.labelMedium,
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
}
@PreviewLightDark
@Composable
private fun Preview() {
TachiyomiTheme {
Surface {
MissingChapterCountListItem(count = 42)
}
}
}

View File

@ -0,0 +1,134 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
import androidx.compose.material.icons.rounded.DisabledByDefault
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@Composable
fun ScanlatorFilterDialog(
availableScanlators: Set<String>,
excludedScanlators: Set<String>,
onDismissRequest: () -> Unit,
onConfirm: (Set<String>) -> Unit,
) {
val sortedAvailableScanlators = remember(availableScanlators) {
availableScanlators.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it })
}
val mutableExcludedScanlators = remember(excludedScanlators) { excludedScanlators.toMutableStateList() }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.exclude_scanlators)) },
text = textFunc@{
if (sortedAvailableScanlators.isEmpty()) {
Text(text = stringResource(R.string.no_scanlators_found))
return@textFunc
}
Box {
val state = rememberLazyListState()
LazyColumn(state = state) {
sortedAvailableScanlators.forEach { scanlator ->
item {
val isExcluded = mutableExcludedScanlators.contains(scanlator)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
if (isExcluded) {
mutableExcludedScanlators.remove(scanlator)
} else {
mutableExcludedScanlators.add(scanlator)
}
}
.minimumInteractiveComponentSize()
.clip(MaterialTheme.shapes.small)
.fillMaxWidth()
.padding(horizontal = MaterialTheme.padding.small),
) {
Icon(
imageVector = if (isExcluded) {
Icons.Rounded.DisabledByDefault
} else {
Icons.Rounded.CheckBoxOutlineBlank
},
tint = if (isExcluded) {
MaterialTheme.colorScheme.primary
} else {
LocalContentColor.current
},
contentDescription = null,
)
Text(
text = scanlator,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(start = 24.dp),
)
}
}
}
}
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
}
},
properties = DialogProperties(
usePlatformDefaultWidth = true,
),
confirmButton = {
if (sortedAvailableScanlators.isEmpty()) {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
} else {
FlowRow {
TextButton(onClick = mutableExcludedScanlators::clear) {
Text(text = stringResource(R.string.action_reset))
}
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
TextButton(
onClick = {
onConfirm(mutableExcludedScanlators.toSet())
onDismissRequest()
},
) {
Text(text = stringResource(R.string.action_ok))
}
}
}
},
)
}

View File

@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.HelpOutline
@ -18,7 +16,7 @@ import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SettingsBackupRestore
import androidx.compose.material.icons.outlined.Storage
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -47,7 +45,7 @@ fun MoreScreen(
onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit,
onClickStats: () -> Unit,
onClickBackupAndRestore: () -> Unit,
onClickDataAndStorage: () -> Unit,
onClickSettings: () -> Unit,
onClickAbout: () -> Unit,
) {
@ -64,7 +62,9 @@ fun MoreScreen(
WarningBanner(
textRes = R.string.fdroid_warning,
modifier = Modifier.clickable {
uriHandler.openUri("https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds")
uriHandler.openUri(
"https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds",
)
},
)
}
@ -130,7 +130,7 @@ fun MoreScreen(
item {
TextPreferenceWidget(
title = stringResource(R.string.categories),
icon = Icons.AutoMirrored.Outlined.Label,
icon = Icons.Outlined.Label,
onPreferenceClick = onClickCategories,
)
}
@ -144,8 +144,8 @@ fun MoreScreen(
item {
TextPreferenceWidget(
title = stringResource(R.string.label_backup_and_sync),
icon = Icons.Outlined.SettingsBackupRestore,
onPreferenceClick = onClickBackupAndRestore,
icon = Icons.Outlined.Storage,
onPreferenceClick = onClickDataAndStorage,
)
}
@ -168,7 +168,7 @@ fun MoreScreen(
item {
TextPreferenceWidget(
title = stringResource(R.string.label_help),
icon = Icons.AutoMirrored.Outlined.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
)
}

View File

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material3.Icon
@ -16,6 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.Material3RichText
@ -24,7 +24,6 @@ import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.screens.InfoScreen
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
fun NewUpdateScreen(
@ -61,13 +60,13 @@ fun NewUpdateScreen(
) {
Text(text = stringResource(R.string.update_check_open))
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
Icon(imageVector = Icons.Default.OpenInNew, contentDescription = null)
}
}
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun NewUpdateScreenPreview() {
TachiyomiTheme {

View File

@ -31,7 +31,8 @@ fun getCategoriesLabel(
val includedItemsText = when {
// Some selected, but not all
includedCategories.isNotEmpty() && includedCategories.size != allCategories.size -> includedCategories.joinToString { it.visualName(context) }
includedCategories.isNotEmpty() && includedCategories.size != allCategories.size ->
includedCategories.joinToString { it.visualName(context) }
// All explicitly selected
includedCategories.size == allCategories.size -> stringResource(R.string.all)
allExcluded -> stringResource(R.string.none)

View File

@ -14,7 +14,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -31,7 +30,6 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.advanced.ClearDatabaseScreen
import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.network.NetworkHelper
@ -61,8 +59,7 @@ import okhttp3.Headers
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -183,40 +180,12 @@ object SettingsAdvancedScreen : SearchableSettings {
@Composable
private fun getDataGroup(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val chapterCache = remember { Injekt.get<ChapterCache>() }
var readableSizeSema by remember { mutableIntStateOf(0) }
val readableSize = remember(readableSizeSema) { chapterCache.readableSize }
return Preference.PreferenceGroup(
title = stringResource(R.string.label_data),
preferenceItems = listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_clear_chapter_cache),
subtitle = stringResource(R.string.used_cache, readableSize),
onClick = {
scope.launchNonCancellable {
try {
val deletedFiles = chapterCache.clear()
withUIContext {
context.toast(context.getString(R.string.cache_deleted, deletedFiles))
readableSizeSema++
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
withUIContext { context.toast(R.string.cache_delete_error) }
}
}
},
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoClearChapterCache(),
title = stringResource(R.string.pref_auto_clear_chapter_cache),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_invalidate_download_cache),
subtitle = stringResource(R.string.pref_invalidate_download_cache_summary),
@ -340,7 +309,7 @@ object SettingsAdvancedScreen : SearchableSettings {
subtitle = stringResource(R.string.pref_reset_viewer_flags_summary),
onClick = {
scope.launchNonCancellable {
val success = Injekt.get<MangaRepository>().resetViewerFlags()
val success = Injekt.get<ResetViewerFlags>().await()
withUIContext {
val message = if (success) {
R.string.pref_reset_viewer_flags_success

View File

@ -120,7 +120,9 @@ object SettingsAppearanceScreen : SearchableSettings {
uiPreferences: UiPreferences,
): Preference.PreferenceGroup {
val langs = remember { getLangs(context) }
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
var currentLanguage by remember {
mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "")
}
val now = remember { Date().time }
val dateFormat by uiPreferences.dateFormat().collectAsState()

View File

@ -1,627 +0,0 @@
package eu.kanade.presentation.more.settings.screen
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.text.format.DateUtils
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.domain.sync.SyncPreferences
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object SettingsBackupAndSyncScreen : SearchableSettings {
@ReadOnlyComposable
@Composable
@StringRes
override fun getTitleRes() = R.string.label_backup_and_sync
@Composable
override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
PermissionRequestHelper.requestStoragePermission()
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
val syncService by syncPreferences.syncService().collectAsState()
return listOf(
getManualBackupGroup(),
getAutomaticBackupGroup(backupPreferences = backupPreferences),
) + listOf(
Preference.PreferenceGroup(
title = stringResource(R.string.label_sync),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = syncPreferences.syncService(),
title = stringResource(R.string.pref_sync_service),
entries = mapOf(
SyncManager.SyncService.NONE.value to stringResource(R.string.off),
SyncManager.SyncService.SYNCYOMI.value to stringResource(R.string.syncyomi),
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(R.string.google_drive),
),
onValueChanged = { true },
),
),
),
) + getSyncServicePreferences(syncPreferences, syncService)
}
@Composable
private fun getManualBackupGroup(): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_backup_manual_category),
preferenceItems = listOf(
getCreateBackupPref(),
getRestoreBackupPref(),
),
)
}
@Composable
private fun getAutomaticBackupGroup(
backupPreferences: BackupPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val backupIntervalPref = backupPreferences.backupInterval()
val backupInterval by backupIntervalPref.collectAsState()
val backupDirPref = backupPreferences.backupsDirectory()
val backupDir by backupDirPref.collectAsState()
val pickBackupLocation = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { uri ->
if (uri != null) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromUri(context, uri)
backupDirPref.set(file.uri.toString())
}
}
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_backup_service_category),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = backupIntervalPref,
title = stringResource(R.string.pref_backup_interval),
entries = mapOf(
0 to stringResource(R.string.off),
6 to stringResource(R.string.update_6hour),
12 to stringResource(R.string.update_12hour),
24 to stringResource(R.string.update_24hour),
48 to stringResource(R.string.update_48hour),
168 to stringResource(R.string.update_weekly),
),
onValueChanged = {
BackupCreateJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_backup_directory),
enabled = backupInterval != 0,
subtitle = remember(backupDir) {
(UniFile.fromUri(context, backupDir.toUri())?.filePath)?.let {
"$it/automatic"
}
} ?: stringResource(R.string.invalid_location, backupDir),
onClick = {
try {
pickBackupLocation.launch(null)
} catch (e: ActivityNotFoundException) {
context.toast(R.string.file_picker_error)
}
},
),
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.numberOfBackups(),
enabled = backupInterval != 0,
title = stringResource(R.string.pref_backup_slots),
entries = listOf(2, 3, 4, 5).associateWith { it.toString() },
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.backup_info)),
),
)
}
@Composable
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
val scope = rememberCoroutineScope()
val context = LocalContext.current
var flag by rememberSaveable { mutableIntStateOf(0) }
val chooseBackupDir = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/*"),
) {
if (it != null) {
context.contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
)
BackupCreateJob.startNow(context, it, flag)
}
flag = 0
}
var showCreateDialog by rememberSaveable { mutableStateOf(false) }
if (showCreateDialog) {
CreateBackupDialog(
onConfirm = {
showCreateDialog = false
flag = it
try {
chooseBackupDir.launch(Backup.getFilename())
} catch (e: ActivityNotFoundException) {
flag = 0
context.toast(R.string.file_picker_error)
}
},
onDismissRequest = { showCreateDialog = false },
)
}
return Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_create_backup),
subtitle = stringResource(R.string.pref_create_backup_summ),
onClick = {
scope.launch {
if (!BackupCreateJob.isManualJobRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
showCreateDialog = true
} else {
context.toast(R.string.backup_in_progress)
}
}
},
)
}
@Composable
private fun CreateBackupDialog(
onConfirm: (flag: Int) -> Unit,
onDismissRequest: () -> Unit,
) {
val choices = remember {
mapOf(
BackupConst.BACKUP_CATEGORY to R.string.categories,
BackupConst.BACKUP_CHAPTER to R.string.chapters,
BackupConst.BACKUP_TRACK to R.string.track,
BackupConst.BACKUP_HISTORY to R.string.history,
BackupConst.BACKUP_APP_PREFS to R.string.app_settings,
BackupConst.BACKUP_SOURCE_PREFS to R.string.source_settings,
)
}
val flags = remember { choices.keys.toMutableStateList() }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.backup_choice)) },
text = {
Box {
val state = rememberLazyListState()
ScrollbarLazyColumn(state = state) {
item {
LabeledCheckbox(
label = stringResource(R.string.manga),
checked = true,
onCheckedChange = {},
)
}
choices.forEach { (k, v) ->
item {
val isSelected = flags.contains(k)
LabeledCheckbox(
label = stringResource(v),
checked = isSelected,
onCheckedChange = {
if (it) {
flags.add(k)
} else {
flags.remove(k)
}
},
)
}
}
}
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
TextButton(
onClick = {
val flag = flags.fold(initial = 0, operation = { a, b -> a or b })
onConfirm(flag)
},
) {
Text(text = stringResource(R.string.action_ok))
}
},
)
}
@Composable
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
val context = LocalContext.current
var error by remember { mutableStateOf<Any?>(null) }
if (error != null) {
val onDismissRequest = { error = null }
when (val err = error) {
is InvalidRestore -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.invalid_backup_file)) },
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
dismissButton = {
TextButton(
onClick = {
context.copyToClipboard(err.message, err.message)
onDismissRequest()
},
) {
Text(text = stringResource(R.string.action_copy_to_clipboard))
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_ok))
}
},
)
}
is MissingRestoreComponents -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_restore_backup)) },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
val msg = buildString {
append(stringResource(R.string.backup_restore_content_full))
if (err.sources.isNotEmpty()) {
append("\n\n").append(stringResource(R.string.backup_restore_missing_sources))
err.sources.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
if (err.trackers.isNotEmpty()) {
append("\n\n").append(stringResource(R.string.backup_restore_missing_trackers))
err.trackers.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
}
Text(text = msg)
}
},
confirmButton = {
TextButton(
onClick = {
BackupRestoreJob.start(context, err.uri)
onDismissRequest()
},
) {
Text(text = stringResource(R.string.action_restore))
}
},
)
}
else -> error = null // Unknown
}
}
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
return Intent.createChooser(intent, context.getString(R.string.file_select_backup))
}
},
) {
if (it == null) {
error = InvalidRestore(message = context.getString(R.string.file_null_uri_error))
return@rememberLauncherForActivityResult
}
val results = try {
BackupFileValidator().validate(context, it)
} catch (e: Exception) {
error = InvalidRestore(it, e.message.toString())
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(context, it)
return@rememberLauncherForActivityResult
}
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
}
return Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_restore_backup),
subtitle = stringResource(R.string.pref_restore_backup_summ),
onClick = {
if (!BackupRestoreJob.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
// no need to catch because it's wrapped with a chooser
chooseBackup.launch("*/*")
} else {
context.toast(R.string.restore_in_progress)
}
},
)
}
@Composable
private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
val syncServiceType = SyncManager.SyncService.fromInt(syncService)
return when (syncServiceType) {
SyncManager.SyncService.NONE -> emptyList()
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
} +
if (syncServiceType == SyncManager.SyncService.NONE) {
emptyList()
} else {
listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
}
}
@Composable
private fun getGoogleDrivePreferences(): List<Preference> {
val context = LocalContext.current
val googleDriveSync = Injekt.get<GoogleDriveService>()
return listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_google_drive_sign_in),
onClick = {
val intent = googleDriveSync.getSignInIntent()
context.startActivity(intent)
},
),
getGoogleDrivePurge(),
)
}
@Composable
private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
val scope = rememberCoroutineScope()
val showPurgeDialog = remember { mutableStateOf(false) }
val context = LocalContext.current
val googleDriveSync = remember { GoogleDriveSyncService(context) }
if (showPurgeDialog.value) {
PurgeConfirmationDialog(
onConfirm = {
showPurgeDialog.value = false
scope.launch {
val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
when (result) {
GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(R.string.google_drive_not_signed_in)
GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(R.string.google_drive_sync_data_not_found)
GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(R.string.google_drive_sync_data_purged)
}
}
},
onDismissRequest = { showPurgeDialog.value = false },
)
}
return Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_google_drive_purge_sync_data),
onClick = { showPurgeDialog.value = true },
)
}
@Composable
fun PurgeConfirmationDialog(
onConfirm: () -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_purge_confirmation_title)) },
text = { Text(text = stringResource(R.string.pref_purge_confirmation_message)) },
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
@Composable
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
return listOf(
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_device_name),
subtitle = stringResource(R.string.pref_sync_device_name_summ),
pref = syncPreferences.deviceName(),
),
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_host),
subtitle = stringResource(R.string.pref_sync_host_summ),
pref = syncPreferences.syncHost(),
),
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_api_key),
subtitle = stringResource(R.string.pref_sync_api_key_summ),
pref = syncPreferences.syncAPIKey(),
),
)
}
@Composable
private fun getSyncNowPref(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
if (showDialog) {
SyncConfirmationDialog(
onConfirm = {
showDialog = false
scope.launch {
if (!SyncDataJob.isAnyJobRunning(context)) {
SyncDataJob.startNow(context)
} else {
context.toast(R.string.sync_in_progress)
}
}
},
onDismissRequest = { showDialog = false },
)
}
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_sync_now_group_title),
preferenceItems = listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_sync_now),
subtitle = stringResource(R.string.pref_sync_now_subtitle),
onClick = {
showDialog = true
},
),
),
)
}
@Composable
private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
val context = LocalContext.current
val syncIntervalPref = syncPreferences.syncInterval()
val lastSync by syncPreferences.syncLastSync().collectAsState()
val formattedLastSync = DateUtils.getRelativeTimeSpanString(lastSync.toEpochMilli(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS)
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_sync_service_category),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = syncIntervalPref,
title = stringResource(R.string.pref_sync_interval),
entries = mapOf(
0 to stringResource(R.string.off),
30 to stringResource(R.string.update_30min),
60 to stringResource(R.string.update_1hour),
180 to stringResource(R.string.update_3hour),
360 to stringResource(R.string.update_6hour),
720 to stringResource(R.string.update_12hour),
1440 to stringResource(R.string.update_24hour),
2880 to stringResource(R.string.update_48hour),
10080 to stringResource(R.string.update_weekly),
),
onValueChanged = {
SyncDataJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.last_synchronization, formattedLastSync)),
),
)
}
@Composable
fun SyncConfirmationDialog(
onConfirm: () -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_sync_confirmation_title)) },
text = { Text(text = stringResource(R.string.pref_sync_confirmation_message)) },
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
}
private data class MissingRestoreComponents(
val uri: Uri,
val sources: List<String>,
val trackers: List<String>,
)
private data class InvalidRestore(
val uri: Uri? = null,
val message: String,
)

View File

@ -0,0 +1,549 @@
package eu.kanade.presentation.more.settings.screen
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.text.format.DateUtils
import android.text.format.Formatter
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.sync.SyncPreferences
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object SettingsDataScreen : SearchableSettings {
@ReadOnlyComposable
@Composable
@StringRes
override fun getTitleRes() = R.string.label_backup_and_sync
@Composable
override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
PermissionRequestHelper.requestStoragePermission()
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
val syncService by syncPreferences.syncService().collectAsState()
return listOf(
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup()) + listOf(
Preference.PreferenceGroup(
title = stringResource(R.string.label_sync),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = syncPreferences.syncService(),
title = stringResource(R.string.pref_sync_service),
entries = mapOf(
SyncManager.SyncService.NONE.value to stringResource(R.string.off),
SyncManager.SyncService.SYNCYOMI.value to stringResource(R.string.syncyomi),
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(R.string.google_drive),
),
onValueChanged = { true },
),
),
),
) + getSyncServicePreferences(syncPreferences, syncService)
}
@Composable
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val context = LocalContext.current
val backupIntervalPref = backupPreferences.backupInterval()
val backupInterval by backupIntervalPref.collectAsState()
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
return Preference.PreferenceGroup(
title = stringResource(R.string.label_backup),
preferenceItems = listOf(
// Manual actions
getCreateBackupPref(),
getRestoreBackupPref(),
// Automatic backups
Preference.PreferenceItem.ListPreference(
pref = backupIntervalPref,
title = stringResource(R.string.pref_backup_interval),
entries = mapOf(
0 to stringResource(R.string.off),
6 to stringResource(R.string.update_6hour),
12 to stringResource(R.string.update_12hour),
24 to stringResource(R.string.update_24hour),
48 to stringResource(R.string.update_48hour),
168 to stringResource(R.string.update_weekly),
),
onValueChanged = {
BackupCreateJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.numberOfBackups(),
enabled = backupInterval != 0,
title = stringResource(R.string.pref_backup_slots),
entries = listOf(2, 3, 4, 5).associateWith { it.toString() },
),
Preference.PreferenceItem.InfoPreference(
stringResource(R.string.backup_info) + "\n\n" +
stringResource(R.string.last_auto_backup_info, relativeTimeSpanString(lastAutoBackup)),
),
),
)
}
@Composable
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
val navigator = LocalNavigator.currentOrThrow
return Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_create_backup),
subtitle = stringResource(R.string.pref_create_backup_summ),
onClick = { navigator.push(CreateBackupScreen()) },
)
}
@Composable
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
val context = LocalContext.current
var error by remember { mutableStateOf<Any?>(null) }
if (error != null) {
val onDismissRequest = { error = null }
when (val err = error) {
is InvalidRestore -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.invalid_backup_file)) },
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
dismissButton = {
TextButton(
onClick = {
context.copyToClipboard(err.message, err.message)
onDismissRequest()
},
) {
Text(text = stringResource(R.string.action_copy_to_clipboard))
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_ok))
}
},
)
}
is MissingRestoreComponents -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_restore_backup)) },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
val msg = buildString {
append(stringResource(R.string.backup_restore_content_full))
if (err.sources.isNotEmpty()) {
append("\n\n").append(stringResource(R.string.backup_restore_missing_sources))
err.sources.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
if (err.trackers.isNotEmpty()) {
append("\n\n").append(stringResource(R.string.backup_restore_missing_trackers))
err.trackers.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
}
Text(text = msg)
}
},
confirmButton = {
TextButton(
onClick = {
BackupRestoreJob.start(context, err.uri)
onDismissRequest()
},
) {
Text(text = stringResource(R.string.action_restore))
}
},
)
}
else -> error = null // Unknown
}
}
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
return Intent.createChooser(intent, context.getString(R.string.file_select_backup))
}
},
) {
if (it == null) {
context.toast(R.string.file_null_uri_error)
return@rememberLauncherForActivityResult
}
val results = try {
BackupFileValidator().validate(context, it)
} catch (e: Exception) {
error = InvalidRestore(it, e.message.toString())
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(context, it)
return@rememberLauncherForActivityResult
}
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
}
return Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_restore_backup),
subtitle = stringResource(R.string.pref_restore_backup_summ),
onClick = {
if (!BackupRestoreJob.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
// no need to catch because it's wrapped with a chooser
chooseBackup.launch("*/*")
} else {
context.toast(R.string.restore_in_progress)
}
},
)
}
@Composable
private fun getDataGroup(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val chapterCache = remember { Injekt.get<ChapterCache>() }
var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
return Preference.PreferenceGroup(
title = stringResource(R.string.label_data),
preferenceItems = listOf(
getStorageInfoPref(cacheReadableSize),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_clear_chapter_cache),
subtitle = stringResource(R.string.used_cache, cacheReadableSize),
onClick = {
scope.launchNonCancellable {
try {
val deletedFiles = chapterCache.clear()
withUIContext {
context.toast(context.getString(R.string.cache_deleted, deletedFiles))
cacheReadableSizeSema++
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
withUIContext { context.toast(R.string.cache_delete_error) }
}
}
},
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoClearChapterCache(),
title = stringResource(R.string.pref_auto_clear_chapter_cache),
),
),
)
}
@Composable
fun getStorageInfoPref(
chapterCacheReadableSize: String,
): Preference.PreferenceItem.CustomPreference {
val context = LocalContext.current
val available = remember {
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
}
val total = remember {
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
}
return Preference.PreferenceItem.CustomPreference(
title = stringResource(R.string.pref_storage_usage),
) {
BasePreferenceWidget(
title = stringResource(R.string.pref_storage_usage),
subcomponent = {
// TODO: downloads, SD cards, bar representation?, i18n
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
Text(text = "Available: $available / $total (chapter cache: $chapterCacheReadableSize)")
}
},
)
}
}
}
@Composable
private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
val syncServiceType = SyncManager.SyncService.fromInt(syncService)
return when (syncServiceType) {
SyncManager.SyncService.NONE -> emptyList()
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
} +
if (syncServiceType == SyncManager.SyncService.NONE) {
emptyList()
} else {
listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
}
}
@Composable
private fun getGoogleDrivePreferences(): List<Preference> {
val context = LocalContext.current
val googleDriveSync = Injekt.get<GoogleDriveService>()
return listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_google_drive_sign_in),
onClick = {
val intent = googleDriveSync.getSignInIntent()
context.startActivity(intent)
},
),
getGoogleDrivePurge(),
)
}
@Composable
private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
val scope = rememberCoroutineScope()
val showPurgeDialog = remember { mutableStateOf(false) }
val context = LocalContext.current
val googleDriveSync = remember { GoogleDriveSyncService(context) }
if (showPurgeDialog.value) {
PurgeConfirmationDialog(
onConfirm = {
showPurgeDialog.value = false
scope.launch {
val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
when (result) {
GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(R.string.google_drive_not_signed_in)
GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(R.string.google_drive_sync_data_not_found)
GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(R.string.google_drive_sync_data_purged)
}
}
},
onDismissRequest = { showPurgeDialog.value = false },
)
}
return Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_google_drive_purge_sync_data),
onClick = { showPurgeDialog.value = true },
)
}
@Composable
fun PurgeConfirmationDialog(
onConfirm: () -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_purge_confirmation_title)) },
text = { Text(text = stringResource(R.string.pref_purge_confirmation_message)) },
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
@Composable
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
return listOf(
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_device_name),
subtitle = stringResource(R.string.pref_sync_device_name_summ),
pref = syncPreferences.deviceName(),
),
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_host),
subtitle = stringResource(R.string.pref_sync_host_summ),
pref = syncPreferences.syncHost(),
),
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_api_key),
subtitle = stringResource(R.string.pref_sync_api_key_summ),
pref = syncPreferences.syncAPIKey(),
),
)
}
@Composable
private fun getSyncNowPref(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
if (showDialog) {
SyncConfirmationDialog(
onConfirm = {
showDialog = false
scope.launch {
if (!SyncDataJob.isAnyJobRunning(context)) {
SyncDataJob.startNow(context)
} else {
context.toast(R.string.sync_in_progress)
}
}
},
onDismissRequest = { showDialog = false },
)
}
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_sync_now_group_title),
preferenceItems = listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_sync_now),
subtitle = stringResource(R.string.pref_sync_now_subtitle),
onClick = {
showDialog = true
},
),
),
)
}
@Composable
private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
val context = LocalContext.current
val syncIntervalPref = syncPreferences.syncInterval()
val lastSync by syncPreferences.syncLastSync().collectAsState()
val formattedLastSync = DateUtils.getRelativeTimeSpanString(lastSync.toEpochMilli(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS)
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_sync_service_category),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = syncIntervalPref,
title = stringResource(R.string.pref_sync_interval),
entries = mapOf(
0 to stringResource(R.string.off),
30 to stringResource(R.string.update_30min),
60 to stringResource(R.string.update_1hour),
180 to stringResource(R.string.update_3hour),
360 to stringResource(R.string.update_6hour),
720 to stringResource(R.string.update_12hour),
1440 to stringResource(R.string.update_24hour),
2880 to stringResource(R.string.update_48hour),
10080 to stringResource(R.string.update_weekly),
),
onValueChanged = {
SyncDataJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.last_synchronization, formattedLastSync)),
),
)
}
@Composable
fun SyncConfirmationDialog(
onConfirm: () -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_sync_confirmation_title)) },
text = { Text(text = stringResource(R.string.pref_sync_confirmation_message)) },
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
private data class MissingRestoreComponents(
val uri: Uri,
val sources: List<String>,
val trackers: List<String>,
)
private data class InvalidRestore(
val uri: Uri? = null,
val message: String,
)

View File

@ -225,20 +225,28 @@ object SettingsLibraryScreen : SearchableSettings {
pref = libraryPreferences.swipeToStartAction(),
title = stringResource(R.string.pref_chapter_swipe_start),
entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(R.string.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.Download to stringResource(R.string.action_download),
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(R.string.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
stringResource(R.string.action_bookmark),
LibraryPreferences.ChapterSwipeAction.ToggleRead to
stringResource(R.string.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.Download to
stringResource(R.string.action_download),
),
),
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToEndAction(),
title = stringResource(R.string.pref_chapter_swipe_end),
entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(R.string.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.Download to stringResource(R.string.action_download),
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(R.string.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
stringResource(R.string.action_bookmark),
LibraryPreferences.ChapterSwipeAction.ToggleRead to
stringResource(R.string.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.Download to
stringResource(R.string.action_download),
),
),
),

View File

@ -9,7 +9,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ChromeReaderMode
import androidx.compose.material.icons.outlined.ChromeReaderMode
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.CollectionsBookmark
@ -19,7 +18,7 @@ import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsBackupRestore
import androidx.compose.material.icons.outlined.Storage
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
@ -187,7 +186,7 @@ object SettingsMainScreen : Screen() {
Item(
titleRes = R.string.pref_category_reader,
subtitleRes = R.string.pref_reader_summary,
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode,
icon = Icons.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen,
),
Item(
@ -210,9 +209,9 @@ object SettingsMainScreen : Screen() {
),
Item(
titleRes = R.string.label_backup_and_sync,
subtitleRes = R.string.pref_backup_and_sync_summary,
icon = Icons.Outlined.SettingsBackupRestore,
screen = SettingsBackupAndSyncScreen,
subtitleRes = R.string.pref_backup_summary,
icon = Icons.Outlined.Storage,
screen = SettingsDataScreen,
),
Item(
titleRes = R.string.pref_category_security,

View File

@ -10,9 +10,9 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -32,7 +32,7 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = readerPref.defaultReadingMode(),
title = stringResource(R.string.pref_viewer_type),
entries = ReadingModeType.entries.drop(1)
entries = ReadingMode.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) },
),
Preference.PreferenceItem.ListPreference(
@ -64,6 +64,11 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPref.pageTransitions(),
title = stringResource(R.string.pref_page_transitions),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.flashOnPageChange(),
title = stringResource(R.string.pref_flash_page),
subtitle = stringResource(R.string.pref_flash_page_summ),
),
getDisplayGroup(readerPreferences = readerPref),
getReadingGroup(readerPreferences = readerPref),
getPagedGroup(readerPreferences = readerPref),
@ -83,7 +88,7 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.defaultOrientationType(),
title = stringResource(R.string.pref_rotation_type),
entries = OrientationType.entries.drop(1)
entries = ReaderOrientation.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) },
),
Preference.PreferenceItem.ListPreference(
@ -169,12 +174,12 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pagerNavInverted(),
title = stringResource(R.string.pref_read_with_tapping_inverted),
entries = mapOf(
ReaderPreferences.TappingInvertMode.NONE to stringResource(R.string.none),
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(R.string.tapping_inverted_horizontal),
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(R.string.tapping_inverted_vertical),
ReaderPreferences.TappingInvertMode.BOTH to stringResource(R.string.tapping_inverted_both),
),
entries = listOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
ReaderPreferences.TappingInvertMode.VERTICAL,
ReaderPreferences.TappingInvertMode.BOTH,
).associateWith { stringResource(it.titleResId) },
enabled = navMode != 5,
),
Preference.PreferenceItem.ListPreference(
@ -261,12 +266,12 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonNavInverted(),
title = stringResource(R.string.pref_read_with_tapping_inverted),
entries = mapOf(
ReaderPreferences.TappingInvertMode.NONE to stringResource(R.string.none),
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(R.string.tapping_inverted_horizontal),
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(R.string.tapping_inverted_vertical),
ReaderPreferences.TappingInvertMode.BOTH to stringResource(R.string.tapping_inverted_both),
),
entries = listOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
ReaderPreferences.TappingInvertMode.VERTICAL,
ReaderPreferences.TappingInvertMode.BOTH,
).associateWith { stringResource(it.titleResId) },
enabled = navMode != 5,
),
Preference.PreferenceItem.SliderPreference(
@ -342,6 +347,11 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPreferences.readWithLongTap(),
title = stringResource(R.string.pref_read_with_long_tap),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.folderPerManga(),
title = stringResource(R.string.pref_create_folder_per_manga),
subtitle = stringResource(R.string.pref_create_folder_per_manga_summary),
),
),
)
}

View File

@ -202,7 +202,11 @@ private fun SearchResult(
SearchResultItem(
route = settingsData.route,
title = p.title,
breadcrumbs = getLocalizedBreadcrumb(path = settingsData.title, node = categoryTitle, isLtr = isLtr),
breadcrumbs = getLocalizedBreadcrumb(
path = settingsData.title,
node = categoryTitle,
isLtr = isLtr,
),
highlightKey = p.title,
)
}
@ -291,7 +295,7 @@ private val settingScreens = listOf(
SettingsDownloadScreen,
SettingsTrackingScreen,
SettingsBrowseScreen,
SettingsBackupAndSyncScreen,
SettingsDataScreen,
SettingsSecurityScreen,
SettingsAdvancedScreen,
)

View File

@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.Close
@ -73,7 +72,7 @@ object SettingsTrackingScreen : SearchableSettings {
val uriHandler = LocalUriHandler.current
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
imageVector = Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.tracking_guide),
)
}

View File

@ -228,6 +228,9 @@ object AboutScreen : Screen() {
is GetApplicationRelease.Result.NoNewUpdate -> {
context.toast(R.string.update_check_no_new_updates)
}
is GetApplicationRelease.Result.OsTooOld -> {
context.toast(R.string.update_check_eol)
}
else -> {}
}
} catch (e: Exception) {

View File

@ -0,0 +1,168 @@
package eu.kanade.presentation.more.settings.screen.data
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.update
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
class CreateBackupScreen : Screen() {
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { CreateBackupScreenModel() }
val state by model.state.collectAsState()
val chooseBackupDir = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/*"),
) {
if (it != null) {
context.contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
)
model.createBackup(context, it)
navigator.pop()
}
}
Scaffold(
topBar = {
AppBar(
title = stringResource(R.string.pref_create_backup),
navigateUp = navigator::pop,
scrollBehavior = it,
)
},
) { contentPadding ->
Column(
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
) {
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(horizontal = MaterialTheme.padding.medium),
) {
item {
LabeledCheckbox(
label = stringResource(R.string.manga),
checked = true,
onCheckedChange = {},
enabled = false,
)
}
BackupChoices.forEach { (k, v) ->
item {
LabeledCheckbox(
label = stringResource(v),
checked = state.flags.contains(k),
onCheckedChange = {
model.toggleFlag(k)
},
)
}
}
}
HorizontalDivider()
Button(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
onClick = {
if (!BackupCreateJob.isManualJobRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
try {
chooseBackupDir.launch(Backup.getFilename())
} catch (e: ActivityNotFoundException) {
context.toast(R.string.file_picker_error)
}
} else {
context.toast(R.string.backup_in_progress)
}
},
) {
Text(
text = stringResource(R.string.action_create),
color = MaterialTheme.colorScheme.onPrimary,
)
}
}
}
}
}
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
fun toggleFlag(flag: Int) {
mutableState.update {
if (it.flags.contains(flag)) {
it.copy(flags = it.flags - flag)
} else {
it.copy(flags = it.flags + flag)
}
}
}
fun createBackup(context: Context, uri: Uri) {
val flags = state.value.flags.fold(initial = 0, operation = { a, b -> a or b })
BackupCreateJob.startNow(context, uri, flags)
}
@Immutable
data class State(
val flags: Set<Int> = BackupChoices.keys,
)
}
private val BackupChoices = mapOf(
BackupCreateFlags.BACKUP_CATEGORY to R.string.categories,
BackupCreateFlags.BACKUP_CHAPTER to R.string.chapters,
BackupCreateFlags.BACKUP_TRACK to R.string.track,
BackupCreateFlags.BACKUP_HISTORY to R.string.history,
BackupCreateFlags.BACKUP_APP_PREFS to R.string.app_settings,
BackupCreateFlags.BACKUP_SOURCE_PREFS to R.string.source_settings,
)

View File

@ -78,7 +78,8 @@ class DebugInfoScreen : Screen() {
value = when (result) {
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed"
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled"
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING -> "Compiled non-matching"
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
"Compiled non-matching"
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE,
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,

View File

@ -115,7 +115,9 @@ class WorkerInfoScreen : Screen() {
private val workManager = context.workManager
val finished = workManager
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
.getWorkInfosLiveData(
WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED),
)
.asFlow()
.map(::constructString)
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "")

View File

@ -38,6 +38,7 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.presentation.manga.components.MangaCover
@ -46,7 +47,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
@ -249,15 +249,17 @@ fun AppThemePreviewItem(
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun AppThemesListPreview() {
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
TachiyomiTheme {
Surface {
AppThemesList(
currentTheme = appTheme,
amoled = false,
onItemClick = { appTheme = it },
)
}
}
}

View File

@ -12,10 +12,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
@ -40,7 +40,7 @@ internal fun InfoWidget(text: String) {
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun InfoWidgetPreview() {
TachiyomiTheme {

View File

@ -9,8 +9,8 @@ import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.presentation.core.util.ThemePreviews
@Composable
fun SwitchPreferenceWidget(
@ -37,7 +37,7 @@ fun SwitchPreferenceWidget(
)
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun SwitchPreferenceWidgetPreview() {
TachiyomiTheme {

View File

@ -12,8 +12,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
@ -59,7 +59,7 @@ fun TextPreferenceWidget(
)
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun TextPreferenceWidgetPreview() {
TachiyomiTheme {

View File

@ -115,8 +115,16 @@ fun <T> TriStateListDialog(
}
}
if (!listState.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!listState.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
if (!listState.isScrolledToStart()) {
HorizontalDivider(
modifier = Modifier.align(Alignment.TopCenter),
)
}
if (!listState.isScrolledToEnd()) {
HorizontalDivider(
modifier = Modifier.align(Alignment.BottomCenter),
)
}
}
}
},

View File

@ -0,0 +1,27 @@
package eu.kanade.presentation.reader
import androidx.annotation.IntRange
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import kotlin.math.abs
@Composable
fun BrightnessOverlay(
@IntRange(from = -100, to = 100) value: Int,
) {
if (value >= 0) return
Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
alpha = abs(value) / 100f
},
) {
drawRect(Color.Black)
}
}

View File

@ -11,8 +11,8 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.OfflinePin
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
@ -32,6 +32,7 @@ import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiTheme
@ -42,7 +43,6 @@ import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import tachiyomi.domain.chapter.service.calculateChapterGap
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
@ -244,7 +244,7 @@ private fun ChapterText(
),
) {
Icon(
imageVector = Icons.Outlined.OfflinePin,
imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.label_downloaded),
)
},
@ -304,7 +304,7 @@ private val FakeChapterLongTitle = previewChapter(
chapterNumber = 1f,
)
@ThemePreviews
@PreviewLightDark
@Composable
private fun TransitionTextPreview() {
TachiyomiTheme {
@ -318,7 +318,7 @@ private fun TransitionTextPreview() {
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun TransitionTextLongTitlePreview() {
TachiyomiTheme {
@ -332,7 +332,7 @@ private fun TransitionTextLongTitlePreview() {
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun TransitionTextWithGapPreview() {
TachiyomiTheme {
@ -346,7 +346,7 @@ private fun TransitionTextWithGapPreview() {
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun TransitionTextNoNextPreview() {
TachiyomiTheme {
@ -360,7 +360,7 @@ private fun TransitionTextNoNextPreview() {
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun TransitionTextNoPreviousPreview() {
TachiyomiTheme {

View File

@ -0,0 +1,45 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.delay
@Stable
class DisplayRefreshHost {
internal var currentDisplayRefresh by mutableStateOf(false)
fun flash() {
currentDisplayRefresh = true
}
}
@Composable
fun DisplayRefreshHost(
hostState: DisplayRefreshHost,
modifier: Modifier = Modifier,
) {
val currentDisplayRefresh = hostState.currentDisplayRefresh
LaunchedEffect(currentDisplayRefresh) {
if (currentDisplayRefresh) {
delay(200)
hostState.currentDisplayRefresh = false
}
}
if (currentDisplayRefresh) {
Canvas(
modifier = modifier.fillMaxSize(),
) {
drawRect(Color.White)
}
}
}

View File

@ -1,54 +0,0 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.SettingsIconGrid
import tachiyomi.presentation.core.components.material.IconToggleButton
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
@Composable
fun OrientationModeSelectDialog(
onDismissRequest: () -> Unit,
screenModel: ReaderSettingsScreenModel,
onChange: (Int) -> Unit,
) {
val manga by screenModel.mangaFlow.collectAsState()
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
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()
},
modifier = Modifier.fillMaxWidth(),
imageVector = ImageVector.vectorResource(mode.iconRes),
title = stringResource(stringRes),
)
}
}
}
}
}

View File

@ -0,0 +1,100 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.tooling.preview.PreviewLightDark
import eu.kanade.domain.manga.model.readerOrientation
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.reader.components.ModeSelectionDialog
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.presentation.core.components.SettingsIconGrid
import tachiyomi.presentation.core.components.material.IconToggleButton
private val ReaderOrientationsWithoutDefault = ReaderOrientation.entries - ReaderOrientation.DEFAULT
@Composable
fun OrientationSelectDialog(
onDismissRequest: () -> Unit,
screenModel: ReaderSettingsScreenModel,
onChange: (Int) -> Unit,
) {
val manga by screenModel.mangaFlow.collectAsState()
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
AdaptiveSheet(onDismissRequest = onDismissRequest) {
DialogContent(
orientation = orientation,
onChangeOrientation = {
screenModel.onChangeOrientation(it)
onChange(it.stringRes)
onDismissRequest()
},
)
}
}
@Composable
private fun DialogContent(
orientation: ReaderOrientation,
onChangeOrientation: (ReaderOrientation) -> Unit,
) {
var selected by remember { mutableStateOf(orientation) }
ModeSelectionDialog(
onUseDefault = {
onChangeOrientation(
ReaderOrientation.DEFAULT,
)
}.takeIf { orientation != ReaderOrientation.DEFAULT },
onApply = { onChangeOrientation(selected) },
) {
SettingsIconGrid(R.string.rotation_type) {
items(ReaderOrientationsWithoutDefault) { mode ->
IconToggleButton(
checked = mode == selected,
onCheckedChange = {
selected = mode
},
modifier = Modifier.fillMaxWidth(),
imageVector = ImageVector.vectorResource(mode.iconRes),
title = stringResource(mode.stringRes),
)
}
}
}
}
@PreviewLightDark
@Composable
private fun DialogContentPreview() {
TachiyomiTheme {
Surface {
Column {
DialogContent(
orientation = ReaderOrientation.DEFAULT,
onChangeOrientation = {},
)
DialogContent(
orientation = ReaderOrientation.FREE,
onChangeOrientation = {},
)
}
}
}
}

View File

@ -2,13 +2,17 @@ package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiTheme
@Composable
fun PageIndicatorText(
@ -19,24 +23,37 @@ fun PageIndicatorText(
val text = "$currentPage / $totalPages"
Box {
Text(
text = text,
color = Color(45, 45, 45),
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
style = TextStyle.Default.copy(
drawStyle = Stroke(width = 4f),
),
)
Text(
text = text,
val style = TextStyle(
color = Color(235, 235, 235),
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
)
val strokeStyle = style.copy(
color = Color(45, 45, 45),
drawStyle = Stroke(width = 4f),
)
Box(
contentAlignment = Alignment.Center,
) {
Text(
text = text,
style = strokeStyle,
)
Text(
text = text,
style = style,
)
}
}
@PreviewLightDark
@Composable
private fun PageIndicatorTextPreview() {
TachiyomiTheme {
Surface {
PageIndicatorText(currentPage = 10, totalPages = 69)
}
}
}

View File

@ -1,28 +1,31 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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 eu.kanade.domain.manga.model.readingModeType
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.domain.manga.model.readingMode
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.reader.components.ModeSelectionDialog
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
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 }
private val ReadingModesWithoutDefault = ReadingMode.entries - ReadingMode.DEFAULT
@Composable
fun ReadingModeSelectDialog(
@ -31,25 +34,63 @@ fun ReadingModeSelectDialog(
onChange: (Int) -> Unit,
) {
val manga by screenModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
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)
DialogContent(
readingMode = readingMode,
onChangeReadingMode = {
screenModel.onChangeReadingMode(it)
onChange(it.stringRes)
onDismissRequest()
},
)
}
}
@Composable
private fun DialogContent(
readingMode: ReadingMode,
onChangeReadingMode: (ReadingMode) -> Unit,
) {
var selected by remember { mutableStateOf(readingMode) }
ModeSelectionDialog(
onUseDefault = { onChangeReadingMode(ReadingMode.DEFAULT) }.takeIf { readingMode != ReadingMode.DEFAULT },
onApply = { onChangeReadingMode(selected) },
) {
SettingsIconGrid(R.string.pref_category_reading_mode) {
items(ReadingModesWithoutDefault) { mode ->
IconToggleButton(
checked = mode == selected,
onCheckedChange = {
selected = mode
},
modifier = Modifier.fillMaxWidth(),
imageVector = ImageVector.vectorResource(mode.iconRes),
title = stringResource(stringRes),
title = stringResource(mode.stringRes),
)
}
}
}
}
@PreviewLightDark
@Composable
private fun DialogContentPreview() {
TachiyomiTheme {
Surface {
Column {
DialogContent(
readingMode = ReadingMode.DEFAULT,
onChangeReadingMode = {},
)
DialogContent(
readingMode = ReadingMode.LEFT_TO_RIGHT,
onChangeReadingMode = {},
)
}
}
}
}

View File

@ -17,16 +17,16 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
@Composable
fun BottomReaderBar(
backgroundColor: Color,
readingMode: ReadingModeType,
readingMode: ReadingMode,
onClickReadingMode: () -> Unit,
orientationMode: OrientationType,
onClickOrientationMode: () -> Unit,
orientation: ReaderOrientation,
onClickOrientation: () -> Unit,
cropEnabled: Boolean,
onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit,
@ -46,6 +46,13 @@ fun BottomReaderBar(
)
}
IconButton(onClick = onClickOrientation) {
Icon(
painter = painterResource(orientation.iconRes),
contentDescription = stringResource(R.string.rotation_type),
)
}
IconButton(onClick = onClickCropBorder) {
Icon(
painter = painterResource(if (cropEnabled) R.drawable.ic_crop_24dp else R.drawable.ic_crop_off_24dp),
@ -53,13 +60,6 @@ fun BottomReaderBar(
)
}
IconButton(onClick = onClickOrientationMode) {
Icon(
painter = painterResource(orientationMode.iconRes),
contentDescription = stringResource(R.string.pref_rotation_type),
)
}
IconButton(onClick = onClickSettings) {
Icon(
imageVector = Icons.Outlined.Settings,

View File

@ -9,10 +9,8 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.material.icons.outlined.BookmarkBorder
@ -25,9 +23,10 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.reader.components.ChapterNavigator
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
@ -56,10 +55,10 @@ fun ReaderAppBars(
totalPages: Int,
onSliderValueChange: (Int) -> Unit,
readingMode: ReadingModeType,
readingMode: ReadingMode,
onClickReadingMode: () -> Unit,
orientationMode: OrientationType,
onClickOrientationMode: () -> Unit,
orientation: ReaderOrientation,
onClickOrientation: () -> Unit,
cropEnabled: Boolean,
onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit,
@ -69,8 +68,8 @@ fun ReaderAppBars(
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
val appBarModifier = if (fullscreen) {
Modifier.windowInsetsPadding(WindowInsets.systemBars)
val modifierWithInsetsPadding = if (fullscreen) {
Modifier.systemBarsPadding()
} else {
Modifier
}
@ -91,7 +90,7 @@ fun ReaderAppBars(
),
) {
AppBar(
modifier = appBarModifier
modifier = modifierWithInsetsPadding
.clickable(onClick = onClickTopAppBar),
backgroundColor = backgroundColor,
title = mangaTitle,
@ -101,7 +100,9 @@ fun ReaderAppBars(
AppBarActions(
listOfNotNull(
AppBar.Action(
title = stringResource(if (bookmarked) R.string.action_remove_bookmark else R.string.action_bookmark),
title = stringResource(
if (bookmarked) R.string.action_remove_bookmark else R.string.action_bookmark,
),
icon = if (bookmarked) Icons.Outlined.Bookmark else Icons.Outlined.BookmarkBorder,
onClick = onToggleBookmarked,
),
@ -137,6 +138,7 @@ fun ReaderAppBars(
),
) {
Column(
modifier = modifierWithInsetsPadding,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ChapterNavigator(
@ -154,8 +156,8 @@ fun ReaderAppBars(
backgroundColor = backgroundColor,
readingMode = readingMode,
onClickReadingMode = onClickReadingMode,
orientationMode = orientationMode,
onClickOrientationMode = onClickOrientationMode,
orientation = orientation,
onClickOrientation = onClickOrientation,
cropEnabled = cropEnabled,
onClickCropBorder = onClickCropBorder,
onClickSettings = onClickSettings,

View File

@ -1,4 +1,4 @@
package eu.kanade.presentation.reader.appbars
package eu.kanade.presentation.reader.components
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -77,7 +77,9 @@ fun ChapterNavigator(
) {
Icon(
imageVector = Icons.Outlined.SkipPrevious,
contentDescription = stringResource(if (isRtl) R.string.action_next_chapter else R.string.action_previous_chapter),
contentDescription = stringResource(
if (isRtl) R.string.action_next_chapter else R.string.action_previous_chapter,
),
)
}
@ -127,7 +129,9 @@ fun ChapterNavigator(
) {
Icon(
imageVector = Icons.Outlined.SkipNext,
contentDescription = stringResource(if (isRtl) R.string.action_previous_chapter else R.string.action_next_chapter),
contentDescription = stringResource(
if (isRtl) R.string.action_previous_chapter else R.string.action_next_chapter,
),
)
}
}

View File

@ -0,0 +1,89 @@
package eu.kanade.presentation.reader.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.SettingsItemsPaddings
@Composable
fun ModeSelectionDialog(
onApply: () -> Unit,
onUseDefault: (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
Box(modifier = Modifier.padding(vertical = 16.dp)) {
Column {
content()
Row(
modifier = Modifier.padding(
horizontal = SettingsItemsPaddings.Horizontal,
),
) {
onUseDefault?.let {
OutlinedButton(onClick = it) {
Text(text = stringResource(R.string.action_revert_to_default))
}
}
Spacer(modifier = Modifier.weight(1f))
FilledTonalButton(
onClick = onApply,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = null,
)
Text(text = stringResource(R.string.action_apply))
}
}
}
}
}
}
@PreviewLightDark
@Composable
private fun Preview() {
TachiyomiTheme {
Surface {
Column {
ModeSelectionDialog(
onApply = {},
onUseDefault = {},
) {
Text("Dummy content")
}
ModeSelectionDialog(
onApply = {},
) {
Text("Dummy content without default")
}
}
}
}
}

View File

@ -68,4 +68,9 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
label = stringResource(R.string.pref_page_transitions),
pref = screenModel.preferences.pageTransitions(),
)
CheckboxItem(
label = stringResource(R.string.pref_flash_page),
pref = screenModel.preferences.flashOnPageChange(),
)
}

View File

@ -8,13 +8,13 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.manga.model.orientationType
import eu.kanade.domain.manga.model.readingModeType
import eu.kanade.domain.manga.model.readerOrientation
import eu.kanade.domain.manga.model.readingMode
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem
@ -23,33 +23,29 @@ import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState
import java.text.NumberFormat
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
private val tappingInvertModeOptions = ReaderPreferences.TappingInvertMode.entries.map { it.titleResId to it }
@Composable
internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) {
HeadingItem(R.string.pref_category_for_this_series)
val manga by screenModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
SettingsChipRow(R.string.pref_category_reading_mode) {
readingModeOptions.map { (stringRes, it) ->
ReadingMode.entries.map {
FilterChip(
selected = it == readingMode,
onClick = { screenModel.onChangeReadingMode(it) },
label = { Text(stringResource(stringRes)) },
label = { Text(stringResource(it.stringRes)) },
)
}
}
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
SettingsChipRow(R.string.rotation_type) {
orientationTypeOptions.map { (stringRes, it) ->
ReaderOrientation.entries.map {
FilterChip(
selected = it == orientationType,
selected = it == orientation,
onClick = { screenModel.onChangeOrientation(it) },
label = { Text(stringResource(stringRes)) },
label = { Text(stringResource(it.stringRes)) },
)
}
}
@ -209,11 +205,11 @@ private fun ColumnScope.TapZonesItems(
if (selected != 5) {
SettingsChipRow(R.string.pref_read_with_tapping_inverted) {
tappingInvertModeOptions.map { (stringRes, mode) ->
ReaderPreferences.TappingInvertMode.entries.map {
FilterChip(
selected = mode == invertMode,
onClick = { onSelectInvertMode(mode) },
label = { Text(stringResource(stringRes)) },
selected = it == invertMode,
onClick = { onSelectInvertMode(it) },
label = { Text(stringResource(it.titleResId)) },
)
}
}

View File

@ -28,6 +28,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.VerticalDivider
@ -44,6 +45,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.model.toDbTrack
@ -54,7 +56,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.presentation.core.util.ThemePreviews
import java.text.DateFormat
private const val UnsetStatusTextAlpha = 0.5F
@ -318,11 +319,15 @@ private fun TrackInfoItemMenu(
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun TrackInfoDialogHomePreviews(
@PreviewParameter(TrackInfoDialogHomePreviewProvider::class)
content: @Composable () -> Unit,
) {
TachiyomiTheme { content() }
TachiyomiTheme {
Surface {
content()
}
}
}

View File

@ -20,6 +20,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.SelectableDates
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.minimumInteractiveComponentSize
@ -29,6 +30,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
@ -37,7 +39,6 @@ import tachiyomi.presentation.core.components.WheelNumberPicker
import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@ -221,10 +222,11 @@ private fun BaseSelector(
)
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun TrackStatusSelectorPreviews() {
TachiyomiTheme {
Surface {
TrackStatusSelector(
selection = 1,
onSelectionChange = {},
@ -241,4 +243,5 @@ private fun TrackStatusSelectorPreviews() {
onDismissRequest = {},
)
}
}
}

View File

@ -28,7 +28,6 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
@ -57,6 +56,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover
@ -68,7 +68,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.plus
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
import tachiyomi.presentation.core.util.secondaryItemAlpha
@ -98,7 +97,7 @@ fun TrackerSearch(
navigationIcon = {
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -241,7 +240,7 @@ private fun SearchResultItem(
) {
if (selected) {
Icon(
imageVector = Icons.Default.CheckCircle,
imageVector = Icons.Filled.CheckCircle,
contentDescription = null,
modifier = Modifier.align(Alignment.TopEnd),
tint = MaterialTheme.colorScheme.primary,
@ -320,7 +319,7 @@ private fun SearchResultItemDetails(
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun TrackerSearchPreviews(
@PreviewParameter(TrackerSearchPreviewProvider::class)

View File

@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.data.track.Tracker
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.clickableNoIndication
@Composable
@ -43,7 +43,7 @@ fun TrackLogoIcon(
}
}
@ThemePreviews
@PreviewLightDark
@Composable
private fun TrackLogoIconPreviews(
@PreviewParameter(TrackLogoIconPreviewProvider::class)

View File

@ -109,9 +109,7 @@ fun UpdateScreen(
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
if (lastUpdated > 0L) {
updatesLastUpdatedItem(lastUpdated)
}
updatesUiItems(
uiModels = state.getUiModel(context, relativeTime),

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.updates
import android.text.format.DateUtils
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -27,7 +26,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
@ -39,6 +37,7 @@ import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
import eu.kanade.presentation.manga.components.DotSeparatorText
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
@ -47,33 +46,18 @@ import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.ReadItemAlpha
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.selectedBackground
import java.util.Date
import kotlin.time.Duration.Companion.minutes
internal fun LazyListScope.updatesLastUpdatedItem(
lastUpdated: Long,
) {
item(key = "updates-lastUpdated") {
val time = remember(lastUpdated) {
val now = Date().time
if (now - lastUpdated < 1.minutes.inWholeMilliseconds) {
null
} else {
DateUtils.getRelativeTimeSpanString(lastUpdated, now, DateUtils.MINUTE_IN_MILLIS)
}
}
Box(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
) {
Text(
text = if (time.isNullOrEmpty()) {
stringResource(R.string.updates_last_update_info, stringResource(R.string.updates_last_update_info_just_now))
} else {
stringResource(R.string.updates_last_update_info, time)
},
text = stringResource(R.string.updates_last_update_info, relativeTimeSpanString(lastUpdated)),
fontStyle = FontStyle.Italic,
)
}

View File

@ -1,8 +1,14 @@
package eu.kanade.presentation.util
import android.content.Context
import android.text.format.DateUtils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import java.util.Date
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
fun Duration.toDurationString(context: Context, fallback: String): String {
return toComponents { days, hours, minutes, seconds, _ ->
@ -14,3 +20,14 @@ fun Duration.toDurationString(context: Context, fallback: String): String {
}.joinToString(" ").ifBlank { fallback }
}
}
@Composable
@ReadOnlyComposable
fun relativeTimeSpanString(epochMillis: Long): String {
val now = Date().time
return when {
epochMillis <= 0L -> stringResource(R.string.relative_time_span_never)
now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource(R.string.updates_last_update_info_just_now)
else -> DateUtils.getRelativeTimeSpanString(epochMillis, now, DateUtils.MINUTE_IN_MILLIS).toString()
}
}

View File

@ -11,8 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close
@ -100,6 +98,12 @@ fun WebViewScreenContent(
request: WebResourceRequest?,
): Boolean {
request?.let {
// Don't attempt to open blobs as webpages
if (it.url.toString().startsWith("blob:http")) {
return false
}
// Continue with request, but with custom headers
view?.loadUrl(it.url.toString(), headers)
}
return super.shouldOverrideUrlLoading(view, request)
@ -121,7 +125,7 @@ fun WebViewScreenContent(
listOf(
AppBar.Action(
title = stringResource(R.string.action_webview_back),
icon = Icons.AutoMirrored.Outlined.ArrowBack,
icon = Icons.Outlined.ArrowBack,
onClick = {
if (navigator.canGoBack) {
navigator.navigateBack()
@ -131,7 +135,7 @@ fun WebViewScreenContent(
),
AppBar.Action(
title = stringResource(R.string.action_webview_forward),
icon = Icons.AutoMirrored.Outlined.ArrowForward,
icon = Icons.Outlined.ArrowForward,
onClick = {
if (navigator.canGoForward) {
navigator.navigateForward()
@ -169,7 +173,9 @@ fun WebViewScreenContent(
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.clickable {
uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare")
uriHandler.openUri(
"https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare",
)
},
)
}
@ -182,7 +188,7 @@ fun WebViewScreenContent(
.align(Alignment.BottomCenter),
)
is LoadingState.Loading -> LinearProgressIndicator(
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
progress = (loadingState as? LoadingState.Loading)?.progress ?: 1f,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),

View File

@ -11,6 +11,7 @@ import android.content.IntentFilter
import android.os.Build
import android.os.Looper
import android.webkit.WebView
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@ -185,7 +186,7 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
}
} catch (e: Exception) {
} catch (_: Exception) {
}
}
return super.getPackageName()
@ -222,7 +223,12 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
fun register() {
if (!registered) {
registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE))
ContextCompat.registerReceiver(
this@App,
this,
IntentFilter(ACTION_DISABLE_INCOGNITO_MODE),
ContextCompat.RECEIVER_NOT_EXPORTED,
)
registered = true
}
}
@ -241,7 +247,7 @@ private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNIT
/**
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
*/
internal object CoilDiskCache {
private object CoilDiskCache {
private const val FOLDER_NAME = "image_cache"
private var instance: DiskCache? = null

View File

@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
@ -121,13 +121,22 @@ object Migrations {
}
}
prefs.edit {
putInt(libraryPreferences.filterDownloaded().key(), convertBooleanPrefToTriState("pref_filter_downloaded_key"))
putInt(
libraryPreferences.filterDownloaded().key(),
convertBooleanPrefToTriState("pref_filter_downloaded_key"),
)
remove("pref_filter_downloaded_key")
putInt(libraryPreferences.filterUnread().key(), convertBooleanPrefToTriState("pref_filter_unread_key"))
putInt(
libraryPreferences.filterUnread().key(),
convertBooleanPrefToTriState("pref_filter_unread_key"),
)
remove("pref_filter_unread_key")
putInt(libraryPreferences.filterCompleted().key(), convertBooleanPrefToTriState("pref_filter_completed_key"))
putInt(
libraryPreferences.filterCompleted().key(),
convertBooleanPrefToTriState("pref_filter_completed_key"),
)
remove("pref_filter_completed_key")
}
}
@ -161,12 +170,12 @@ object Migrations {
if (oldVersion < 60) {
// Migrate Rotation and Viewer values to default values for viewer_flags
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> OrientationType.FREE.flagValue
2 -> OrientationType.PORTRAIT.flagValue
3 -> OrientationType.LANDSCAPE.flagValue
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
else -> OrientationType.FREE.flagValue
1 -> ReaderOrientation.FREE.flagValue
2 -> ReaderOrientation.PORTRAIT.flagValue
3 -> ReaderOrientation.LANDSCAPE.flagValue
4 -> ReaderOrientation.LOCKED_PORTRAIT.flagValue
5 -> ReaderOrientation.LOCKED_LANDSCAPE.flagValue
else -> ReaderOrientation.FREE.flagValue
}
// Reading mode flag and prefValue is the same value
@ -242,7 +251,10 @@ object Migrations {
if (oldSecureScreen) {
securityPreferences.secureScreen().set(SecurityPreferences.SecureScreenMode.ALWAYS)
}
if (DeviceUtil.isMiui && basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller.PACKAGEINSTALLER) {
if (
DeviceUtil.isMiui &&
basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller.PACKAGEINSTALLER
) {
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY)
}
}
@ -259,7 +271,12 @@ object Migrations {
if (oldVersion < 81) {
// Handle renamed enum values
prefs.edit {
val newSortingMode = when (val oldSortingMode = prefs.getString(libraryPreferences.sortingMode().key(), "ALPHABETICAL")) {
val newSortingMode = when (
val oldSortingMode = prefs.getString(
libraryPreferences.sortingMode().key(),
"ALPHABETICAL",
)
) {
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
"UNREAD" -> "UNREAD_COUNT"
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
@ -375,17 +392,28 @@ object Migrations {
}
}
if (oldVersion < 107) {
preferenceStore.getAll()
.filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }
.forEach { (key, value) ->
if (value is String) {
preferenceStore
.getString(Preference.privateKey(key))
.set(value)
preferenceStore.getString(key).delete()
}
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
newKey = { Preference.privateKey(it) },
)
}
if (oldVersion < 108) {
val prefsToReplace = listOf(
"pref_download_only",
"incognito_mode",
"last_catalogue_source",
"trusted_signatures",
"last_app_closed",
"library_update_last_timestamp",
"library_unseen_updates_count",
"last_used_category",
)
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key in prefsToReplace },
newKey = { Preference.appStateKey(it) },
)
}
return true
}
@ -393,3 +421,41 @@ object Migrations {
return false
}
}
@Suppress("UNCHECKED_CAST")
private fun replacePreferences(
preferenceStore: PreferenceStore,
filterPredicate: (Map.Entry<String, Any?>) -> Boolean,
newKey: (String) -> String,
) {
preferenceStore.getAll()
.filter(filterPredicate)
.forEach { (key, value) ->
when (value) {
is Int -> {
preferenceStore.getInt(newKey(key)).set(value)
preferenceStore.getInt(key).delete()
}
is Long -> {
preferenceStore.getLong(newKey(key)).set(value)
preferenceStore.getLong(key).delete()
}
is Float -> {
preferenceStore.getFloat(newKey(key)).set(value)
preferenceStore.getFloat(key).delete()
}
is String -> {
preferenceStore.getString(newKey(key)).set(value)
preferenceStore.getString(key).delete()
}
is Boolean -> {
preferenceStore.getBoolean(newKey(key)).set(value)
preferenceStore.getBoolean(key).delete()
}
is Set<*> -> (value as? Set<String>)?.let {
preferenceStore.getStringSet(newKey(key)).set(value)
preferenceStore.getStringSet(key).delete()
}
}
}
}

View File

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.data.backup
// Filter options
internal object BackupConst {
const val BACKUP_CATEGORY = 0x1
const val BACKUP_CATEGORY_MASK = 0x1
const val BACKUP_CHAPTER = 0x2
const val BACKUP_CHAPTER_MASK = 0x2
const val BACKUP_HISTORY = 0x4
const val BACKUP_HISTORY_MASK = 0x4
const val BACKUP_TRACK = 0x8
const val BACKUP_TRACK_MASK = 0x8
const val BACKUP_APP_PREFS = 0x10
const val BACKUP_APP_PREFS_MASK = 0x10
const val BACKUP_SOURCE_PREFS = 0x20
const val BACKUP_SOURCE_PREFS_MASK = 0x20
const val BACKUP_ALL = 0x3F
}

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.data.backup
internal object BackupCreateFlags {
const val BACKUP_CATEGORY = 0x1
const val BACKUP_CHAPTER = 0x2
const val BACKUP_HISTORY = 0x4
const val BACKUP_TRACK = 0x8
const val BACKUP_APP_PREFS = 0x10
const val BACKUP_SOURCE_PREFS = 0x20
const val AutomaticDefaults = BACKUP_CATEGORY or
BACKUP_CHAPTER or
BACKUP_HISTORY or
BACKUP_TRACK or
BACKUP_APP_PREFS or
BACKUP_SOURCE_PREFS
}

View File

@ -23,6 +23,7 @@ import tachiyomi.core.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
@ -40,7 +41,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
val backupPreferences = Injekt.get<BackupPreferences>()
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: backupPreferences.backupsDirectory().get().toUri()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
try {
setForeground(getForegroundInfo())
@ -50,7 +51,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
return try {
val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
if (isAutoBackup) {
backupPreferences.lastAutoBackupTimestamp().set(Date().time)
} else {
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
}
Result.success()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)

View File

@ -5,20 +5,15 @@ import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_APP_PREFS
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_SOURCE_PREFS
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
@ -160,7 +155,7 @@ class BackupCreator(
*/
suspend fun backupCategories(options: Int): List<BackupCategory> {
// Check if user wants category information in backup
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
return if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) {
getCategories.await()
.filterNot(Category::isSystemCategory)
.map(backupCategoryMapper)
@ -182,21 +177,26 @@ class BackupCreator(
* @param options options for the backup
* @return [BackupManga] containing manga in a serializable form
*/
private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
suspend fun backupManga(manga: Manga, options: Int): BackupManga {
// Entry for this manga
val mangaObject = BackupManga.copyFrom(manga)
// Check if user wants chapter information in backup
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
if (options and BACKUP_CHAPTER == BACKUP_CHAPTER) {
// Backup all the chapters
val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
if (chapters.isNotEmpty()) {
mangaObject.chapters = chapters
handler.awaitList {
chaptersQueries.getChaptersByMangaId(
mangaId = manga.id,
applyScanlatorFilter = 0, // false
mapper = backupChapterMapper,
)
}
.takeUnless(List<BackupChapter>::isEmpty)
?.let { mangaObject.chapters = it }
}
// Check if user wants category information in backup
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) {
// Backup categories for this manga
val categoriesForManga = getCategories.await(manga.id)
if (categoriesForManga.isNotEmpty()) {
@ -205,7 +205,7 @@ class BackupCreator(
}
// Check if user wants track information in backup
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
if (options and BACKUP_TRACK == BACKUP_TRACK) {
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
if (tracks.isNotEmpty()) {
mangaObject.tracking = tracks
@ -213,7 +213,7 @@ class BackupCreator(
}
// Check if user wants history information in backup
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
if (options and BACKUP_HISTORY == BACKUP_HISTORY) {
val historyByMangaId = getHistory.await(manga.id)
if (historyByMangaId.isNotEmpty()) {
val history = historyByMangaId.map { history ->
@ -230,13 +230,13 @@ class BackupCreator(
}
fun backupAppPreferences(flags: Int): List<BackupPreference> {
if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList()
return preferenceStore.getAll().toBackupPreferences()
}
fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList()
return sourceManager.getCatalogueSources()
.filterIsInstance<ConfigurableSource>()
@ -250,7 +250,9 @@ class BackupCreator(
@Suppress("UNCHECKED_CAST")
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
return this.filterKeys { !Preference.isPrivate(it) }
return this.filterKeys {
!Preference.isPrivate(it) && !Preference.isAppState(it)
}
.mapNotNull { (key, value) ->
when (value) {
is Int -> BackupPreference(key, IntPreferenceValue(value))

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