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,4 +1,5 @@
[*.{kt,kts}] [*.{kt,kts}]
max_line_length = 120
indent_size = 4 indent_size = 4
insert_final_newline = true insert_final_newline = true
ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma = true

View File

@ -3,7 +3,7 @@
I acknowledge that: I acknowledge that:
- I have updated: - I have updated:
- To the latest version of the app (stable is v0.14.6) - To the latest version of the app (stable is v0.14.7)
- All extensions - All extensions
- I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/) - I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/)
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions

View File

@ -53,7 +53,7 @@ body:
label: Tachiyomi version label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**. description: You can find your Tachiyomi version in **More → About**.
placeholder: | placeholder: |
Example: "0.14.6" Example: "0.14.7"
validations: validations:
required: true required: true
@ -98,7 +98,7 @@ body:
required: true required: true
- label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/). - label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/).
required: true required: true
- label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. - label: I have updated the app to version **[0.14.7](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true

View File

@ -33,7 +33,7 @@ body:
required: true required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose). - label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
required: true required: true
- label: I have 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 required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
class BasePreferences( class BasePreferences(
@ -12,9 +13,12 @@ class BasePreferences(
private val preferenceStore: PreferenceStore, 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) 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.copyFromSChapter
import eu.kanade.domain.chapter.model.toSChapter 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.interactor.UpdateManga
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.download.DownloadManager 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.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import tachiyomi.data.chapter.ChapterSanitizer 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.ShouldUpdateDbChapter
import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
@ -32,7 +33,8 @@ class SyncChaptersWithSource(
private val shouldUpdateDbChapter: ShouldUpdateDbChapter, private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
private val updateManga: UpdateManga, private val updateManga: UpdateManga,
private val updateChapter: UpdateChapter, 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. // Chapters from db.
val dbChapters = getChapterByMangaId.await(manga.id) val dbChapters = getChaptersByMangaId.await(manga.id)
// Chapters from the source not in db. // Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>() val toAdd = mutableListOf<Chapter>()
@ -116,7 +118,9 @@ class SyncChaptersWithSource(
} else { } else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) { if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(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) { if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter) downloadManager.renameChapter(source, manga, dbChapter, chapter)
@ -206,6 +210,10 @@ class SyncChaptersWithSource(
val reAddedUrls = reAdded.map { it.url }.toHashSet() 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.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import tachiyomi.data.Chapters
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
@ -23,18 +22,7 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
url = sChapter.url, url = sChapter.url,
dateUpload = sChapter.date_upload, dateUpload = sChapter.date_upload,
chapterNumber = sChapter.chapter_number.toDouble(), chapterNumber = sChapter.chapter_number.toDouble(),
scanlator = sChapter.scanlator?.ifBlank { null }, scanlator = sChapter.scanlator?.ifBlank { null }?.trim(),
)
}
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,
) )
} }

View File

@ -2,7 +2,7 @@ package eu.kanade.domain.chapter.model
import eu.kanade.domain.manga.model.downloadedFilter import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.tachiyomi.data.download.DownloadManager 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.model.Chapter
import tachiyomi.domain.chapter.service.getChapterSort import tachiyomi.domain.chapter.service.getChapterSort
import tachiyomi.domain.manga.model.Manga 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(bookmarkedFilter) { chapter.bookmark } }
.filter { chapter -> .filter { chapter ->
applyFilter(downloadedFilter) { 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 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. * Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted. * @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 isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter 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 package eu.kanade.domain.manga.interactor
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.manga.repository.MangaRepository
@ -9,22 +9,22 @@ class SetMangaViewerFlags(
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
) { ) {
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) { suspend fun awaitSetReadingMode(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id) val manga = mangaRepository.getMangaById(id)
mangaRepository.update( mangaRepository.update(
MangaUpdate( MangaUpdate(
id = id, 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) val manga = mangaRepository.getMangaById(id)
mangaRepository.update( mangaRepository.update(
MangaUpdate( MangaUpdate(
id = id, 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.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
@ -14,11 +14,11 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
// TODO: move these into the domain model // TODO: move these into the domain model
val Manga.readingModeType: Long val Manga.readingMode: Long
get() = viewerFlags and ReadingModeType.MASK.toLong() get() = viewerFlags and ReadingMode.MASK.toLong()
val Manga.orientationType: Long val Manga.readerOrientation: Long
get() = viewerFlags and OrientationType.MASK.toLong() get() = viewerFlags and ReaderOrientation.MASK.toLong()
val Manga.downloadedFilter: TriState val Manga.downloadedFilter: TriState
get() { get() {

View File

@ -3,12 +3,11 @@ package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import tachiyomi.core.util.lang.compareToWithCollator
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.repository.SourceRepository import tachiyomi.domain.source.repository.SourceRepository
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import java.text.Collator
import java.util.Collections import java.util.Collections
import java.util.Locale
class GetSourcesWithFavoriteCount( class GetSourcesWithFavoriteCount(
private val repository: SourceRepository, private val repository: SourceRepository,
@ -31,17 +30,13 @@ class GetSourcesWithFavoriteCount(
direction: SetMigrateSorting.Direction, direction: SetMigrateSorting.Direction,
sorting: SetMigrateSorting.Mode, sorting: SetMigrateSorting.Mode,
): java.util.Comparator<Pair<Source, Long>> { ): 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 -> val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b ->
when (sorting) { when (sorting) {
SetMigrateSorting.Mode.ALPHABETICAL -> { SetMigrateSorting.Mode.ALPHABETICAL -> {
when { when {
a.first.isStub && b.first.isStub.not() -> -1 a.first.isStub && b.first.isStub.not() -> -1
b.first.isStub && a.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 -> { 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.domain.source.interactor.SetMigrateSorting
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.getEnum import tachiyomi.core.preference.getEnum
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
@ -10,7 +11,12 @@ class SourcePreferences(
private val preferenceStore: PreferenceStore, 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()) fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
@ -18,17 +24,23 @@ class SourcePreferences(
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) 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 showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL) 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 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) fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
} }

View File

@ -1,23 +1,84 @@
package eu.kanade.domain.track.interactor package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack 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.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withNonCancellableContext import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.system.logcat 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.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZoneOffset
class AddTracks( class AddTracks(
private val getTracks: GetTracks, private val getTracks: GetTracks,
private val insertTrack: InsertTrack, private val insertTrack: InsertTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, 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) getTracks.await(manga.id)
.filterIsInstance<EnhancedTracker>() .filterIsInstance<EnhancedTracker>()
.filter { it.accept(source) } .filter { it.accept(source) }
@ -43,3 +104,4 @@ class AddTracks(
} }
} }
} }
}

View File

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

View File

@ -43,7 +43,9 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
track?.copy(lastChapterRead = it.lastChapterRead.toDouble()) track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
} }
.forEach { track -> .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) 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.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons 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.Public
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
@ -79,7 +79,7 @@ fun BrowseSourceContent(
listOf( listOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.local_source_help_guide, stringResId = R.string.local_source_help_guide,
icon = Icons.AutoMirrored.Outlined.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick, onClick = onLocalSourceHelpClick,
), ),
) )
@ -97,7 +97,7 @@ fun BrowseSourceContent(
), ),
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.label_help, stringResId = R.string.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = onHelpClick, onClick = onHelpClick,
), ),
) )

View File

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

View File

@ -74,7 +74,9 @@ internal fun GlobalSearchContent(
items.forEach { (source, result) -> items.forEach { (source, result) ->
item(key = source.id) { item(key = source.id) {
GlobalSearchResultItem( 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), subtitle = LocaleHelper.getDisplayName(source.lang),
onClick = { onClickSource(source) }, onClick = { onClickSource(source) },
) { ) {

View File

@ -102,14 +102,26 @@ private fun MigrateSourceList(
IconButton(onClick = onToggleSortingMode) { IconButton(onClick = onToggleSortingMode) {
when (sortingMode) { when (sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(Icons.Outlined.SortByAlpha, contentDescription = stringResource(R.string.action_sort_alpha)) SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
SetMigrateSorting.Mode.TOTAL -> Icon(Icons.Outlined.Numbers, contentDescription = stringResource(R.string.action_sort_count)) 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) { IconButton(onClick = onToggleSortingDirection) {
when (sortingDirection) { when (sortingDirection) {
SetMigrateSorting.Direction.ASCENDING -> Icon(Icons.Outlined.ArrowUpward, contentDescription = stringResource(R.string.action_asc)) SetMigrateSorting.Direction.ASCENDING -> Icon(
SetMigrateSorting.Direction.DESCENDING -> Icon(Icons.Outlined.ArrowDownward, contentDescription = stringResource(R.string.action_desc)) 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, onClick: () -> Unit,
) { ) {
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin 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 val description = if (isPinned) R.string.action_unpin else R.string.action_pin
IconButton(onClick = onClick) { IconButton(onClick = onClick) {
Icon( Icon(

View File

@ -25,7 +25,9 @@ fun BaseSourceItem(
action: @Composable RowScope.(Source) -> Unit = {}, action: @Composable RowScope.(Source) -> Unit = {},
content: @Composable RowScope.(Source, String?) -> Unit = defaultContent, 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( BaseBrowseItem(
modifier = modifier, modifier = modifier,
onClickItem = onClickItem, onClickItem = onClickItem,

View File

@ -1,7 +1,7 @@
package eu.kanade.presentation.browse.components package eu.kanade.presentation.browse.components
import androidx.compose.material.icons.Icons 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.material.icons.filled.ViewModule
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
@ -56,7 +56,11 @@ fun BrowseSourceToolbar(
actions = listOfNotNull( actions = listOfNotNull(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_display_mode), 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 }, onClick = { selectingDisplayMode = true },
), ),
if (isLocalSource) { 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons 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.ArrowForward
import androidx.compose.material.icons.outlined.Error import androidx.compose.material.icons.outlined.Error
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -55,7 +54,7 @@ fun GlobalSearchResultItem(
Text(text = subtitle) Text(text = subtitle)
} }
IconButton(onClick = onClick) { IconButton(onClick = onClick) {
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
} }
} }
content() content()

View File

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

View File

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

View File

@ -74,7 +74,11 @@ fun CategoryCreateDialog(
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text(text = stringResource(R.string.name)) }, label = { Text(text = stringResource(R.string.name)) },
supportingText = { 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)) Text(text = stringResource(msgRes))
}, },
isError = name.isNotEmpty() && nameAlreadyExists, isError = name.isNotEmpty() && nameAlreadyExists,
@ -134,7 +138,11 @@ fun CategoryRenameDialog(
}, },
label = { Text(text = stringResource(R.string.name)) }, label = { Text(text = stringResource(R.string.name)) },
supportingText = { 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)) Text(text = stringResource(msgRes))
}, },
isError = valueHasChanged && nameAlreadyExists, isError = valueHasChanged && nameAlreadyExists,
@ -257,8 +265,12 @@ fun ChangeCategoryDialog(
onClick = { onClick = {
onDismissRequest() onDismissRequest()
onConfirm( onConfirm(
selection.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }.map { it.value.id }, selection
selection.filter { it is CheckboxState.State.None || it is CheckboxState.TriState.None }.map { it.value.id }, .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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.ArrowDropUp import androidx.compose.material.icons.outlined.ArrowDropUp
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
@ -50,7 +49,7 @@ fun CategoryListItem(
), ),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "") Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
Text( Text(
text = category.name, text = category.name,
modifier = Modifier modifier = Modifier
@ -72,7 +71,10 @@ fun CategoryListItem(
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) { 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) { IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(R.string.action_delete)) 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.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons 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.Close
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Search 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.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -367,7 +370,11 @@ fun SearchToolbar(
@Composable @Composable
fun UpIcon(navigationIcon: ImageVector? = null) { fun UpIcon(navigationIcon: ImageVector? = null) {
val icon = navigationIcon val icon = navigationIcon
?: Icons.AutoMirrored.Outlined.ArrowBack ?: if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
Icons.Outlined.ArrowBack
} else {
Icons.Outlined.ArrowForward
}
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = stringResource(R.string.abc_action_bar_up_description), 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.ColumnScope
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons 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.RadioButtonChecked
import androidx.compose.material.icons.outlined.RadioButtonUnchecked import androidx.compose.material.icons.outlined.RadioButtonUnchecked
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@ -15,8 +16,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -74,13 +77,14 @@ fun NestedMenuItem(
) { ) {
var nestedExpanded by remember { mutableStateOf(false) } var nestedExpanded by remember { mutableStateOf(false) }
val closeMenu = { nestedExpanded = false } val closeMenu = { nestedExpanded = false }
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
DropdownMenuItem( DropdownMenuItem(
text = text, text = text,
onClick = { nestedExpanded = true }, onClick = { nestedExpanded = true },
trailingIcon = { trailingIcon = {
Icon( Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowRight, imageVector = if (isLtr) Icons.Outlined.ArrowRight else Icons.Outlined.ArrowLeft,
contentDescription = null, contentDescription = null,
) )
}, },

View File

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

View File

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

View File

@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -67,7 +67,7 @@ fun TabbedScreen(
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
), ),
) { ) {
PrimaryTabRow( TabRow(
selectedTabIndex = state.currentPage, selectedTabIndex = state.currentPage,
indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) }, 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.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.screens.InfoScreen import tachiyomi.presentation.core.screens.InfoScreen
import tachiyomi.presentation.core.util.ThemePreviews
@Composable @Composable
fun CrashScreen( fun CrashScreen(
@ -60,7 +60,7 @@ fun CrashScreen(
} }
} }
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun CrashScreenPreview() { private fun CrashScreenPreview() {
TachiyomiTheme { TachiyomiTheme {

View File

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

View File

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

View File

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

View File

@ -144,6 +144,13 @@ private fun ColumnScope.SortPage(
val sortingMode = category.sort.type val sortingMode = category.sort.type
val sortDescending = !category.sort.isAscending 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( listOf(
R.string.action_sort_alpha to LibrarySort.Type.Alphabetical, R.string.action_sort_alpha to LibrarySort.Type.Alphabetical,
R.string.action_sort_total to LibrarySort.Type.TotalChapters, 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_latest_chapter to LibrarySort.Type.LatestChapter,
R.string.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate, R.string.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
R.string.action_sort_date_added to LibrarySort.Type.DateAdded, R.string.action_sort_date_added to LibrarySort.Type.DateAdded,
).map { (titleRes, mode) -> ).plus(trackerSortOption).map { (titleRes, mode) ->
SortItem( SortItem(
label = stringResource(titleRes), label = stringResource(titleRes),
sortDescending = sortDescending.takeIf { sortingMode == mode }, sortDescending = sortDescending.takeIf { sortingMode == mode },
onClick = { onClick = {
val isTogglingDirection = sortingMode == mode val isTogglingDirection = sortingMode == mode
val direction = when { val direction = when {
isTogglingDirection -> if (sortDescending) LibrarySort.Direction.Ascending else LibrarySort.Direction.Descending isTogglingDirection -> if (sortDescending) {
else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending LibrarySort.Direction.Ascending
} else {
LibrarySort.Direction.Descending
}
else -> if (sortDescending) {
LibrarySort.Direction.Descending
} else {
LibrarySort.Direction.Ascending
}
} }
screenModel.setSort(category, mode, direction) 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.material.icons.outlined.Folder
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewLightDark
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.presentation.core.components.Badge import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.util.ThemePreviews
@Composable @Composable
internal fun DownloadsBadge(count: Long) { internal fun DownloadsBadge(count: Long) {
@ -47,7 +47,7 @@ internal fun LanguageBadge(
} }
} }
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun BadgePreview() { private fun BadgePreview() {
TachiyomiTheme { TachiyomiTheme {

View File

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

View File

@ -1,13 +1,21 @@
package eu.kanade.presentation.manga package eu.kanade.presentation.manga
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope 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.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll 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.AlertDialog
import androidx.compose.material3.DropdownMenuItem 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.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -15,6 +23,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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.RadioItem
import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.theme.active
@Composable @Composable
fun ChapterSettingsDialog( fun ChapterSettingsDialog(
@ -37,9 +47,12 @@ fun ChapterSettingsDialog(
onDownloadFilterChanged: (TriState) -> Unit, onDownloadFilterChanged: (TriState) -> Unit,
onUnreadFilterChanged: (TriState) -> Unit, onUnreadFilterChanged: (TriState) -> Unit,
onBookmarkedFilterChanged: (TriState) -> Unit, onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
onSortModeChanged: (Long) -> Unit, onSortModeChanged: (Long) -> Unit,
onDisplayModeChanged: (Long) -> Unit, onDisplayModeChanged: (Long) -> Unit,
onSetAsDefault: (applyToExistingManga: Boolean) -> Unit, onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
onResetToDefault: () -> Unit,
) { ) {
var showSetAsDefaultDialog by rememberSaveable { mutableStateOf(false) } var showSetAsDefaultDialog by rememberSaveable { mutableStateOf(false) }
if (showSetAsDefaultDialog) { if (showSetAsDefaultDialog) {
@ -64,6 +77,13 @@ fun ChapterSettingsDialog(
closeMenu() closeMenu()
}, },
) )
DropdownMenuItem(
text = { Text(stringResource(R.string.action_reset)) },
onClick = {
onResetToDefault()
closeMenu()
},
)
}, },
) { page -> ) { page ->
Column( Column(
@ -75,11 +95,14 @@ fun ChapterSettingsDialog(
0 -> { 0 -> {
FilterPage( FilterPage(
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED, downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { manga?.forceDownloaded() == true }, onDownloadFilterChanged = onDownloadFilterChanged
.takeUnless { manga?.forceDownloaded() == true },
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED, unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
onUnreadFilterChanged = onUnreadFilterChanged, onUnreadFilterChanged = onUnreadFilterChanged,
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED, bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
onBookmarkedFilterChanged = onBookmarkedFilterChanged, onBookmarkedFilterChanged = onBookmarkedFilterChanged,
scanlatorFilterActive = scanlatorFilterActive,
onScanlatorFilterClicked = onScanlatorFilterClicked,
) )
} }
1 -> { 1 -> {
@ -108,6 +131,8 @@ private fun ColumnScope.FilterPage(
onUnreadFilterChanged: (TriState) -> Unit, onUnreadFilterChanged: (TriState) -> Unit,
bookmarkedFilter: TriState, bookmarkedFilter: TriState,
onBookmarkedFilterChanged: (TriState) -> Unit, onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
) { ) {
TriStateItem( TriStateItem(
label = stringResource(R.string.label_downloaded), label = stringResource(R.string.label_downloaded),
@ -124,6 +149,39 @@ private fun ColumnScope.FilterPage(
state = bookmarkedFilter, state = bookmarkedFilter,
onClick = onBookmarkedFilterChanged, 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 @Composable
@ -136,6 +194,7 @@ private fun ColumnScope.SortPage(
R.string.sort_by_source to Manga.CHAPTER_SORTING_SOURCE, R.string.sort_by_source to Manga.CHAPTER_SORTING_SOURCE,
R.string.sort_by_number to Manga.CHAPTER_SORTING_NUMBER, R.string.sort_by_number to Manga.CHAPTER_SORTING_NUMBER,
R.string.sort_by_upload_date to Manga.CHAPTER_SORTING_UPLOAD_DATE, 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) -> ).map { (titleRes, mode) ->
SortItem( SortItem(
label = stringResource(titleRes), 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.fastAll
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap 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.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterHeader import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription 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.MangaChapterListItem
import eu.kanade.presentation.manga.components.MangaInfoBox import eu.kanade.presentation.manga.components.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaToolbar import eu.kanade.presentation.manga.components.MangaToolbar
import eu.kanade.presentation.manga.components.MissingChapterCountListItem
import eu.kanade.presentation.util.formatChapterNumber import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.manga.ChapterItem import eu.kanade.tachiyomi.ui.manga.ChapterList
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
@ -92,7 +92,7 @@ fun MangaScreen(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
@ -123,10 +123,10 @@ fun MangaScreen(
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For chapter swipe // For chapter swipe
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection // Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
) { ) {
@ -225,7 +225,7 @@ private fun MangaScreenSmallImpl(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
@ -257,16 +257,17 @@ private fun MangaScreenSmallImpl(
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For chapter swipe // For chapter swipe
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection // Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
) { ) {
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
val chapters = remember(state) { state.processedChapters } val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember { val isAnySelected by remember {
derivedStateOf { derivedStateOf {
@ -306,7 +307,7 @@ private fun MangaScreenSmallImpl(
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha }, titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha }, backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.manga.chaptersFiltered(), hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed, onBackClicked = internalOnBackPressed,
onClickFilter = onFilterClicked, onClickFilter = onFilterClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
@ -447,7 +448,8 @@ private fun MangaScreenSmallImpl(
sharedChapterItems( sharedChapterItems(
manga = state.manga, manga = state.manga,
chapters = chapters, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
@ -474,7 +476,7 @@ fun MangaScreenLargeImpl(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
@ -506,10 +508,10 @@ fun MangaScreenLargeImpl(
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For swipe actions // For swipe actions
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection // Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
) { ) {
@ -517,6 +519,7 @@ fun MangaScreenLargeImpl(
val density = LocalDensity.current val density = LocalDensity.current
val chapters = remember(state) { state.processedChapters } val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember { val isAnySelected by remember {
derivedStateOf { derivedStateOf {
@ -557,7 +560,7 @@ fun MangaScreenLargeImpl(
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f }, titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f }, backgroundAlphaProvider = { 1f },
hasFilters = state.manga.chaptersFiltered(), hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed, onBackClicked = internalOnBackPressed,
onClickFilter = onFilterButtonClicked, onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
@ -604,7 +607,9 @@ fun MangaScreenLargeImpl(
val isReading = remember(state.chapters) { val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read } 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) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading, onClick = onContinueReading,
@ -688,7 +693,8 @@ fun MangaScreenLargeImpl(
sharedChapterItems( sharedChapterItems(
manga = state.manga, manga = state.manga,
chapters = chapters, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
@ -708,12 +714,12 @@ fun MangaScreenLargeImpl(
@Composable @Composable
private fun SharedMangaBottomActionMenu( private fun SharedMangaBottomActionMenu(
selected: List<ChapterItem>, selected: List<ChapterList.Item>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
fillFraction: Float, fillFraction: Float,
) { ) {
@ -750,34 +756,45 @@ private fun SharedMangaBottomActionMenu(
private fun LazyListScope.sharedChapterItems( private fun LazyListScope.sharedChapterItems(
manga: Manga, manga: Manga,
chapters: List<ChapterItem>, chapters: List<ChapterList>,
isAnyChapterSelected: Boolean,
dateRelativeTime: Boolean, dateRelativeTime: Boolean,
dateFormat: DateFormat, dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
) { ) {
items( items(
items = chapters, 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 }, contentType = { MangaScreenItem.CHAPTER },
) { chapterItem -> ) { item ->
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
when (item) {
is ChapterList.MissingCount -> {
MissingChapterCountListItem(count = item.count)
}
is ChapterList.Item -> {
MangaChapterListItem( MangaChapterListItem(
title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {
stringResource( stringResource(
R.string.display_mode_chapter, R.string.display_mode_chapter,
formatChapterNumber(chapterItem.chapter.chapterNumber), formatChapterNumber(item.chapter.chapterNumber),
) )
} else { } else {
chapterItem.chapter.name item.chapter.name
}, },
date = chapterItem.chapter.dateUpload date = item.chapter.dateUpload
.takeIf { it > 0L } .takeIf { it > 0L }
?.let { ?.let {
Date(it).toRelativeString( Date(it).toRelativeString(
@ -786,56 +803,58 @@ private fun LazyListScope.sharedChapterItems(
dateFormat, dateFormat,
) )
}, },
readProgress = chapterItem.chapter.lastPageRead readProgress = item.chapter.lastPageRead
.takeIf { !chapterItem.chapter.read && it > 0L } .takeIf { !item.chapter.read && it > 0L }
?.let { ?.let {
stringResource( stringResource(
R.string.chapter_progress, R.string.chapter_progress,
it + 1, it + 1,
) )
}, },
scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() }, scanlator = item.chapter.scanlator.takeIf { !it.isNullOrBlank() },
read = chapterItem.chapter.read, read = item.chapter.read,
bookmark = chapterItem.chapter.bookmark, bookmark = item.chapter.bookmark,
selected = chapterItem.selected, selected = item.selected,
downloadIndicatorEnabled = chapters.fastAll { !it.selected }, downloadIndicatorEnabled = !isAnyChapterSelected,
downloadStateProvider = { chapterItem.downloadState }, downloadStateProvider = { item.downloadState },
downloadProgressProvider = { chapterItem.downloadProgress }, downloadProgressProvider = { item.downloadProgress },
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
onLongClick = { onLongClick = {
onChapterSelected(chapterItem, !chapterItem.selected, true, true) onChapterSelected(item, !item.selected, true, true)
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}, },
onClick = { onClick = {
onChapterItemClick( onChapterItemClick(
chapterItem = chapterItem, chapterItem = item,
chapters = chapters, isAnyChapterSelected = isAnyChapterSelected,
onToggleSelection = { onChapterSelected(chapterItem, !chapterItem.selected, true, false) }, onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
) )
}, },
onDownloadClick = if (onDownloadChapter != null) { onDownloadClick = if (onDownloadChapter != null) {
{ onDownloadChapter(listOf(chapterItem), it) } { onDownloadChapter(listOf(item), it) }
} else { } else {
null null
}, },
onChapterSwipe = { onChapterSwipe = {
onChapterSwipe(chapterItem, it) onChapterSwipe(item, it)
}, },
) )
} }
} }
}
}
private fun onChapterItemClick( private fun onChapterItemClick(
chapterItem: ChapterItem, chapterItem: ChapterList.Item,
chapters: List<ChapterItem>, isAnyChapterSelected: Boolean,
onToggleSelection: (Boolean) -> Unit, onToggleSelection: (Boolean) -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
) { ) {
when { when {
chapterItem.selected -> onToggleSelection(false) chapterItem.selected -> onToggleSelection(false)
chapters.fastAny { it.selected } -> onToggleSelection(true) isAnyChapterSelected -> onToggleSelection(true)
else -> onChapterClicked(chapterItem.chapter) else -> onChapterClicked(chapterItem.chapter)
} }
} }

View File

@ -148,7 +148,7 @@ private fun DownloadingIndicator(
MaterialTheme.colorScheme.background MaterialTheme.colorScheme.background
} }
CircularProgressIndicator( CircularProgressIndicator(
progress = { animatedProgress }, progress = animatedProgress,
modifier = IndicatorModifier, modifier = IndicatorModifier,
color = strokeColor, color = strokeColor,
strokeWidth = IndicatorSize / 2, 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.layout.windowInsetsPadding
import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons 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.BookmarkAdd
import androidx.compose.material.icons.outlined.BookmarkRemove import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
@ -259,7 +258,7 @@ fun LibraryBottomActionMenu(
) { ) {
Button( Button(
title = stringResource(R.string.action_move_category), title = stringResource(R.string.action_move_category),
icon = Icons.AutoMirrored.Outlined.Label, icon = Icons.Outlined.Label,
toConfirm = confirm[0], toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) }, onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked, onClick = onChangeCategoryClicked,

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import android.content.Context
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.graphics.res.animatedVectorResource 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.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons 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.Favorite
import androidx.compose.material.icons.filled.HourglassEmpty 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.filled.Warning
import androidx.compose.material.icons.outlined.AttachMoney import androidx.compose.material.icons.outlined.AttachMoney
import androidx.compose.material.icons.outlined.Block 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.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChip
@ -132,7 +134,6 @@ fun MangaInfoBox(
coverDataProvider = coverDataProvider, coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick, onCoverClick = onCoverClick,
title = title, title = title,
context = LocalContext.current,
doSearch = doSearch, doSearch = doSearch,
author = author, author = author,
artist = artist, artist = artist,
@ -146,7 +147,6 @@ fun MangaInfoBox(
coverDataProvider = coverDataProvider, coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick, onCoverClick = onCoverClick,
title = title, title = title,
context = LocalContext.current,
doSearch = doSearch, doSearch = doSearch,
author = author, author = author,
artist = artist, artist = artist,
@ -189,7 +189,11 @@ fun MangaActionRow(
) )
if (onEditIntervalClicked != null && fetchInterval != null) { if (onEditIntervalClicked != null && fetchInterval != null) {
MangaActionButton( 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, icon = Icons.Default.HourglassEmpty,
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor, color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onEditIntervalClicked, onClick = onEditIntervalClicked,
@ -321,7 +325,6 @@ private fun MangaAndSourceTitlesLarge(
coverDataProvider: () -> Manga, coverDataProvider: () -> Manga,
onCoverClick: () -> Unit, onCoverClick: () -> Unit,
title: String, title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
author: String?, author: String?,
artist: String?, artist: String?,
@ -342,102 +345,16 @@ private fun MangaAndSourceTitlesLarge(
onClick = onCoverClick, onClick = onCoverClick,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( MangaContentInfo(
text = title.ifBlank { stringResource(R.string.unknown_title) }, title = title,
style = MaterialTheme.typography.titleLarge, doSearch = doSearch,
modifier = Modifier.clickableNoIndication( author = author,
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) }, artist = artist,
onClick = { if (title.isNotBlank()) doSearch(title, true) }, status = status,
), sourceName = sourceName,
isStubSource = isStubSource,
textAlign = TextAlign.Center, 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, coverDataProvider: () -> Manga,
onCoverClick: () -> Unit, onCoverClick: () -> Unit,
title: String, title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
author: String?, author: String?,
artist: String?, artist: String?,
@ -459,6 +375,7 @@ private fun MangaAndSourceTitlesSmall(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp), .padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
MangaCover.Book( MangaCover.Book(
@ -469,7 +386,34 @@ private fun MangaAndSourceTitlesSmall(
contentDescription = stringResource(R.string.manga_cover), contentDescription = stringResource(R.string.manga_cover),
onClick = onCoverClick, 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(
text = title.ifBlank { stringResource(R.string.unknown_title) }, text = title.ifBlank { stringResource(R.string.unknown_title) },
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@ -484,15 +428,26 @@ private fun MangaAndSourceTitlesSmall(
}, },
onClick = { if (title.isNotBlank()) doSearch(title, true) }, onClick = { if (title.isNotBlank()) doSearch(title, true) },
), ),
textAlign = textAlign,
) )
Spacer(modifier = Modifier.height(2.dp)) 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(
text = author?.takeIf { it.isNotBlank() } text = author?.takeIf { it.isNotBlank() }
?: stringResource(R.string.unknown_author), ?: stringResource(R.string.unknown_author),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier modifier = Modifier
.secondaryItemAlpha()
.padding(top = 2.dp)
.clickableNoIndication( .clickableNoIndication(
onLongClick = { onLongClick = {
if (!author.isNullOrBlank()) { if (!author.isNullOrBlank()) {
@ -504,21 +459,36 @@ private fun MangaAndSourceTitlesSmall(
}, },
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
), ),
textAlign = textAlign,
) )
}
if (!artist.isNullOrBlank() && author != artist) { 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(
text = artist, text = artist,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier modifier = Modifier
.secondaryItemAlpha()
.padding(top = 2.dp)
.clickableNoIndication( .clickableNoIndication(
onLongClick = { context.copyToClipboard(artist, artist) }, onLongClick = { context.copyToClipboard(artist, artist) },
onClick = { doSearch(artist, true) }, onClick = { doSearch(artist, true) },
), ),
textAlign = textAlign,
) )
} }
Spacer(modifier = Modifier.height(4.dp)) }
Spacer(modifier = Modifier.height(2.dp))
Row( Row(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -577,8 +547,6 @@ private fun MangaAndSourceTitlesSmall(
} }
} }
} }
}
}
@Composable @Composable
private fun MangaSummary( private fun MangaSummary(
@ -623,7 +591,9 @@ private fun MangaSummary(
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down) val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
Icon( Icon(
painter = rememberAnimatedVectorPainter(image, !expanded), 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, tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())), 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.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons 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.CloudOff
import androidx.compose.material.icons.outlined.GetApp import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.HelpOutline 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.Label
import androidx.compose.material.icons.outlined.QueryStats import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Settings 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.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -47,7 +45,7 @@ fun MoreScreen(
onClickDownloadQueue: () -> Unit, onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit, onClickCategories: () -> Unit,
onClickStats: () -> Unit, onClickStats: () -> Unit,
onClickBackupAndRestore: () -> Unit, onClickDataAndStorage: () -> Unit,
onClickSettings: () -> Unit, onClickSettings: () -> Unit,
onClickAbout: () -> Unit, onClickAbout: () -> Unit,
) { ) {
@ -64,7 +62,9 @@ fun MoreScreen(
WarningBanner( WarningBanner(
textRes = R.string.fdroid_warning, textRes = R.string.fdroid_warning,
modifier = Modifier.clickable { modifier = Modifier.clickable {
uriHandler.openUri("https://tachiyomi.org/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 { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(R.string.categories), title = stringResource(R.string.categories),
icon = Icons.AutoMirrored.Outlined.Label, icon = Icons.Outlined.Label,
onPreferenceClick = onClickCategories, onPreferenceClick = onClickCategories,
) )
} }
@ -144,8 +144,8 @@ fun MoreScreen(
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(R.string.label_backup_and_sync), title = stringResource(R.string.label_backup_and_sync),
icon = Icons.Outlined.SettingsBackupRestore, icon = Icons.Outlined.Storage,
onPreferenceClick = onClickBackupAndRestore, onPreferenceClick = onClickDataAndStorage,
) )
} }
@ -168,7 +168,7 @@ fun MoreScreen(
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(R.string.label_help), title = stringResource(R.string.label_help),
icon = Icons.AutoMirrored.Outlined.HelpOutline, icon = Icons.Outlined.HelpOutline,
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) }, 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.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons 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.filled.OpenInNew
import androidx.compose.material.icons.outlined.NewReleases import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -16,6 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.Material3RichText import com.halilibo.richtext.ui.material3.Material3RichText
@ -24,7 +24,6 @@ import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.screens.InfoScreen import tachiyomi.presentation.core.screens.InfoScreen
import tachiyomi.presentation.core.util.ThemePreviews
@Composable @Composable
fun NewUpdateScreen( fun NewUpdateScreen(
@ -61,13 +60,13 @@ fun NewUpdateScreen(
) { ) {
Text(text = stringResource(R.string.update_check_open)) Text(text = stringResource(R.string.update_check_open))
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny)) 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 @Composable
private fun NewUpdateScreenPreview() { private fun NewUpdateScreenPreview() {
TachiyomiTheme { TachiyomiTheme {

View File

@ -31,7 +31,8 @@ fun getCategoriesLabel(
val includedItemsText = when { val includedItemsText = when {
// Some selected, but not all // 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 // All explicitly selected
includedCategories.size == allCategories.size -> stringResource(R.string.all) includedCategories.size == allCategories.size -> stringResource(R.string.all)
allExcluded -> stringResource(R.string.none) 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.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.advanced.ClearDatabaseScreen
import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
@ -61,8 +59,7 @@ import okhttp3.Headers
import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -183,40 +180,12 @@ object SettingsAdvancedScreen : SearchableSettings {
@Composable @Composable
private fun getDataGroup(): Preference.PreferenceGroup { private fun getDataGroup(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow 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( return Preference.PreferenceGroup(
title = stringResource(R.string.label_data), title = stringResource(R.string.label_data),
preferenceItems = listOf( 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( Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_invalidate_download_cache), title = stringResource(R.string.pref_invalidate_download_cache),
subtitle = stringResource(R.string.pref_invalidate_download_cache_summary), 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), subtitle = stringResource(R.string.pref_reset_viewer_flags_summary),
onClick = { onClick = {
scope.launchNonCancellable { scope.launchNonCancellable {
val success = Injekt.get<MangaRepository>().resetViewerFlags() val success = Injekt.get<ResetViewerFlags>().await()
withUIContext { withUIContext {
val message = if (success) { val message = if (success) {
R.string.pref_reset_viewer_flags_success R.string.pref_reset_viewer_flags_success

View File

@ -120,7 +120,9 @@ object SettingsAppearanceScreen : SearchableSettings {
uiPreferences: UiPreferences, uiPreferences: UiPreferences,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
val langs = remember { getLangs(context) } 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 now = remember { Date().time }
val dateFormat by uiPreferences.dateFormat().collectAsState() 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(), pref = libraryPreferences.swipeToStartAction(),
title = stringResource(R.string.pref_chapter_swipe_start), title = stringResource(R.string.pref_chapter_swipe_start),
entries = mapOf( entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.disabled), LibraryPreferences.ChapterSwipeAction.Disabled to
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark), stringResource(R.string.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(R.string.action_mark_as_read), LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
LibraryPreferences.ChapterSwipeAction.Download to stringResource(R.string.action_download), 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( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToEndAction(), pref = libraryPreferences.swipeToEndAction(),
title = stringResource(R.string.pref_chapter_swipe_end), title = stringResource(R.string.pref_chapter_swipe_end),
entries = mapOf( entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.disabled), LibraryPreferences.ChapterSwipeAction.Disabled to
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark), stringResource(R.string.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(R.string.action_mark_as_read), LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
LibraryPreferences.ChapterSwipeAction.Download to stringResource(R.string.action_download), 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.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.ChromeReaderMode
import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.CollectionsBookmark 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.Palette
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Security 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.material.icons.outlined.Sync
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -187,7 +186,7 @@ object SettingsMainScreen : Screen() {
Item( Item(
titleRes = R.string.pref_category_reader, titleRes = R.string.pref_category_reader,
subtitleRes = R.string.pref_reader_summary, subtitleRes = R.string.pref_reader_summary,
icon = Icons.AutoMirrored.Outlined.ChromeReaderMode, icon = Icons.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen, screen = SettingsReaderScreen,
), ),
Item( Item(
@ -210,9 +209,9 @@ object SettingsMainScreen : Screen() {
), ),
Item( Item(
titleRes = R.string.label_backup_and_sync, titleRes = R.string.label_backup_and_sync,
subtitleRes = R.string.pref_backup_and_sync_summary, subtitleRes = R.string.pref_backup_summary,
icon = Icons.Outlined.SettingsBackupRestore, icon = Icons.Outlined.Storage,
screen = SettingsBackupAndSyncScreen, screen = SettingsDataScreen,
), ),
Item( Item(
titleRes = R.string.pref_category_security, 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 androidx.compose.ui.res.stringResource
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences 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 tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -32,7 +32,7 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPref.defaultReadingMode(), pref = readerPref.defaultReadingMode(),
title = stringResource(R.string.pref_viewer_type), title = stringResource(R.string.pref_viewer_type),
entries = ReadingModeType.entries.drop(1) entries = ReadingMode.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) }, .associate { it.flagValue to stringResource(it.stringRes) },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
@ -64,6 +64,11 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPref.pageTransitions(), pref = readerPref.pageTransitions(),
title = stringResource(R.string.pref_page_transitions), 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), getDisplayGroup(readerPreferences = readerPref),
getReadingGroup(readerPreferences = readerPref), getReadingGroup(readerPreferences = readerPref),
getPagedGroup(readerPreferences = readerPref), getPagedGroup(readerPreferences = readerPref),
@ -83,7 +88,7 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.defaultOrientationType(), pref = readerPreferences.defaultOrientationType(),
title = stringResource(R.string.pref_rotation_type), title = stringResource(R.string.pref_rotation_type),
entries = OrientationType.entries.drop(1) entries = ReaderOrientation.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) }, .associate { it.flagValue to stringResource(it.stringRes) },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
@ -169,12 +174,12 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pagerNavInverted(), pref = readerPreferences.pagerNavInverted(),
title = stringResource(R.string.pref_read_with_tapping_inverted), title = stringResource(R.string.pref_read_with_tapping_inverted),
entries = mapOf( entries = listOf(
ReaderPreferences.TappingInvertMode.NONE to stringResource(R.string.none), ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(R.string.tapping_inverted_horizontal), ReaderPreferences.TappingInvertMode.HORIZONTAL,
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(R.string.tapping_inverted_vertical), ReaderPreferences.TappingInvertMode.VERTICAL,
ReaderPreferences.TappingInvertMode.BOTH to stringResource(R.string.tapping_inverted_both), ReaderPreferences.TappingInvertMode.BOTH,
), ).associateWith { stringResource(it.titleResId) },
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
@ -261,12 +266,12 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonNavInverted(), pref = readerPreferences.webtoonNavInverted(),
title = stringResource(R.string.pref_read_with_tapping_inverted), title = stringResource(R.string.pref_read_with_tapping_inverted),
entries = mapOf( entries = listOf(
ReaderPreferences.TappingInvertMode.NONE to stringResource(R.string.none), ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(R.string.tapping_inverted_horizontal), ReaderPreferences.TappingInvertMode.HORIZONTAL,
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(R.string.tapping_inverted_vertical), ReaderPreferences.TappingInvertMode.VERTICAL,
ReaderPreferences.TappingInvertMode.BOTH to stringResource(R.string.tapping_inverted_both), ReaderPreferences.TappingInvertMode.BOTH,
), ).associateWith { stringResource(it.titleResId) },
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
@ -342,6 +347,11 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPreferences.readWithLongTap(), pref = readerPreferences.readWithLongTap(),
title = stringResource(R.string.pref_read_with_long_tap), 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( SearchResultItem(
route = settingsData.route, route = settingsData.route,
title = p.title, title = p.title,
breadcrumbs = getLocalizedBreadcrumb(path = settingsData.title, node = categoryTitle, isLtr = isLtr), breadcrumbs = getLocalizedBreadcrumb(
path = settingsData.title,
node = categoryTitle,
isLtr = isLtr,
),
highlightKey = p.title, highlightKey = p.title,
) )
} }
@ -291,7 +295,7 @@ private val settingScreens = listOf(
SettingsDownloadScreen, SettingsDownloadScreen,
SettingsTrackingScreen, SettingsTrackingScreen,
SettingsBrowseScreen, SettingsBrowseScreen,
SettingsBackupAndSyncScreen, SettingsDataScreen,
SettingsSecurityScreen, SettingsSecurityScreen,
SettingsAdvancedScreen, SettingsAdvancedScreen,
) )

View File

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

View File

@ -228,6 +228,9 @@ object AboutScreen : Screen() {
is GetApplicationRelease.Result.NoNewUpdate -> { is GetApplicationRelease.Result.NoNewUpdate -> {
context.toast(R.string.update_check_no_new_updates) context.toast(R.string.update_check_no_new_updates)
} }
is GetApplicationRelease.Result.OsTooOld -> {
context.toast(R.string.update_check_eol)
}
else -> {} else -> {}
} }
} catch (e: Exception) { } 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) { value = when (result) {
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed" ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed"
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled" 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_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE, ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE,
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST, ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,

View File

@ -115,7 +115,9 @@ class WorkerInfoScreen : Screen() {
private val workManager = context.workManager private val workManager = context.workManager
val finished = 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() .asFlow()
.map(::constructString) .map(::constructString)
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") .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.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.presentation.manga.components.MangaCover 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.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
@ -249,11 +249,12 @@ fun AppThemePreviewItem(
} }
} }
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun AppThemesListPreview() { private fun AppThemesListPreview() {
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) } var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
TachiyomiTheme { TachiyomiTheme {
Surface {
AppThemesList( AppThemesList(
currentTheme = appTheme, currentTheme = appTheme,
amoled = false, amoled = false,
@ -261,3 +262,4 @@ private fun AppThemesListPreview() {
) )
} }
} }
}

View File

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

View File

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

View File

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

View File

@ -115,8 +115,16 @@ fun <T> TriStateListDialog(
} }
} }
if (!listState.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) if (!listState.isScrolledToStart()) {
if (!listState.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) 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.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.icons.Icons 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.Info
import androidx.compose.material.icons.outlined.OfflinePin
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.CardColors import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults 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.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiTheme 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.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import tachiyomi.domain.chapter.service.calculateChapterGap import tachiyomi.domain.chapter.service.calculateChapterGap
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
@ -244,7 +244,7 @@ private fun ChapterText(
), ),
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.OfflinePin, imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.label_downloaded), contentDescription = stringResource(R.string.label_downloaded),
) )
}, },
@ -304,7 +304,7 @@ private val FakeChapterLongTitle = previewChapter(
chapterNumber = 1f, chapterNumber = 1f,
) )
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun TransitionTextPreview() { private fun TransitionTextPreview() {
TachiyomiTheme { TachiyomiTheme {
@ -318,7 +318,7 @@ private fun TransitionTextPreview() {
} }
} }
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun TransitionTextLongTitlePreview() { private fun TransitionTextLongTitlePreview() {
TachiyomiTheme { TachiyomiTheme {
@ -332,7 +332,7 @@ private fun TransitionTextLongTitlePreview() {
} }
} }
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun TransitionTextWithGapPreview() { private fun TransitionTextWithGapPreview() {
TachiyomiTheme { TachiyomiTheme {
@ -346,7 +346,7 @@ private fun TransitionTextWithGapPreview() {
} }
} }
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun TransitionTextNoNextPreview() { private fun TransitionTextNoNextPreview() {
TachiyomiTheme { TachiyomiTheme {
@ -360,7 +360,7 @@ private fun TransitionTextNoNextPreview() {
} }
} }
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun TransitionTextNoPreviousPreview() { private fun TransitionTextNoPreviousPreview() {
TachiyomiTheme { 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.foundation.layout.Box
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiTheme
@Composable @Composable
fun PageIndicatorText( fun PageIndicatorText(
@ -19,24 +23,37 @@ fun PageIndicatorText(
val text = "$currentPage / $totalPages" val text = "$currentPage / $totalPages"
Box { val style = TextStyle(
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,
color = Color(235, 235, 235), color = Color(235, 235, 235),
fontSize = MaterialTheme.typography.bodySmall.fontSize, fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
letterSpacing = 1.sp, 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 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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.items 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource 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.components.AdaptiveSheet
import eu.kanade.presentation.reader.components.ModeSelectionDialog
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.presentation.core.components.SettingsIconGrid import tachiyomi.presentation.core.components.SettingsIconGrid
import tachiyomi.presentation.core.components.material.IconToggleButton 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 @Composable
fun ReadingModeSelectDialog( fun ReadingModeSelectDialog(
@ -31,25 +34,63 @@ fun ReadingModeSelectDialog(
onChange: (Int) -> Unit, onChange: (Int) -> Unit,
) { ) {
val manga by screenModel.mangaFlow.collectAsState() val manga by screenModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) } val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
AdaptiveSheet(onDismissRequest = onDismissRequest) { AdaptiveSheet(onDismissRequest = onDismissRequest) {
Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) { DialogContent(
SettingsIconGrid(R.string.pref_category_reading_mode) { readingMode = readingMode,
items(readingModeOptions) { (stringRes, mode) -> onChangeReadingMode = {
IconToggleButton( screenModel.onChangeReadingMode(it)
checked = mode == readingMode, onChange(it.stringRes)
onCheckedChange = {
screenModel.onChangeReadingMode(mode)
onChange(stringRes)
onDismissRequest() 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(), modifier = Modifier.fillMaxWidth(),
imageVector = ImageVector.vectorResource(mode.iconRes), 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
@Composable @Composable
fun BottomReaderBar( fun BottomReaderBar(
backgroundColor: Color, backgroundColor: Color,
readingMode: ReadingModeType, readingMode: ReadingMode,
onClickReadingMode: () -> Unit, onClickReadingMode: () -> Unit,
orientationMode: OrientationType, orientation: ReaderOrientation,
onClickOrientationMode: () -> Unit, onClickOrientation: () -> Unit,
cropEnabled: Boolean, cropEnabled: Boolean,
onClickCropBorder: () -> Unit, onClickCropBorder: () -> Unit,
onClickSettings: () -> 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) { IconButton(onClick = onClickCropBorder) {
Icon( Icon(
painter = painterResource(if (cropEnabled) R.drawable.ic_crop_24dp else R.drawable.ic_crop_off_24dp), 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) { IconButton(onClick = onClickSettings) {
Icon( Icon(
imageVector = Icons.Outlined.Settings, 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmark import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.material.icons.outlined.BookmarkBorder import androidx.compose.material.icons.outlined.BookmarkBorder
@ -25,9 +23,10 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.reader.components.ChapterNavigator
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
@ -56,10 +55,10 @@ fun ReaderAppBars(
totalPages: Int, totalPages: Int,
onSliderValueChange: (Int) -> Unit, onSliderValueChange: (Int) -> Unit,
readingMode: ReadingModeType, readingMode: ReadingMode,
onClickReadingMode: () -> Unit, onClickReadingMode: () -> Unit,
orientationMode: OrientationType, orientation: ReaderOrientation,
onClickOrientationMode: () -> Unit, onClickOrientation: () -> Unit,
cropEnabled: Boolean, cropEnabled: Boolean,
onClickCropBorder: () -> Unit, onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit, onClickSettings: () -> Unit,
@ -69,8 +68,8 @@ fun ReaderAppBars(
.surfaceColorAtElevation(3.dp) .surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f) .copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
val appBarModifier = if (fullscreen) { val modifierWithInsetsPadding = if (fullscreen) {
Modifier.windowInsetsPadding(WindowInsets.systemBars) Modifier.systemBarsPadding()
} else { } else {
Modifier Modifier
} }
@ -91,7 +90,7 @@ fun ReaderAppBars(
), ),
) { ) {
AppBar( AppBar(
modifier = appBarModifier modifier = modifierWithInsetsPadding
.clickable(onClick = onClickTopAppBar), .clickable(onClick = onClickTopAppBar),
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
title = mangaTitle, title = mangaTitle,
@ -101,7 +100,9 @@ fun ReaderAppBars(
AppBarActions( AppBarActions(
listOfNotNull( listOfNotNull(
AppBar.Action( 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, icon = if (bookmarked) Icons.Outlined.Bookmark else Icons.Outlined.BookmarkBorder,
onClick = onToggleBookmarked, onClick = onToggleBookmarked,
), ),
@ -137,6 +138,7 @@ fun ReaderAppBars(
), ),
) { ) {
Column( Column(
modifier = modifierWithInsetsPadding,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
ChapterNavigator( ChapterNavigator(
@ -154,8 +156,8 @@ fun ReaderAppBars(
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
readingMode = readingMode, readingMode = readingMode,
onClickReadingMode = onClickReadingMode, onClickReadingMode = onClickReadingMode,
orientationMode = orientationMode, orientation = orientation,
onClickOrientationMode = onClickOrientationMode, onClickOrientation = onClickOrientation,
cropEnabled = cropEnabled, cropEnabled = cropEnabled,
onClickCropBorder = onClickCropBorder, onClickCropBorder = onClickCropBorder,
onClickSettings = onClickSettings, 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.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@ -77,7 +77,9 @@ fun ChapterNavigator(
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.SkipPrevious, 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( Icon(
imageVector = Icons.Outlined.SkipNext, 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), label = stringResource(R.string.pref_page_transitions),
pref = screenModel.preferences.pageTransitions(), 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.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.domain.manga.model.orientationType import eu.kanade.domain.manga.model.readerOrientation
import eu.kanade.domain.manga.model.readingModeType import eu.kanade.domain.manga.model.readingMode
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.HeadingItem
@ -23,33 +23,29 @@ import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import java.text.NumberFormat 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 @Composable
internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) { internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) {
HeadingItem(R.string.pref_category_for_this_series) HeadingItem(R.string.pref_category_for_this_series)
val manga by screenModel.mangaFlow.collectAsState() val manga by screenModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) } val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
SettingsChipRow(R.string.pref_category_reading_mode) { SettingsChipRow(R.string.pref_category_reading_mode) {
readingModeOptions.map { (stringRes, it) -> ReadingMode.entries.map {
FilterChip( FilterChip(
selected = it == readingMode, selected = it == readingMode,
onClick = { screenModel.onChangeReadingMode(it) }, 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) { SettingsChipRow(R.string.rotation_type) {
orientationTypeOptions.map { (stringRes, it) -> ReaderOrientation.entries.map {
FilterChip( FilterChip(
selected = it == orientationType, selected = it == orientation,
onClick = { screenModel.onChangeOrientation(it) }, onClick = { screenModel.onChangeOrientation(it) },
label = { Text(stringResource(stringRes)) }, label = { Text(stringResource(it.stringRes)) },
) )
} }
} }
@ -209,11 +205,11 @@ private fun ColumnScope.TapZonesItems(
if (selected != 5) { if (selected != 5) {
SettingsChipRow(R.string.pref_read_with_tapping_inverted) { SettingsChipRow(R.string.pref_read_with_tapping_inverted) {
tappingInvertModeOptions.map { (stringRes, mode) -> ReaderPreferences.TappingInvertMode.entries.map {
FilterChip( FilterChip(
selected = mode == invertMode, selected = it == invertMode,
onClick = { onSelectInvertMode(mode) }, onClick = { onSelectInvertMode(it) },
label = { Text(stringResource(stringRes)) }, 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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.VerticalDivider 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow 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.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.model.toDbTrack 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.data.track.Tracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.presentation.core.util.ThemePreviews
import java.text.DateFormat import java.text.DateFormat
private const val UnsetStatusTextAlpha = 0.5F private const val UnsetStatusTextAlpha = 0.5F
@ -318,11 +319,15 @@ private fun TrackInfoItemMenu(
} }
} }
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun TrackInfoDialogHomePreviews( private fun TrackInfoDialogHomePreviews(
@PreviewParameter(TrackInfoDialogHomePreviewProvider::class) @PreviewParameter(TrackInfoDialogHomePreviewProvider::class)
content: @Composable () -> Unit, 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.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.SelectableDates import androidx.compose.material3.SelectableDates
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.minimumInteractiveComponentSize
@ -29,6 +30,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R 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.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart import tachiyomi.presentation.core.util.isScrolledToStart
@ -221,10 +222,11 @@ private fun BaseSelector(
) )
} }
@ThemePreviews @PreviewLightDark
@Composable @Composable
private fun TrackStatusSelectorPreviews() { private fun TrackStatusSelectorPreviews() {
TachiyomiTheme { TachiyomiTheme {
Surface {
TrackStatusSelector( TrackStatusSelector(
selection = 1, selection = 1,
onSelectionChange = {}, onSelectionChange = {},
@ -242,3 +244,4 @@ private fun TrackStatusSelectorPreviews() {
) )
} }
} }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,14 @@
package eu.kanade.presentation.util package eu.kanade.presentation.util
import android.content.Context 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 eu.kanade.tachiyomi.R
import java.util.Date
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
fun Duration.toDurationString(context: Context, fallback: String): String { fun Duration.toDurationString(context: Context, fallback: String): String {
return toComponents { days, hours, minutes, seconds, _ -> return toComponents { days, hours, minutes, seconds, _ ->
@ -14,3 +20,14 @@ fun Duration.toDurationString(context: Context, fallback: String): String {
}.joinToString(" ").ifBlank { fallback } }.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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
@ -100,6 +98,12 @@ fun WebViewScreenContent(
request: WebResourceRequest?, request: WebResourceRequest?,
): Boolean { ): Boolean {
request?.let { 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) view?.loadUrl(it.url.toString(), headers)
} }
return super.shouldOverrideUrlLoading(view, request) return super.shouldOverrideUrlLoading(view, request)
@ -121,7 +125,7 @@ fun WebViewScreenContent(
listOf( listOf(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_webview_back), title = stringResource(R.string.action_webview_back),
icon = Icons.AutoMirrored.Outlined.ArrowBack, icon = Icons.Outlined.ArrowBack,
onClick = { onClick = {
if (navigator.canGoBack) { if (navigator.canGoBack) {
navigator.navigateBack() navigator.navigateBack()
@ -131,7 +135,7 @@ fun WebViewScreenContent(
), ),
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_webview_forward), title = stringResource(R.string.action_webview_forward),
icon = Icons.AutoMirrored.Outlined.ArrowForward, icon = Icons.Outlined.ArrowForward,
onClick = { onClick = {
if (navigator.canGoForward) { if (navigator.canGoForward) {
navigator.navigateForward() navigator.navigateForward()
@ -169,7 +173,9 @@ fun WebViewScreenContent(
modifier = Modifier modifier = Modifier
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.small)
.clickable { .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), .align(Alignment.BottomCenter),
) )
is LoadingState.Loading -> LinearProgressIndicator( is LoadingState.Loading -> LinearProgressIndicator(
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f }, progress = (loadingState as? LoadingState.Loading)?.progress ?: 1f,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),

View File

@ -11,6 +11,7 @@ import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.os.Looper import android.os.Looper
import android.webkit.WebView import android.webkit.WebView
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@ -185,7 +186,7 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) { if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME return WebViewUtil.SPOOF_PACKAGE_NAME
} }
} catch (e: Exception) { } catch (_: Exception) {
} }
} }
return super.getPackageName() return super.getPackageName()
@ -222,7 +223,12 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
fun register() { fun register() {
if (!registered) { 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 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. * 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 const val FOLDER_NAME = "image_cache"
private var instance: DiskCache? = null 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.data.track.TrackerManager
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE 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.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -121,13 +121,22 @@ object Migrations {
} }
} }
prefs.edit { 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") 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") 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") remove("pref_filter_completed_key")
} }
} }
@ -161,12 +170,12 @@ object Migrations {
if (oldVersion < 60) { if (oldVersion < 60) {
// Migrate Rotation and Viewer values to default values for viewer_flags // Migrate Rotation and Viewer values to default values for viewer_flags
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) { val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> OrientationType.FREE.flagValue 1 -> ReaderOrientation.FREE.flagValue
2 -> OrientationType.PORTRAIT.flagValue 2 -> ReaderOrientation.PORTRAIT.flagValue
3 -> OrientationType.LANDSCAPE.flagValue 3 -> ReaderOrientation.LANDSCAPE.flagValue
4 -> OrientationType.LOCKED_PORTRAIT.flagValue 4 -> ReaderOrientation.LOCKED_PORTRAIT.flagValue
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue 5 -> ReaderOrientation.LOCKED_LANDSCAPE.flagValue
else -> OrientationType.FREE.flagValue else -> ReaderOrientation.FREE.flagValue
} }
// Reading mode flag and prefValue is the same value // Reading mode flag and prefValue is the same value
@ -242,7 +251,10 @@ object Migrations {
if (oldSecureScreen) { if (oldSecureScreen) {
securityPreferences.secureScreen().set(SecurityPreferences.SecureScreenMode.ALWAYS) 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) basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY)
} }
} }
@ -259,7 +271,12 @@ object Migrations {
if (oldVersion < 81) { if (oldVersion < 81) {
// Handle renamed enum values // Handle renamed enum values
prefs.edit { 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" "LAST_CHECKED" -> "LAST_MANGA_UPDATE"
"UNREAD" -> "UNREAD_COUNT" "UNREAD" -> "UNREAD_COUNT"
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE" "DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
@ -375,17 +392,28 @@ object Migrations {
} }
} }
if (oldVersion < 107) { if (oldVersion < 107) {
preferenceStore.getAll() replacePreferences(
.filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") } preferenceStore = preferenceStore,
.forEach { (key, value) -> filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
if (value is String) { newKey = { Preference.privateKey(it) },
preferenceStore )
.getString(Preference.privateKey(key))
.set(value)
preferenceStore.getString(key).delete()
}
} }
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 return true
} }
@ -393,3 +421,41 @@ object Migrations {
return false 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 tachiyomi.domain.backup.service.BackupPreferences
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration import kotlin.time.toJavaDuration
@ -40,7 +41,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
val backupPreferences = Injekt.get<BackupPreferences>() val backupPreferences = Injekt.get<BackupPreferences>()
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: backupPreferences.backupsDirectory().get().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 { try {
setForeground(getForegroundInfo()) setForeground(getForegroundInfo())
@ -50,7 +51,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
return try { return try {
val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup) 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() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)

View File

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

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