mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 20:27:06 +03:00
Merge branch 'upstream/master'
This commit is contained in:
commit
acaf5bd43e
408 changed files with 7315 additions and 5506 deletions
6
.github/workflows/build_pull_request.yml
vendored
6
.github/workflows/build_pull_request.yml
vendored
|
@ -3,8 +3,10 @@ on:
|
|||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'i18n/src/main/res/**/strings.xml'
|
||||
- 'i18n/src/main/res/**/strings-aniyomi.xml'
|
||||
- 'i18n/src/commonMain/resources/**/strings-aniyomi.xml'
|
||||
- 'i18n/src/commonMain/resources/**/strings.xml'
|
||||
- 'i18n/src/commonMain/resources/**/plurals-aniyomi.xml'
|
||||
- 'i18n/src/commonMain/resources/**/plurals.xml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|-------|-----------|-------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------|---------|
|
||||
| [![CI](https://github.com/aniyomiorg/aniyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/aniyomiorg/aniyomi/actions/workflows/build_push.yml) | [![latest preview build](https://img.shields.io/github/v/release/aniyomiorg/aniyomi-preview.svg?maxAge=3600&label=download)](https://github.com/aniyomiorg/aniyomi-preview/releases) | [![CodeFactor](https://www.codefactor.io/repository/github/aniyomiorg/aniyomi/badge)](https://www.codefactor.io/repository/github/aniyomiorg/aniyomi) | [![stable release](https://img.shields.io/github/release/aniyomiorg/aniyomi.svg?maxAge=3600&label=download)](https://github.com/aniyomiorg/aniyomi/releases) | [![Translation status](https://hosted.weblate.org/widgets/aniyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/aniyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/841701076242530374?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/F32UjdJZrR) |
|
||||
|
||||
|
||||
# ![app icon](.github/readme-images/app-icon.png)Aniyomi
|
||||
Aniyomi is an unofficial fork of the free and open source manga reader [Tachiyomi](https://github.com/tachiyomiorg/tachiyomi) that adds anime capabilities! For Android 6.0 and above.
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ android {
|
|||
defaultConfig {
|
||||
applicationId = "xyz.jmir.tachiyomi.mi"
|
||||
|
||||
versionCode = 112
|
||||
versionName = "0.14.7"
|
||||
versionCode = 115
|
||||
versionName = "0.15.0.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
|
@ -130,6 +130,7 @@ android {
|
|||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
buildConfig = true
|
||||
|
||||
// Disable some unused things
|
||||
aidl = false
|
||||
|
@ -253,7 +254,7 @@ dependencies {
|
|||
implementation(libs.logcat)
|
||||
|
||||
// Crash reports
|
||||
implementation(libs.acra.http)
|
||||
implementation(libs.bundles.acra)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -50,7 +50,7 @@
|
|||
|
||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||
-dontnote kotlinx.serialization.** # core serialization annotations
|
||||
|
||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||
-keepclassmembers class kotlinx.serialization.json.** {
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
@ -20,10 +21,12 @@
|
|||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
|
@ -201,17 +204,15 @@
|
|||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="shortService" />
|
||||
|
||||
<service
|
||||
android:name=".extension.anime.util.AnimeExtensionInstallService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="shortService" />
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
|
|
|
@ -19,6 +19,9 @@ import eu.kanade.domain.items.chapter.interactor.SetReadStatus
|
|||
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.items.episode.interactor.SetSeenStatus
|
||||
import eu.kanade.domain.items.episode.interactor.SyncEpisodesWithSource
|
||||
import eu.kanade.domain.source.anime.interactor.CreateAnimeSourceRepo
|
||||
import eu.kanade.domain.source.anime.interactor.DeleteAnimeSourceRepo
|
||||
import eu.kanade.domain.source.anime.interactor.GetAnimeSourceRepos
|
||||
import eu.kanade.domain.source.anime.interactor.GetAnimeSourcesWithFavoriteCount
|
||||
import eu.kanade.domain.source.anime.interactor.GetEnabledAnimeSources
|
||||
import eu.kanade.domain.source.anime.interactor.GetLanguagesWithAnimeSources
|
||||
|
@ -26,8 +29,11 @@ import eu.kanade.domain.source.anime.interactor.ToggleAnimeSource
|
|||
import eu.kanade.domain.source.anime.interactor.ToggleAnimeSourcePin
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||
import eu.kanade.domain.source.manga.interactor.CreateMangaSourceRepo
|
||||
import eu.kanade.domain.source.manga.interactor.DeleteMangaSourceRepo
|
||||
import eu.kanade.domain.source.manga.interactor.GetEnabledMangaSources
|
||||
import eu.kanade.domain.source.manga.interactor.GetLanguagesWithMangaSources
|
||||
import eu.kanade.domain.source.manga.interactor.GetMangaSourceRepos
|
||||
import eu.kanade.domain.source.manga.interactor.GetMangaSourcesWithFavoriteCount
|
||||
import eu.kanade.domain.source.manga.interactor.ToggleMangaSource
|
||||
import eu.kanade.domain.source.manga.interactor.ToggleMangaSourcePin
|
||||
|
@ -88,12 +94,12 @@ import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
|
|||
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
|
||||
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.GetMangaByUrlAndSourceId
|
||||
import tachiyomi.domain.entries.manga.interactor.GetMangaByUrlAndSourceId
|
||||
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||
import tachiyomi.domain.entries.manga.interactor.GetAnimeByUrlAndSourceId
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeByUrlAndSourceId
|
||||
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||
|
@ -322,5 +328,12 @@ class DomainModule : InjektModule {
|
|||
addFactory { ToggleLanguage(get()) }
|
||||
addFactory { ToggleMangaSource(get()) }
|
||||
addFactory { ToggleMangaSourcePin(get()) }
|
||||
|
||||
addFactory { CreateMangaSourceRepo(get()) }
|
||||
addFactory { DeleteMangaSourceRepo(get()) }
|
||||
addFactory { GetMangaSourceRepos(get()) }
|
||||
addFactory { CreateAnimeSourceRepo(get()) }
|
||||
addFactory { DeleteAnimeSourceRepo(get()) }
|
||||
addFactory { GetAnimeSourceRepos(get()) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,10 +35,10 @@ class BasePreferences(
|
|||
|
||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||
|
||||
enum class ExtensionInstaller(val titleRes: StringResource) {
|
||||
LEGACY(MR.strings.ext_installer_legacy),
|
||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),
|
||||
SHIZUKU(MR.strings.ext_installer_shizuku),
|
||||
PRIVATE(MR.strings.ext_installer_private),
|
||||
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||
LEGACY(MR.strings.ext_installer_legacy, true),
|
||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
|
||||
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||
PRIVATE(MR.strings.ext_installer_private, false),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
|||
import tachiyomi.domain.items.chapter.service.ChapterRecognition
|
||||
import tachiyomi.source.local.entries.manga.isLocal
|
||||
import java.lang.Long.max
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TreeSet
|
||||
|
||||
|
@ -57,6 +56,7 @@ class SyncChaptersWithSource(
|
|||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val nowMillis = now.toInstant().toEpochMilli()
|
||||
|
||||
val sourceChapters = rawSourceChapters
|
||||
.distinctBy { it.url }
|
||||
|
@ -67,36 +67,27 @@ class SyncChaptersWithSource(
|
|||
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
||||
}
|
||||
|
||||
// Chapters from db.
|
||||
val dbChapters = getChaptersByMangaId.await(manga.id)
|
||||
|
||||
// Chapters from the source not in db.
|
||||
val toAdd = mutableListOf<Chapter>()
|
||||
|
||||
// Chapters whose metadata have changed.
|
||||
val toChange = mutableListOf<Chapter>()
|
||||
|
||||
// Chapters from the db not in source.
|
||||
val toDelete = dbChapters.filterNot { dbChapter ->
|
||||
val newChapters = mutableListOf<Chapter>()
|
||||
val updatedChapters = mutableListOf<Chapter>()
|
||||
val removedChapters = dbChapters.filterNot { dbChapter ->
|
||||
sourceChapters.any { sourceChapter ->
|
||||
dbChapter.url == sourceChapter.url
|
||||
}
|
||||
}
|
||||
|
||||
val rightNow = Instant.now().toEpochMilli()
|
||||
|
||||
// Used to not set upload date of older chapters
|
||||
// to a higher value than newer chapters
|
||||
var maxSeenUploadDate = 0L
|
||||
|
||||
val sManga = manga.toSManga()
|
||||
for (sourceChapter in sourceChapters) {
|
||||
var chapter = sourceChapter
|
||||
|
||||
// Update metadata from source if necessary.
|
||||
if (source is HttpSource) {
|
||||
val sChapter = chapter.toSChapter()
|
||||
source.prepareNewChapter(sChapter, sManga)
|
||||
source.prepareNewChapter(sChapter, manga.toSManga())
|
||||
chapter = chapter.copyFromSChapter(sChapter)
|
||||
}
|
||||
|
||||
|
@ -112,13 +103,13 @@ class SyncChaptersWithSource(
|
|||
|
||||
if (dbChapter == null) {
|
||||
val toAddChapter = if (chapter.dateUpload == 0L) {
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
|
||||
chapter.copy(dateUpload = altDateUpload)
|
||||
} else {
|
||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
||||
chapter
|
||||
}
|
||||
toAdd.add(toAddChapter)
|
||||
newChapters.add(toAddChapter)
|
||||
} else {
|
||||
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
|
||||
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(
|
||||
|
@ -144,13 +135,13 @@ class SyncChaptersWithSource(
|
|||
if (chapter.dateUpload != 0L) {
|
||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||
}
|
||||
toChange.add(toChangeChapter)
|
||||
updatedChapters.add(toChangeChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
|
||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga,
|
||||
|
@ -167,20 +158,20 @@ class SyncChaptersWithSource(
|
|||
val deletedReadChapterNumbers = TreeSet<Double>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
||||
|
||||
toDelete.forEach { chapter ->
|
||||
removedChapters.forEach { chapter ->
|
||||
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
||||
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
||||
deletedChapterNumbers.add(chapter.chapterNumber)
|
||||
}
|
||||
|
||||
val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
|
||||
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
|
||||
.associate { it.chapterNumber to it.dateFetch }
|
||||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// Sources MUST return the chapters from most to less recent, which is common.
|
||||
var itemCount = toAdd.size
|
||||
var updatedToAdd = toAdd.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
||||
var itemCount = newChapters.size
|
||||
var updatedToAdd = newChapters.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||
|
||||
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
||||
|
||||
|
@ -199,8 +190,8 @@ class SyncChaptersWithSource(
|
|||
chapter
|
||||
}
|
||||
|
||||
if (toDelete.isNotEmpty()) {
|
||||
val toDeleteIds = toDelete.map { it.id }
|
||||
if (removedChapters.isNotEmpty()) {
|
||||
val toDeleteIds = removedChapters.map { it.id }
|
||||
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
||||
}
|
||||
|
||||
|
@ -208,8 +199,8 @@ class SyncChaptersWithSource(
|
|||
updatedToAdd = chapterRepository.addAllChapters(updatedToAdd)
|
||||
}
|
||||
|
||||
if (toChange.isNotEmpty()) {
|
||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||
if (updatedChapters.isNotEmpty()) {
|
||||
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||
|
|
|
@ -21,7 +21,6 @@ import tachiyomi.domain.items.episode.repository.EpisodeRepository
|
|||
import tachiyomi.domain.items.episode.service.EpisodeRecognition
|
||||
import tachiyomi.source.local.entries.anime.isLocal
|
||||
import java.lang.Long.max
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TreeSet
|
||||
|
||||
|
@ -55,6 +54,7 @@ class SyncEpisodesWithSource(
|
|||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val nowMillis = now.toInstant().toEpochMilli()
|
||||
|
||||
val sourceEpisodes = rawSourceEpisodes
|
||||
.distinctBy { it.url }
|
||||
|
@ -65,36 +65,27 @@ class SyncEpisodesWithSource(
|
|||
.copy(animeId = anime.id, sourceOrder = i.toLong())
|
||||
}
|
||||
|
||||
// Episodes from db.
|
||||
val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
|
||||
|
||||
// Episodes from the source not in db.
|
||||
val toAdd = mutableListOf<Episode>()
|
||||
|
||||
// Episodes whose metadata have changed.
|
||||
val toChange = mutableListOf<Episode>()
|
||||
|
||||
// Episodes from the db not in source.
|
||||
val toDelete = dbEpisodes.filterNot { dbEpisode ->
|
||||
val newEpisodes = mutableListOf<Episode>()
|
||||
val updatedEpisodes = mutableListOf<Episode>()
|
||||
val removedEpisodes = dbEpisodes.filterNot { dbEpisode ->
|
||||
sourceEpisodes.any { sourceEpisode ->
|
||||
dbEpisode.url == sourceEpisode.url
|
||||
}
|
||||
}
|
||||
|
||||
val rightNow = Instant.now().toEpochMilli()
|
||||
|
||||
// Used to not set upload date of older episodes
|
||||
// to a higher value than newer episodes
|
||||
var maxSeenUploadDate = 0L
|
||||
|
||||
val sAnime = anime.toSAnime()
|
||||
for (sourceEpisode in sourceEpisodes) {
|
||||
var episode = sourceEpisode
|
||||
|
||||
// Update metadata from source if necessary.
|
||||
if (source is AnimeHttpSource) {
|
||||
val sEpisode = episode.toSEpisode()
|
||||
source.prepareNewEpisode(sEpisode, sAnime)
|
||||
source.prepareNewEpisode(sEpisode, anime.toSAnime())
|
||||
episode = episode.copyFromSEpisode(sEpisode)
|
||||
}
|
||||
|
||||
|
@ -110,13 +101,13 @@ class SyncEpisodesWithSource(
|
|||
|
||||
if (dbEpisode == null) {
|
||||
val toAddEpisode = if (episode.dateUpload == 0L) {
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
|
||||
episode.copy(dateUpload = altDateUpload)
|
||||
} else {
|
||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceEpisode.dateUpload)
|
||||
episode
|
||||
}
|
||||
toAdd.add(toAddEpisode)
|
||||
newEpisodes.add(toAddEpisode)
|
||||
} else {
|
||||
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
|
||||
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(
|
||||
|
@ -144,13 +135,13 @@ class SyncEpisodesWithSource(
|
|||
dateUpload = sourceEpisode.dateUpload,
|
||||
)
|
||||
}
|
||||
toChange.add(toChangeEpisode)
|
||||
updatedEpisodes.add(toChangeEpisode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||
if (newEpisodes.isEmpty() && removedEpisodes.isEmpty() && updatedEpisodes.isEmpty()) {
|
||||
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
|
||||
updateAnime.awaitUpdateFetchInterval(
|
||||
anime,
|
||||
|
@ -167,20 +158,20 @@ class SyncEpisodesWithSource(
|
|||
val deletedSeenEpisodeNumbers = TreeSet<Double>()
|
||||
val deletedBookmarkedEpisodeNumbers = TreeSet<Double>()
|
||||
|
||||
toDelete.forEach { episode ->
|
||||
removedEpisodes.forEach { episode ->
|
||||
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
|
||||
if (episode.bookmark) deletedBookmarkedEpisodeNumbers.add(episode.episodeNumber)
|
||||
deletedEpisodeNumbers.add(episode.episodeNumber)
|
||||
}
|
||||
|
||||
val deletedEpisodeNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
|
||||
val deletedEpisodeNumberDateFetchMap = removedEpisodes.sortedByDescending { it.dateFetch }
|
||||
.associate { it.episodeNumber to it.dateFetch }
|
||||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// Sources MUST return the episodes from most to less recent, which is common.
|
||||
var itemCount = toAdd.size
|
||||
var updatedToAdd = toAdd.map { toAddItem ->
|
||||
var episode = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
||||
var itemCount = newEpisodes.size
|
||||
var updatedToAdd = newEpisodes.map { toAddItem ->
|
||||
var episode = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||
|
||||
if (episode.isRecognizedNumber.not() || episode.episodeNumber !in deletedEpisodeNumbers) return@map episode
|
||||
|
||||
|
@ -199,8 +190,8 @@ class SyncEpisodesWithSource(
|
|||
episode
|
||||
}
|
||||
|
||||
if (toDelete.isNotEmpty()) {
|
||||
val toDeleteIds = toDelete.map { it.id }
|
||||
if (removedEpisodes.isNotEmpty()) {
|
||||
val toDeleteIds = removedEpisodes.map { it.id }
|
||||
episodeRepository.removeEpisodesWithIds(toDeleteIds)
|
||||
}
|
||||
|
||||
|
@ -208,8 +199,8 @@ class SyncEpisodesWithSource(
|
|||
updatedToAdd = episodeRepository.addAllEpisodes(updatedToAdd)
|
||||
}
|
||||
|
||||
if (toChange.isNotEmpty()) {
|
||||
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
|
||||
if (updatedEpisodes.isNotEmpty()) {
|
||||
val episodeUpdates = updatedEpisodes.map { it.toEpisodeUpdate() }
|
||||
updateEpisode.awaitAll(episodeUpdates)
|
||||
}
|
||||
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package eu.kanade.domain.source.anime.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.plusAssign
|
||||
|
||||
class CreateAnimeSourceRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(name: String): Result {
|
||||
// Do not allow invalid formats
|
||||
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_REPO_BASE_URL)) {
|
||||
return Result.InvalidUrl
|
||||
}
|
||||
|
||||
preferences.animeExtensionRepos() += name.substringBeforeLast("/index.min.json")
|
||||
|
||||
return Result.Success
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data object InvalidUrl : Result
|
||||
data object Success : Result
|
||||
}
|
||||
}
|
||||
|
||||
const val OFFICIAL_REPO_BASE_URL = "https://raw.githubusercontent.com/aniyomiorg/aniyomi-extensions/repo"
|
||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
|
@ -0,0 +1,11 @@
|
|||
package eu.kanade.domain.source.anime.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.minusAssign
|
||||
|
||||
class DeleteAnimeSourceRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(repo: String) {
|
||||
preferences.animeExtensionRepos() -= repo
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package eu.kanade.domain.source.anime.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetAnimeSourceRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun subscribe(): Flow<List<String>> {
|
||||
return preferences.animeExtensionRepos().changes()
|
||||
.map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package eu.kanade.domain.source.manga.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.plusAssign
|
||||
|
||||
class CreateMangaSourceRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(name: String): Result {
|
||||
// Do not allow invalid formats
|
||||
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_REPO_BASE_URL)) {
|
||||
return Result.InvalidUrl
|
||||
}
|
||||
|
||||
preferences.mangaExtensionRepos() += name.substringBeforeLast("/index.min.json")
|
||||
|
||||
return Result.Success
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data object InvalidUrl : Result
|
||||
data object Success : Result
|
||||
}
|
||||
}
|
||||
|
||||
const val OFFICIAL_REPO_BASE_URL = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo"
|
||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
|
@ -0,0 +1,11 @@
|
|||
package eu.kanade.domain.source.manga.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.minusAssign
|
||||
|
||||
class DeleteMangaSourceRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(repo: String) {
|
||||
preferences.mangaExtensionRepos() -= repo
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package eu.kanade.domain.source.manga.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetMangaSourceRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun subscribe(): Flow<List<String>> {
|
||||
return preferences.mangaExtensionRepos().changes()
|
||||
.map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||
}
|
||||
}
|
|
@ -37,6 +37,12 @@ class SourcePreferences(
|
|||
SetMigrateSorting.Direction.ASCENDING,
|
||||
)
|
||||
|
||||
fun animeExtensionRepos() = preferenceStore.getStringSet("anime_extension_repos", emptySet())
|
||||
|
||||
fun mangaExtensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||
|
||||
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
||||
|
||||
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
|
||||
|
||||
// Mixture Sources
|
||||
|
|
|
@ -25,7 +25,7 @@ class RefreshAnimeTracks(
|
|||
suspend fun await(animeId: Long): List<Pair<Tracker?, Throwable>> {
|
||||
return supervisorScope {
|
||||
return@supervisorScope getTracks.await(animeId)
|
||||
.map { it to trackerManager.get(it.syncId) }
|
||||
.map { it to trackerManager.get(it.trackerId) }
|
||||
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||
.map { (track, service) ->
|
||||
async {
|
||||
|
|
|
@ -28,7 +28,7 @@ class TrackEpisode(
|
|||
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||
|
||||
tracks.mapNotNull { track ->
|
||||
val service = trackerManager.get(track.syncId)
|
||||
val service = trackerManager.get(track.trackerId)
|
||||
if (service == null || !service.isLoggedIn || episodeNumber <= track.lastEpisodeSeen) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
|
|
@ -13,10 +13,10 @@ fun AnimeTrack.copyPersonalFrom(other: AnimeTrack): AnimeTrack {
|
|||
)
|
||||
}
|
||||
|
||||
fun AnimeTrack.toDbTrack(): DbAnimeTrack = DbAnimeTrack.create(syncId).also {
|
||||
fun AnimeTrack.toDbTrack(): DbAnimeTrack = DbAnimeTrack.create(trackerId).also {
|
||||
it.id = id
|
||||
it.anime_id = animeId
|
||||
it.media_id = remoteId
|
||||
it.remote_id = remoteId
|
||||
it.library_id = libraryId
|
||||
it.title = title
|
||||
it.last_episode_seen = lastEpisodeSeen.toFloat()
|
||||
|
@ -33,8 +33,8 @@ fun DbAnimeTrack.toDomainTrack(idRequired: Boolean = true): AnimeTrack? {
|
|||
return AnimeTrack(
|
||||
id = trackId,
|
||||
animeId = anime_id,
|
||||
syncId = sync_id.toLong(),
|
||||
remoteId = media_id,
|
||||
trackerId = tracker_id.toLong(),
|
||||
remoteId = remote_id,
|
||||
libraryId = library_id,
|
||||
title = title,
|
||||
lastEpisodeSeen = last_episode_seen.toDouble(),
|
||||
|
|
|
@ -25,7 +25,7 @@ class RefreshMangaTracks(
|
|||
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
|
||||
return supervisorScope {
|
||||
return@supervisorScope getTracks.await(mangaId)
|
||||
.map { it to trackerManager.get(it.syncId) }
|
||||
.map { it to trackerManager.get(it.trackerId) }
|
||||
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||
.map { (track, service) ->
|
||||
async {
|
||||
|
|
|
@ -27,7 +27,7 @@ class TrackChapter(
|
|||
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||
|
||||
tracks.mapNotNull { track ->
|
||||
val service = trackerManager.get(track.syncId)
|
||||
val service = trackerManager.get(track.trackerId)
|
||||
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
||||
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
||||
return@mapNotNull null
|
||||
|
|
|
@ -13,10 +13,10 @@ fun MangaTrack.copyPersonalFrom(other: MangaTrack): MangaTrack {
|
|||
)
|
||||
}
|
||||
|
||||
fun MangaTrack.toDbTrack(): DbMangaTrack = DbMangaTrack.create(syncId).also {
|
||||
fun MangaTrack.toDbTrack(): DbMangaTrack = DbMangaTrack.create(trackerId).also {
|
||||
it.id = id
|
||||
it.manga_id = mangaId
|
||||
it.media_id = remoteId
|
||||
it.remote_id = remoteId
|
||||
it.library_id = libraryId
|
||||
it.title = title
|
||||
it.last_chapter_read = lastChapterRead.toFloat()
|
||||
|
@ -33,8 +33,8 @@ fun DbMangaTrack.toDomainTrack(idRequired: Boolean = true): MangaTrack? {
|
|||
return MangaTrack(
|
||||
id = trackId,
|
||||
mangaId = manga_id,
|
||||
syncId = sync_id.toLong(),
|
||||
remoteId = media_id,
|
||||
trackerId = tracker_id.toLong(),
|
||||
remoteId = remote_id,
|
||||
libraryId = library_id,
|
||||
title = title,
|
||||
lastChapterRead = last_chapter_read.toDouble(),
|
||||
|
|
|
@ -2,22 +2,29 @@ package eu.kanade.domain.track.service
|
|||
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import tachiyomi.core.preference.Preference
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class TrackPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
|
||||
fun trackUsername(tracker: Tracker) = preferenceStore.getString(
|
||||
Preference.privateKey("pref_mangasync_username_${tracker.id}"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
|
||||
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
|
||||
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun setCredentials(sync: Tracker, username: String, password: String) {
|
||||
trackUsername(sync).set(username)
|
||||
trackPassword(sync).set(password)
|
||||
fun setCredentials(tracker: Tracker, username: String, password: String) {
|
||||
trackUsername(tracker).set(username)
|
||||
trackPassword(tracker).set(password)
|
||||
}
|
||||
|
||||
fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
|
||||
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
|
||||
|
||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
|
||||
|
@ -29,12 +36,4 @@ class TrackPreferences(
|
|||
"show_next_episode_airing_time",
|
||||
true,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
|
||||
|
||||
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
||||
|
||||
private fun trackToken(syncId: Long) = "track_token_$syncId"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ fun GlobalSearchResultItem(
|
|||
modifier = Modifier
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.tiny,
|
||||
end = MaterialTheme.padding.extraSmall,
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
|
|
|
@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Button
|
||||
|
@ -36,6 +35,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -66,7 +66,6 @@ fun AnimeExtensionDetailsScreen(
|
|||
state: AnimeExtensionDetailsScreenModel.State,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickWhatsNew: () -> Unit,
|
||||
onClickReadme: () -> Unit,
|
||||
onClickEnableAll: () -> Unit,
|
||||
onClickDisableAll: () -> Unit,
|
||||
onClickClearCookies: () -> Unit,
|
||||
|
@ -90,13 +89,6 @@ fun AnimeExtensionDetailsScreen(
|
|||
onClick = onClickWhatsNew,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_faq_and_guides),
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
onClick = onClickReadme,
|
||||
),
|
||||
)
|
||||
}
|
||||
addAll(
|
||||
listOf(
|
||||
|
@ -124,7 +116,7 @@ fun AnimeExtensionDetailsScreen(
|
|||
) { paddingValues ->
|
||||
if (state.extension == null) {
|
||||
EmptyScreen(
|
||||
stringRes = MR.strings.empty_screen,
|
||||
MR.strings.empty_screen,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
|
@ -157,6 +149,21 @@ private fun AnimeExtensionDetails(
|
|||
contentPadding = contentPadding,
|
||||
) {
|
||||
when {
|
||||
extension.isRepoSource ->
|
||||
item {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
WarningBanner(
|
||||
MR.strings.repo_extension_message,
|
||||
modifier = Modifier.clickable {
|
||||
extension.repoUrl ?: return@clickable
|
||||
uriHandler.openUri(
|
||||
extension.repoUrl
|
||||
.replace("https://raw.githubusercontent.com", "https://github.com")
|
||||
.removeSuffix("/repo/"),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
extension.isUnofficial ->
|
||||
item {
|
||||
WarningBanner(MR.strings.unofficial_anime_extension_message)
|
||||
|
@ -296,7 +303,7 @@ private fun DetailsHeader(
|
|||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.presentation.browse.anime
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
@ -41,12 +42,15 @@ import eu.kanade.presentation.browse.BaseBrowseItem
|
|||
import eu.kanade.presentation.browse.anime.components.AnimeExtensionIcon
|
||||
import eu.kanade.presentation.browse.manga.ExtensionHeader
|
||||
import eu.kanade.presentation.browse.manga.ExtensionTrustDialog
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
|
@ -65,7 +69,7 @@ fun AnimeExtensionScreen(
|
|||
searchQuery: String?,
|
||||
onLongClickItem: (AnimeExtension) -> Unit,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||
onClickItemWebView: (AnimeExtension.Available) -> Unit,
|
||||
onOpenWebView: (AnimeExtension.Available) -> Unit,
|
||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||
onUninstallExtension: (AnimeExtension) -> Unit,
|
||||
onUpdateExtension: (AnimeExtension.Installed) -> Unit,
|
||||
|
@ -98,7 +102,7 @@ fun AnimeExtensionScreen(
|
|||
contentPadding = contentPadding,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onOpenWebView = onOpenWebView,
|
||||
onInstallExtension = onInstallExtension,
|
||||
onUninstallExtension = onUninstallExtension,
|
||||
onUpdateExtension = onUpdateExtension,
|
||||
|
@ -116,7 +120,7 @@ private fun AnimeExtensionContent(
|
|||
state: AnimeExtensionsScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (AnimeExtension) -> Unit,
|
||||
onClickItemWebView: (AnimeExtension.Available) -> Unit,
|
||||
onOpenWebView: (AnimeExtension.Available) -> Unit,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||
onUninstallExtension: (AnimeExtension) -> Unit,
|
||||
|
@ -125,11 +129,24 @@ private fun AnimeExtensionContent(
|
|||
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var trustState by remember { mutableStateOf<AnimeExtension.Untrusted?>(null) }
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState()
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
||||
item {
|
||||
WarningBanner(
|
||||
textRes = MR.strings.ext_permission_install_apps_warning,
|
||||
modifier = Modifier.clickable {
|
||||
context.launchRequestPackageInstallsPermission()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.items.forEach { (header, items) ->
|
||||
item(
|
||||
contentType = "header",
|
||||
|
@ -183,8 +200,14 @@ private fun AnimeExtensionContent(
|
|||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemSecondaryAction = {
|
||||
when (it) {
|
||||
is AnimeExtension.Available -> onOpenWebView(it)
|
||||
is AnimeExtension.Installed -> onOpenExtension(it)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
is AnimeExtension.Available -> onInstallExtension(it)
|
||||
|
@ -227,10 +250,10 @@ private fun AnimeExtensionItem(
|
|||
item: AnimeExtensionUiModel.Item,
|
||||
onClickItem: (AnimeExtension) -> Unit,
|
||||
onLongClickItem: (AnimeExtension) -> Unit,
|
||||
onClickItemWebView: (AnimeExtension.Available) -> Unit,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||
onClickItemAction: (AnimeExtension) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItemSecondaryAction: (AnimeExtension) -> Unit,
|
||||
) {
|
||||
val (extension, installStep) = item
|
||||
BaseBrowseItem(
|
||||
|
@ -271,9 +294,9 @@ private fun AnimeExtensionItem(
|
|||
AnimeExtensionItemActions(
|
||||
extension = extension,
|
||||
installStep = installStep,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = onClickItemAction,
|
||||
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -303,7 +326,7 @@ private fun AnimeExtensionItemContent(
|
|||
// Won't look good but it's not like we can ellipsize overflowing content
|
||||
FlowRow(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
if (extension is AnimeExtension.Installed && extension.lang.isNotEmpty()) {
|
||||
|
@ -358,15 +381,15 @@ private fun AnimeExtensionItemActions(
|
|||
extension: AnimeExtension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItemWebView: (AnimeExtension.Available) -> Unit = {},
|
||||
onClickItemCancel: (AnimeExtension) -> Unit = {},
|
||||
onClickItemAction: (AnimeExtension) -> Unit = {},
|
||||
onClickItemSecondaryAction: (AnimeExtension) -> Unit = {},
|
||||
) {
|
||||
val isIdle = installStep.isCompleted()
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
when {
|
||||
!isIdle -> {
|
||||
|
@ -388,6 +411,13 @@ private fun AnimeExtensionItemActions(
|
|||
installStep == InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is AnimeExtension.Installed -> {
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.hasUpdate) {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
|
@ -396,13 +426,6 @@ private fun AnimeExtensionItemActions(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
}
|
||||
is AnimeExtension.Untrusted -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
|
@ -415,7 +438,7 @@ private fun AnimeExtensionItemActions(
|
|||
is AnimeExtension.Available -> {
|
||||
if (extension.sources.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = { onClickItemWebView(extension) },
|
||||
onClick = { onClickItemSecondaryAction(extension) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Public,
|
||||
|
|
|
@ -38,7 +38,7 @@ fun GlobalAnimeSearchCardRow(
|
|||
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
items(titles) {
|
||||
val title by getAnime(it)
|
||||
|
|
|
@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
|
@ -38,6 +37,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -67,7 +67,6 @@ fun ExtensionDetailsScreen(
|
|||
state: MangaExtensionDetailsScreenModel.State,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickWhatsNew: () -> Unit,
|
||||
onClickReadme: () -> Unit,
|
||||
onClickEnableAll: () -> Unit,
|
||||
onClickDisableAll: () -> Unit,
|
||||
onClickClearCookies: () -> Unit,
|
||||
|
@ -91,13 +90,6 @@ fun ExtensionDetailsScreen(
|
|||
onClick = onClickWhatsNew,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_faq_and_guides),
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
onClick = onClickReadme,
|
||||
),
|
||||
)
|
||||
}
|
||||
addAll(
|
||||
listOf(
|
||||
|
@ -125,7 +117,7 @@ fun ExtensionDetailsScreen(
|
|||
) { paddingValues ->
|
||||
if (state.extension == null) {
|
||||
EmptyScreen(
|
||||
stringRes = MR.strings.empty_screen,
|
||||
MR.strings.empty_screen,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
|
@ -158,6 +150,21 @@ private fun ExtensionDetails(
|
|||
contentPadding = contentPadding,
|
||||
) {
|
||||
when {
|
||||
extension.isRepoSource ->
|
||||
item {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
WarningBanner(
|
||||
MR.strings.repo_extension_message,
|
||||
modifier = Modifier.clickable {
|
||||
extension.repoUrl ?: return@clickable
|
||||
uriHandler.openUri(
|
||||
extension.repoUrl
|
||||
.replace("https://raw.githubusercontent.com", "https://github.com")
|
||||
.removeSuffix("/repo/"),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
extension.isUnofficial ->
|
||||
item {
|
||||
WarningBanner(MR.strings.unofficial_extension_message)
|
||||
|
@ -295,7 +302,7 @@ private fun DetailsHeader(
|
|||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.presentation.browse.manga
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
@ -42,12 +43,15 @@ import androidx.compose.ui.unit.dp
|
|||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.presentation.browse.BaseBrowseItem
|
||||
import eu.kanade.presentation.browse.manga.components.MangaExtensionIcon
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.MangaExtensionsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
|
@ -67,7 +71,7 @@ fun MangaExtensionScreen(
|
|||
searchQuery: String?,
|
||||
onLongClickItem: (MangaExtension) -> Unit,
|
||||
onClickItemCancel: (MangaExtension) -> Unit,
|
||||
onClickItemWebView: (MangaExtension.Available) -> Unit,
|
||||
onOpenWebView: (MangaExtension.Available) -> Unit,
|
||||
onInstallExtension: (MangaExtension.Available) -> Unit,
|
||||
onUninstallExtension: (MangaExtension) -> Unit,
|
||||
onUpdateExtension: (MangaExtension.Installed) -> Unit,
|
||||
|
@ -100,7 +104,7 @@ fun MangaExtensionScreen(
|
|||
contentPadding = contentPadding,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onOpenWebView = onOpenWebView,
|
||||
onInstallExtension = onInstallExtension,
|
||||
onUninstallExtension = onUninstallExtension,
|
||||
onUpdateExtension = onUpdateExtension,
|
||||
|
@ -118,7 +122,7 @@ private fun ExtensionContent(
|
|||
state: MangaExtensionsScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (MangaExtension) -> Unit,
|
||||
onClickItemWebView: (MangaExtension.Available) -> Unit,
|
||||
onOpenWebView: (MangaExtension.Available) -> Unit,
|
||||
onClickItemCancel: (MangaExtension) -> Unit,
|
||||
onInstallExtension: (MangaExtension.Available) -> Unit,
|
||||
onUninstallExtension: (MangaExtension) -> Unit,
|
||||
|
@ -127,11 +131,24 @@ private fun ExtensionContent(
|
|||
onOpenExtension: (MangaExtension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var trustState by remember { mutableStateOf<MangaExtension.Untrusted?>(null) }
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState()
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
||||
item {
|
||||
WarningBanner(
|
||||
textRes = MR.strings.ext_permission_install_apps_warning,
|
||||
modifier = Modifier.clickable {
|
||||
context.launchRequestPackageInstallsPermission()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.items.forEach { (header, items) ->
|
||||
item(
|
||||
contentType = "header",
|
||||
|
@ -185,7 +202,13 @@ private fun ExtensionContent(
|
|||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onClickItemSecondaryAction = {
|
||||
when (it) {
|
||||
is MangaExtension.Available -> onOpenWebView(it)
|
||||
is MangaExtension.Installed -> onOpenExtension(it)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
|
@ -229,9 +252,9 @@ private fun ExtensionItem(
|
|||
item: MangaExtensionUiModel.Item,
|
||||
onClickItem: (MangaExtension) -> Unit,
|
||||
onLongClickItem: (MangaExtension) -> Unit,
|
||||
onClickItemWebView: (MangaExtension.Available) -> Unit,
|
||||
onClickItemCancel: (MangaExtension) -> Unit,
|
||||
onClickItemAction: (MangaExtension) -> Unit,
|
||||
onClickItemSecondaryAction: (MangaExtension) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (extension, installStep) = item
|
||||
|
@ -273,9 +296,9 @@ private fun ExtensionItem(
|
|||
ExtensionItemActions(
|
||||
extension = extension,
|
||||
installStep = installStep,
|
||||
onClickItemWebView = onClickItemWebView,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = onClickItemAction,
|
||||
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -305,7 +328,7 @@ private fun ExtensionItemContent(
|
|||
// Won't look good but it's not like we can ellipsize overflowing content
|
||||
FlowRow(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
if (extension is MangaExtension.Installed && extension.lang.isNotEmpty()) {
|
||||
|
@ -360,15 +383,15 @@ private fun ExtensionItemActions(
|
|||
extension: MangaExtension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItemWebView: (MangaExtension.Available) -> Unit = {},
|
||||
onClickItemCancel: (MangaExtension) -> Unit = {},
|
||||
onClickItemAction: (MangaExtension) -> Unit = {},
|
||||
onClickItemSecondaryAction: (MangaExtension) -> Unit = {},
|
||||
) {
|
||||
val isIdle = installStep.isCompleted()
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
when {
|
||||
!isIdle -> {
|
||||
|
@ -390,6 +413,13 @@ private fun ExtensionItemActions(
|
|||
installStep == InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is MangaExtension.Installed -> {
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.hasUpdate) {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
|
@ -398,13 +428,6 @@ private fun ExtensionItemActions(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
}
|
||||
is MangaExtension.Untrusted -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
|
@ -417,7 +440,7 @@ private fun ExtensionItemActions(
|
|||
is MangaExtension.Available -> {
|
||||
if (extension.sources.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = { onClickItemWebView(extension) },
|
||||
onClick = { onClickItemSecondaryAction(extension) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Public,
|
||||
|
|
|
@ -38,7 +38,7 @@ fun GlobalMangaSearchCardRow(
|
|||
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
items(titles) {
|
||||
val title by getManga(it)
|
||||
|
|
|
@ -27,6 +27,8 @@ import androidx.compose.ui.focus.FocusRequester
|
|||
import androidx.compose.ui.focus.focusRequester
|
||||
import eu.kanade.core.preference.asToggleableState
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.core.preference.CheckboxState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
|
@ -39,12 +41,12 @@ import kotlin.time.Duration.Companion.seconds
|
|||
fun CategoryCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: (String) -> Unit,
|
||||
categories: List<Category>,
|
||||
categories: ImmutableList<String>,
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
||||
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
@ -69,10 +71,13 @@ fun CategoryCreateDialog(
|
|||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text(text = stringResource(MR.strings.name)) },
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.name))
|
||||
},
|
||||
supportingText = {
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||
MR.strings.error_category_exists
|
||||
|
@ -98,14 +103,14 @@ fun CategoryCreateDialog(
|
|||
fun CategoryRenameDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onRename: (String) -> Unit,
|
||||
categories: List<Category>,
|
||||
category: Category,
|
||||
categories: ImmutableList<String>,
|
||||
category: String,
|
||||
) {
|
||||
var name by remember { mutableStateOf(category.name) }
|
||||
var name by remember { mutableStateOf(category) }
|
||||
var valueHasChanged by remember { mutableStateOf(false) }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
||||
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
@ -162,7 +167,7 @@ fun CategoryRenameDialog(
|
|||
fun CategoryDeleteDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
category: Category,
|
||||
category: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
@ -183,7 +188,7 @@ fun CategoryDeleteDialog(
|
|||
Text(text = stringResource(MR.strings.delete_category))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.delete_category_confirmation, category.name))
|
||||
Text(text = stringResource(MR.strings.delete_category_confirmation, category))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -219,7 +224,7 @@ fun CategorySortAlphabeticallyDialog(
|
|||
|
||||
@Composable
|
||||
fun ChangeCategoryDialog(
|
||||
initialSelection: List<CheckboxState<Category>>,
|
||||
initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onEditCategories: () -> Unit,
|
||||
onConfirm: (List<Long>, List<Long>) -> Unit,
|
||||
|
@ -291,7 +296,7 @@ fun ChangeCategoryDialog(
|
|||
if (index != -1) {
|
||||
val mutableList = selection.toMutableList()
|
||||
mutableList[index] = it.next()
|
||||
selection = mutableList.toList()
|
||||
selection = mutableList.toList().toImmutableList()
|
||||
}
|
||||
}
|
||||
Row(
|
||||
|
@ -325,7 +330,3 @@ fun ChangeCategoryDialog(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<Category>.anyWithName(name: String): Boolean {
|
||||
return any { name == it.name }
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.material.icons.outlined.Add
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@ -16,11 +17,13 @@ import tachiyomi.presentation.core.util.isScrollingUp
|
|||
fun CategoryFloatingActionButton(
|
||||
lazyListState: LazyListState,
|
||||
onCreate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
|
||||
onClick = onCreate,
|
||||
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ fun CategoryListItem(
|
|||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Text(
|
||||
text = category.name,
|
||||
modifier = Modifier
|
||||
|
@ -64,13 +64,13 @@ fun CategoryListItem(
|
|||
onClick = { onMoveUp(category) },
|
||||
enabled = canMoveUp,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onMoveDown(category) },
|
||||
enabled = canMoveDown,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onRename) {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
fun relativeDateText(
|
||||
dateEpochMillis: Long,
|
||||
): String {
|
||||
return relativeDateText(
|
||||
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun relativeDateText(
|
||||
date: Date?,
|
||||
): String {
|
||||
val context = LocalContext.current
|
||||
|
||||
val preferences = remember { Injekt.get<UiPreferences>() }
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
return date
|
||||
?.toRelativeString(
|
||||
context = context,
|
||||
relative = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
?: stringResource(MR.strings.not_applicable)
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
||||
|
@ -22,12 +25,17 @@ import tachiyomi.i18n.MR
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
||||
|
||||
/**
|
||||
* DropdownMenu but overlaps anchor and has width constraints to better
|
||||
* match non-Compose implementation.
|
||||
*/
|
||||
@Composable
|
||||
fun DropdownMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
offset: DpOffset = DpOffset(8.dp, (-56).dp),
|
||||
scrollState: ScrollState = rememberScrollState(),
|
||||
properties: PopupProperties = PopupProperties(focusable = true),
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
|
@ -36,6 +44,7 @@ fun DropdownMenu(
|
|||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
|
||||
offset = offset,
|
||||
scrollState = scrollState,
|
||||
properties = properties,
|
||||
content = content,
|
||||
)
|
||||
|
@ -45,6 +54,7 @@ fun DropdownMenu(
|
|||
fun RadioMenuItem(
|
||||
text: @Composable () -> Unit,
|
||||
isChecked: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
|
@ -64,6 +74,7 @@ fun RadioMenuItem(
|
|||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -71,25 +82,29 @@ fun RadioMenuItem(
|
|||
fun NestedMenuItem(
|
||||
text: @Composable () -> Unit,
|
||||
children: @Composable ColumnScope.(() -> Unit) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var nestedExpanded by remember { mutableStateOf(false) }
|
||||
val closeMenu = { nestedExpanded = false }
|
||||
|
||||
DropdownMenuItem(
|
||||
text = text,
|
||||
onClick = { nestedExpanded = true },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
Box {
|
||||
DropdownMenuItem(
|
||||
text = text,
|
||||
onClick = { nestedExpanded = true },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = nestedExpanded,
|
||||
onDismissRequest = closeMenu,
|
||||
) {
|
||||
children(closeMenu)
|
||||
DropdownMenu(
|
||||
expanded = nestedExpanded,
|
||||
onDismissRequest = closeMenu,
|
||||
modifier = modifier,
|
||||
) {
|
||||
children(closeMenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ package eu.kanade.presentation.components
|
|||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.entries.DownloadAction
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@ -14,20 +16,24 @@ fun EntryDownloadDropdownMenu(
|
|||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
isManga: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val downloadAmount = if (isManga) MR.plurals.download_amount else MR.plurals.download_amount_anime
|
||||
val downloadUnviewed = if (isManga) MR.strings.download_unread else MR.strings.download_unseen
|
||||
val options = persistentListOf(
|
||||
DownloadAction.NEXT_1_ITEM to pluralStringResource(downloadAmount, 1, 1),
|
||||
DownloadAction.NEXT_5_ITEMS to pluralStringResource(downloadAmount, 5, 5),
|
||||
DownloadAction.NEXT_10_ITEMS to pluralStringResource(downloadAmount, 10, 10),
|
||||
DownloadAction.NEXT_25_ITEMS to pluralStringResource(downloadAmount, 25, 25),
|
||||
DownloadAction.UNVIEWED_ITEMS to stringResource(downloadUnviewed),
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
) {
|
||||
val downloadAmount = if (isManga) MR.plurals.download_amount_manga else MR.plurals.download_amount_anime
|
||||
val downloadUnviewed = if (isManga) MR.strings.download_unread else MR.strings.download_unseen
|
||||
listOfNotNull(
|
||||
DownloadAction.NEXT_1_ITEM to pluralStringResource(downloadAmount, 1, 1),
|
||||
DownloadAction.NEXT_5_ITEMS to pluralStringResource(downloadAmount, 5, 5),
|
||||
DownloadAction.NEXT_10_ITEMS to pluralStringResource(downloadAmount, 10, 10),
|
||||
DownloadAction.NEXT_25_ITEMS to pluralStringResource(downloadAmount, 25, 25),
|
||||
DownloadAction.UNVIEWED_ITEMS to stringResource(downloadUnviewed),
|
||||
).map { (downloadAction, string) ->
|
||||
options.map { (downloadAction, string) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = string) },
|
||||
onClick = {
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
fun RelativeDateHeader(
|
||||
date: Date,
|
||||
relativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
ListGroupHeader(
|
||||
modifier = modifier,
|
||||
text = remember {
|
||||
date.toRelativeString(
|
||||
context,
|
||||
relativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
|
@ -53,6 +53,7 @@ import androidx.compose.ui.util.fastMap
|
|||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.entries.anime.model.episodesFiltered
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.entries.DownloadAction
|
||||
import eu.kanade.presentation.entries.EntryScreenItem
|
||||
import eu.kanade.presentation.entries.anime.components.AnimeActionRow
|
||||
|
@ -70,10 +71,9 @@ import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
|||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
|
||||
import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeSourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel
|
||||
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
|
@ -90,17 +90,15 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||
import tachiyomi.presentation.core.util.isScrollingUp
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import tachiyomi.source.local.entries.anime.isLocal
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Composable
|
||||
fun AnimeScreen(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fetchInterval: Int?,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
nextUpdate: Instant?,
|
||||
isTabletUi: Boolean,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
|
@ -156,16 +154,14 @@ fun AnimeScreen(
|
|||
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val onSettingsClicked: (() -> Unit)? = {
|
||||
navigator.push(SourcePreferencesScreen(state.source.id))
|
||||
navigator.push(AnimeSourcePreferencesScreen(state.source.id))
|
||||
}.takeIf { state.source is ConfigurableAnimeSource }
|
||||
|
||||
if (!isTabletUi) {
|
||||
AnimeScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||
|
@ -204,13 +200,11 @@ fun AnimeScreen(
|
|||
AnimeScreenLargeImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
nextUpdate = nextUpdate,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
onBackClicked = onBackClicked,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onDownloadEpisode = onDownloadEpisode,
|
||||
|
@ -249,9 +243,7 @@ fun AnimeScreen(
|
|||
private fun AnimeScreenSmallImpl(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
showNextEpisodeAirTime: Boolean,
|
||||
|
@ -455,7 +447,7 @@ private fun AnimeScreenSmallImpl(
|
|||
AnimeActionRow(
|
||||
favorite = state.anime.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
isUserIntervalMode = state.anime.fetchInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
|
@ -526,8 +518,6 @@ private fun AnimeScreenSmallImpl(
|
|||
anime = state.anime,
|
||||
episodes = listItem,
|
||||
isAnyEpisodeSelected = episodes.fastAny { it.selected },
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
|
@ -546,9 +536,7 @@ private fun AnimeScreenSmallImpl(
|
|||
fun AnimeScreenLargeImpl(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
showNextEpisodeAirTime: Boolean,
|
||||
|
@ -734,7 +722,7 @@ fun AnimeScreenLargeImpl(
|
|||
AnimeActionRow(
|
||||
favorite = state.anime.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
isUserIntervalMode = state.anime.fetchInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
|
@ -812,8 +800,6 @@ fun AnimeScreenLargeImpl(
|
|||
anime = state.anime,
|
||||
episodes = listItem,
|
||||
isAnyEpisodeSelected = episodes.fastAny { it.selected },
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
|
@ -884,8 +870,6 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
anime: Anime,
|
||||
episodes: List<EpisodeList>,
|
||||
isAnyEpisodeSelected: Boolean,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||
|
@ -904,7 +888,6 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
contentType = { EntryScreenItem.ITEM },
|
||||
) { episodeItem ->
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
|
||||
when (episodeItem) {
|
||||
is EpisodeList.MissingCount -> {
|
||||
|
@ -920,15 +903,7 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
} else {
|
||||
episodeItem.episode.name
|
||||
},
|
||||
date = episodeItem.episode.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
date = relativeDateText(episodeItem.episode.dateUpload),
|
||||
watchProgress = episodeItem.episode.lastSecondSeen
|
||||
.takeIf { !episodeItem.episode.seen && it > 0L }
|
||||
?.let {
|
||||
|
@ -942,7 +917,7 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
seen = episodeItem.episode.seen,
|
||||
bookmark = episodeItem.episode.bookmark,
|
||||
selected = episodeItem.selected,
|
||||
downloadIndicatorEnabled = !isAnyEpisodeSelected,
|
||||
downloadIndicatorEnabled = !isAnyEpisodeSelected && !anime.isLocal(),
|
||||
downloadStateProvider = { episodeItem.downloadState },
|
||||
downloadProgressProvider = { episodeItem.downloadProgress },
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
|
|
|
@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
|
@ -28,7 +29,7 @@ fun DuplicateAnimeDialog(
|
|||
},
|
||||
confirmButton = {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
|
|
@ -210,15 +210,13 @@ fun AnimeEpisodeListItem(
|
|||
}
|
||||
}
|
||||
|
||||
if (onDownloadClick != null) {
|
||||
EpisodeDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = onDownloadClick,
|
||||
)
|
||||
}
|
||||
EpisodeDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = { onDownloadClick?.invoke(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
@ -87,14 +88,14 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import kotlin.math.absoluteValue
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||
|
||||
@Composable
|
||||
fun AnimeInfoBox(
|
||||
modifier: Modifier = Modifier,
|
||||
isTabletUi: Boolean,
|
||||
appBarPadding: Dp,
|
||||
title: String,
|
||||
|
@ -106,6 +107,7 @@ fun AnimeInfoBox(
|
|||
status: Long,
|
||||
onCoverClick: () -> Unit,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
// Backdrop
|
||||
|
@ -164,10 +166,9 @@ fun AnimeInfoBox(
|
|||
|
||||
@Composable
|
||||
fun AnimeActionRow(
|
||||
modifier: Modifier = Modifier,
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
isUserIntervalMode: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
|
@ -175,9 +176,20 @@ fun AnimeActionRow(
|
|||
onTrackingClicked: () -> Unit,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
|
||||
// TODO: show something better when using custom interval
|
||||
val nextUpdateDays = remember(nextUpdate) {
|
||||
return@remember if (nextUpdate != null) {
|
||||
val now = Instant.now()
|
||||
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
AnimeActionButton(
|
||||
title = if (favorite) {
|
||||
|
@ -190,18 +202,20 @@ fun AnimeActionRow(
|
|||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
||||
AnimeActionButton(
|
||||
title = pluralStringResource(
|
||||
AnimeActionButton(
|
||||
title = if (nextUpdateDays != null) {
|
||||
pluralStringResource(
|
||||
MR.plurals.day,
|
||||
count = fetchInterval.absoluteValue,
|
||||
fetchInterval.absoluteValue,
|
||||
),
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onEditIntervalClicked,
|
||||
)
|
||||
}
|
||||
count = nextUpdateDays,
|
||||
nextUpdateDays,
|
||||
)
|
||||
} else {
|
||||
stringResource(MR.strings.not_applicable)
|
||||
},
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = { onEditIntervalClicked?.invoke() },
|
||||
)
|
||||
AnimeActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
stringResource(MR.strings.manga_tracking_tab)
|
||||
|
@ -227,12 +241,12 @@ fun AnimeActionRow(
|
|||
|
||||
@Composable
|
||||
fun ExpandableAnimeDescription(
|
||||
modifier: Modifier = Modifier,
|
||||
defaultExpandState: Boolean,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
onTagSearch: (String) -> Unit,
|
||||
onCopyTagToClipboard: (tag: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
val (expanded, onExpanded) = rememberSaveable {
|
||||
|
@ -288,7 +302,7 @@ fun ExpandableAnimeDescription(
|
|||
if (expanded) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
|
@ -304,7 +318,7 @@ fun ExpandableAnimeDescription(
|
|||
} else {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
|
@ -407,15 +421,15 @@ private fun AnimeAndSourceTitlesSmall(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeContentInfo(
|
||||
private fun ColumnScope.AnimeContentInfo(
|
||||
title: String,
|
||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
status: Long,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Text(
|
||||
|
@ -439,7 +453,7 @@ private fun AnimeContentInfo(
|
|||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
@ -470,7 +484,7 @@ private fun AnimeContentInfo(
|
|||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
@ -20,8 +20,8 @@ import tachiyomi.presentation.core.components.material.padding
|
|||
|
||||
@Composable
|
||||
fun BaseAnimeListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
anime: Anime,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItem: () -> Unit = {},
|
||||
onClickCover: () -> Unit = onClickItem,
|
||||
cover: @Composable RowScope.() -> Unit = { defaultCover(anime, onClickCover) },
|
||||
|
|
|
@ -46,10 +46,10 @@ enum class EpisodeDownloadAction {
|
|||
@Composable
|
||||
fun EpisodeDownloadIndicator(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
downloadStateProvider: () -> AnimeDownload.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
onClick: (EpisodeDownloadAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (val downloadState = downloadStateProvider()) {
|
||||
AnimeDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
||||
|
@ -106,10 +106,10 @@ private fun NotDownloadedIndicator(
|
|||
@Composable
|
||||
private fun DownloadingIndicator(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
downloadState: AnimeDownload.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
onClick: (EpisodeDownloadAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
|
|
|
@ -2,13 +2,24 @@ package eu.kanade.presentation.entries.components
|
|||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun DotSeparatorText() {
|
||||
Text(text = " • ")
|
||||
fun DotSeparatorText(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = " • ",
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DotSeparatorNoSpaceText() {
|
||||
Text(text = "•")
|
||||
fun DotSeparatorNoSpaceText(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = "•",
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -225,7 +225,10 @@ private fun RowScope.Button(
|
|||
onClick: () -> Unit,
|
||||
content: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
|
||||
val animatedWeight by animateFloatAsState(
|
||||
targetValue = if (toConfirm) 2f else 1f,
|
||||
label = "weight",
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
|
@ -262,13 +265,13 @@ private fun RowScope.Button(
|
|||
@Composable
|
||||
fun LibraryBottomActionMenu(
|
||||
visible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onChangeCategoryClicked: () -> Unit,
|
||||
onMarkAsViewedClicked: () -> Unit,
|
||||
onMarkAsUnviewedClicked: () -> Unit,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onDeleteClicked: () -> Unit,
|
||||
isManga: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
|
|
|
@ -20,7 +20,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
|
|
|
@ -22,8 +22,8 @@ enum class ItemCover(val ratio: Float) {
|
|||
|
||||
@Composable
|
||||
operator fun invoke(
|
||||
modifier: Modifier = Modifier,
|
||||
data: Any?,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String = "",
|
||||
shape: Shape = MaterialTheme.shapes.extraSmall,
|
||||
onClick: (() -> Unit)? = null,
|
||||
|
|
|
@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
|
@ -23,16 +24,17 @@ fun ItemHeader(
|
|||
missingItemsCount: Int,
|
||||
onClick: () -> Unit,
|
||||
isManga: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
Text(
|
||||
text = if (itemCount == null) {
|
||||
|
|
|
@ -1,23 +1,36 @@
|
|||
package eu.kanade.presentation.entries.components
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@Composable
|
||||
fun DeleteItemsDialog(
|
||||
|
@ -55,35 +68,74 @@ fun DeleteItemsDialog(
|
|||
@Composable
|
||||
fun SetIntervalDialog(
|
||||
interval: Int,
|
||||
nextUpdate: Instant?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onValueChanged: (Int) -> Unit,
|
||||
isManga: Boolean,
|
||||
onValueChanged: ((Int) -> Unit)? = null,
|
||||
) {
|
||||
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
||||
|
||||
val nextUpdateDays = remember(nextUpdate) {
|
||||
return@remember if (nextUpdate != null) {
|
||||
val now = Instant.now()
|
||||
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(MR.strings.manga_modify_calculated_interval_title)) },
|
||||
title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) },
|
||||
text = {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||
val items = (0..28)
|
||||
.map {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.label_default)
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
Column {
|
||||
if (nextUpdateDays != null && nextUpdateDays >= 0) {
|
||||
Text(
|
||||
stringResource(
|
||||
MR.strings.manga_interval_expected_update,
|
||||
pluralStringResource(
|
||||
MR.plurals.day,
|
||||
count = nextUpdateDays,
|
||||
nextUpdateDays,
|
||||
),
|
||||
pluralStringResource(
|
||||
MR.plurals.day,
|
||||
count = interval,
|
||||
interval,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(MaterialTheme.padding.small))
|
||||
}
|
||||
|
||||
// TODO: selecting "1" then doesn't allow for future changes unless defaulting first?
|
||||
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
||||
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||
val maxInterval = if (isManga) MangaFetchInterval.MAX_INTERVAL else AnimeFetchInterval.MAX_INTERVAL
|
||||
val items = (0..maxInterval)
|
||||
.map {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.label_default)
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
.toImmutableList()
|
||||
WheelTextPicker(
|
||||
items = items,
|
||||
size = size,
|
||||
startIndex = selectedInterval,
|
||||
onSelectionChanged = { selectedInterval = it },
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
WheelTextPicker(
|
||||
items = items,
|
||||
size = size,
|
||||
startIndex = selectedInterval,
|
||||
onSelectionChanged = { selectedInterval = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
|
@ -93,7 +145,7 @@ fun SetIntervalDialog(
|
|||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onValueChanged(selectedInterval)
|
||||
onValueChanged?.invoke(selectedInterval)
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
|
|
|
@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
|
@ -28,7 +29,7 @@ fun DuplicateMangaDialog(
|
|||
},
|
||||
confirmButton = {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
|
|
@ -49,6 +49,7 @@ import androidx.compose.ui.util.fastAny
|
|||
import androidx.compose.ui.util.fastMap
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.entries.DownloadAction
|
||||
import eu.kanade.presentation.entries.EntryScreenItem
|
||||
import eu.kanade.presentation.entries.components.EntryBottomActionMenu
|
||||
|
@ -67,7 +68,6 @@ import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo
|
|||
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.entries.manga.ChapterList
|
||||
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
|
@ -83,16 +83,14 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||
import tachiyomi.presentation.core.util.isScrollingUp
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import tachiyomi.source.local.entries.manga.isLocal
|
||||
import java.time.Instant
|
||||
|
||||
@Composable
|
||||
fun MangaScreen(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fetchInterval: Int?,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
nextUpdate: Instant?,
|
||||
isTabletUi: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -152,9 +150,7 @@ fun MangaScreen(
|
|||
MangaScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onBackClicked = onBackClicked,
|
||||
|
@ -190,11 +186,9 @@ fun MangaScreen(
|
|||
MangaScreenLargeImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
onBackClicked = onBackClicked,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
|
@ -231,9 +225,7 @@ fun MangaScreen(
|
|||
private fun MangaScreenSmallImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
@ -425,7 +417,7 @@ private fun MangaScreenSmallImpl(
|
|||
MangaActionRow(
|
||||
favorite = state.manga.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
isUserIntervalMode = state.manga.fetchInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
|
@ -469,8 +461,6 @@ private fun MangaScreenSmallImpl(
|
|||
manga = state.manga,
|
||||
chapters = listItem,
|
||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onChapterClicked = onChapterClicked,
|
||||
|
@ -488,9 +478,7 @@ private fun MangaScreenSmallImpl(
|
|||
fun MangaScreenLargeImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
@ -670,7 +658,7 @@ fun MangaScreenLargeImpl(
|
|||
MangaActionRow(
|
||||
favorite = state.manga.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
fetchInterval = fetchInterval,
|
||||
nextUpdate = nextUpdate,
|
||||
isUserIntervalMode = state.manga.fetchInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
|
@ -721,8 +709,6 @@ fun MangaScreenLargeImpl(
|
|||
manga = state.manga,
|
||||
chapters = listItem,
|
||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onChapterClicked = onChapterClicked,
|
||||
|
@ -775,7 +761,7 @@ private fun SharedMangaBottomActionMenu(
|
|||
onDeleteClicked = {
|
||||
onMultiDeleteClicked(selected.fastMap { it.chapter })
|
||||
}.takeIf {
|
||||
onDownloadChapter != null && selected.fastAny { it.downloadState == MangaDownload.State.DOWNLOADED }
|
||||
selected.fastAny { it.downloadState == MangaDownload.State.DOWNLOADED }
|
||||
},
|
||||
isManga = true,
|
||||
)
|
||||
|
@ -785,8 +771,6 @@ private fun LazyListScope.sharedChapterItems(
|
|||
manga: Manga,
|
||||
chapters: List<ChapterList>,
|
||||
isAnyChapterSelected: Boolean,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
|
@ -805,7 +789,6 @@ private fun LazyListScope.sharedChapterItems(
|
|||
contentType = { EntryScreenItem.ITEM },
|
||||
) { item ->
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
|
||||
when (item) {
|
||||
is ChapterList.MissingCount -> {
|
||||
|
@ -821,15 +804,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||
} else {
|
||||
item.chapter.name
|
||||
},
|
||||
date = item.chapter.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
date = relativeDateText(item.chapter.dateUpload),
|
||||
readProgress = item.chapter.lastPageRead
|
||||
.takeIf { !item.chapter.read && it > 0L }
|
||||
?.let {
|
||||
|
@ -842,7 +817,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||
read = item.chapter.read,
|
||||
bookmark = item.chapter.bookmark,
|
||||
selected = item.selected,
|
||||
downloadIndicatorEnabled = !isAnyChapterSelected,
|
||||
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
|
||||
downloadStateProvider = { item.downloadState },
|
||||
downloadProgressProvider = { item.downloadProgress },
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
|
|
|
@ -20,8 +20,8 @@ import tachiyomi.presentation.core.components.material.padding
|
|||
|
||||
@Composable
|
||||
fun BaseMangaListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
manga: Manga,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItem: () -> Unit = {},
|
||||
onClickCover: () -> Unit = onClickItem,
|
||||
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
|
||||
|
|
|
@ -45,10 +45,10 @@ enum class ChapterDownloadAction {
|
|||
@Composable
|
||||
fun ChapterDownloadIndicator(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
downloadStateProvider: () -> MangaDownload.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
onClick: (ChapterDownloadAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (val downloadState = downloadStateProvider()) {
|
||||
MangaDownload.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
|
||||
|
@ -105,10 +105,10 @@ private fun NotDownloadedIndicator(
|
|||
@Composable
|
||||
private fun DownloadingIndicator(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
downloadState: MangaDownload.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
onClick: (ChapterDownloadAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
|
|
|
@ -207,15 +207,13 @@ fun MangaChapterListItem(
|
|||
}
|
||||
}
|
||||
|
||||
if (onDownloadClick != null) {
|
||||
ChapterDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = onDownloadClick,
|
||||
)
|
||||
}
|
||||
ChapterDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = { onDownloadClick?.invoke(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
@ -87,7 +88,8 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import kotlin.math.absoluteValue
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||
|
@ -166,7 +168,7 @@ fun MangaInfoBox(
|
|||
fun MangaActionRow(
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
fetchInterval: Int?,
|
||||
nextUpdate: Instant?,
|
||||
isUserIntervalMode: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
|
@ -178,6 +180,16 @@ fun MangaActionRow(
|
|||
) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
|
||||
// TODO: show something better when using custom interval
|
||||
val nextUpdateDays = remember(nextUpdate) {
|
||||
return@remember if (nextUpdate != null) {
|
||||
val now = Instant.now()
|
||||
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
MangaActionButton(
|
||||
title = if (favorite) {
|
||||
|
@ -190,18 +202,20 @@ fun MangaActionRow(
|
|||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
||||
MangaActionButton(
|
||||
title = pluralStringResource(
|
||||
MangaActionButton(
|
||||
title = if (nextUpdateDays != null) {
|
||||
pluralStringResource(
|
||||
MR.plurals.day,
|
||||
count = fetchInterval.absoluteValue,
|
||||
fetchInterval.absoluteValue,
|
||||
),
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onEditIntervalClicked,
|
||||
)
|
||||
}
|
||||
count = nextUpdateDays,
|
||||
nextUpdateDays,
|
||||
)
|
||||
} else {
|
||||
stringResource(MR.strings.not_applicable)
|
||||
},
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = { onEditIntervalClicked?.invoke() },
|
||||
)
|
||||
MangaActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
stringResource(MR.strings.manga_tracking_tab)
|
||||
|
@ -287,7 +301,7 @@ fun ExpandableMangaDescription(
|
|||
if (expanded) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
|
@ -303,7 +317,7 @@ fun ExpandableMangaDescription(
|
|||
} else {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
|
@ -406,7 +420,7 @@ private fun MangaAndSourceTitlesSmall(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaContentInfo(
|
||||
private fun ColumnScope.MangaContentInfo(
|
||||
title: String,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
|
@ -438,7 +452,7 @@ private fun MangaContentInfo(
|
|||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
@ -469,7 +483,7 @@ private fun MangaContentInfo(
|
|||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.presentation.history
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -11,10 +12,10 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.random.Random
|
||||
|
||||
|
@ -32,7 +33,7 @@ fun HistoryDeleteDialog(
|
|||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
val subtitle = if (isManga) {
|
||||
MR.strings.dialog_with_checkbox_remove_description
|
|
@ -6,24 +6,20 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.components.RelativeDateHeader
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.history.anime.components.AnimeHistoryItem
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
|
||||
import tachiyomi.core.preference.InMemoryPreferenceStore
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
|
@ -33,7 +29,6 @@ fun AnimeHistoryScreen(
|
|||
onClickCover: (animeId: Long) -> Unit,
|
||||
onClickResume: (animeId: Long, episodeId: Long) -> Unit,
|
||||
onDialogChange: (AnimeHistoryScreenModel.Dialog?) -> Unit,
|
||||
preferences: UiPreferences = Injekt.get(),
|
||||
searchQuery: String? = null,
|
||||
) {
|
||||
Scaffold(
|
||||
|
@ -53,17 +48,12 @@ fun AnimeHistoryScreen(
|
|||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
} else {
|
||||
AnimeHistoryContent(
|
||||
AnimeHistoryScreenContent(
|
||||
history = it,
|
||||
contentPadding = contentPadding,
|
||||
onClickCover = { history -> onClickCover(history.animeId) },
|
||||
onClickResume = { history -> onClickResume(history.animeId, history.episodeId) },
|
||||
onClickDelete = { item ->
|
||||
onDialogChange(
|
||||
AnimeHistoryScreenModel.Dialog.Delete(item),
|
||||
)
|
||||
},
|
||||
preferences = preferences,
|
||||
onClickDelete = { item -> onDialogChange(AnimeHistoryScreenModel.Dialog.Delete(item)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -71,17 +61,13 @@ fun AnimeHistoryScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeHistoryContent(
|
||||
private fun AnimeHistoryScreenContent(
|
||||
history: List<AnimeHistoryUiModel>,
|
||||
contentPadding: PaddingValues,
|
||||
onClickCover: (AnimeHistoryWithRelations) -> Unit,
|
||||
onClickResume: (AnimeHistoryWithRelations) -> Unit,
|
||||
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
||||
preferences: UiPreferences,
|
||||
) {
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
|
@ -97,11 +83,9 @@ private fun AnimeHistoryContent(
|
|||
) { item ->
|
||||
when (item) {
|
||||
is AnimeHistoryUiModel.Header -> {
|
||||
RelativeDateHeader(
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
text = relativeDateText(item.date),
|
||||
)
|
||||
}
|
||||
is AnimeHistoryUiModel.Item -> {
|
||||
|
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
|
|||
onClickCover = {},
|
||||
onClickResume = { _, _ -> run {} },
|
||||
onDialogChange = {},
|
||||
preferences = UiPreferences(
|
||||
InMemoryPreferenceStore(
|
||||
sequenceOf(
|
||||
InMemoryPreferenceStore.InMemoryPreference(
|
||||
key = "relative_time_v2",
|
||||
data = false,
|
||||
defaultValue = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,24 +6,20 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.components.RelativeDateHeader
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.history.manga.components.MangaHistoryItem
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel
|
||||
import tachiyomi.core.preference.InMemoryPreferenceStore
|
||||
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
|
@ -33,7 +29,6 @@ fun MangaHistoryScreen(
|
|||
onClickCover: (mangaId: Long) -> Unit,
|
||||
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
||||
onDialogChange: (MangaHistoryScreenModel.Dialog?) -> Unit,
|
||||
preferences: UiPreferences = Injekt.get(),
|
||||
searchQuery: String? = null,
|
||||
) {
|
||||
Scaffold(
|
||||
|
@ -53,17 +48,12 @@ fun MangaHistoryScreen(
|
|||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
} else {
|
||||
MangaHistoryContent(
|
||||
MangaHistoryScreenContent(
|
||||
history = it,
|
||||
contentPadding = contentPadding,
|
||||
onClickCover = { history -> onClickCover(history.mangaId) },
|
||||
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
||||
onClickDelete = { item ->
|
||||
onDialogChange(
|
||||
MangaHistoryScreenModel.Dialog.Delete(item),
|
||||
)
|
||||
},
|
||||
preferences = preferences,
|
||||
onClickDelete = { item -> onDialogChange(MangaHistoryScreenModel.Dialog.Delete(item)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -71,17 +61,13 @@ fun MangaHistoryScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaHistoryContent(
|
||||
private fun MangaHistoryScreenContent(
|
||||
history: List<MangaHistoryUiModel>,
|
||||
contentPadding: PaddingValues,
|
||||
onClickCover: (MangaHistoryWithRelations) -> Unit,
|
||||
onClickResume: (MangaHistoryWithRelations) -> Unit,
|
||||
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
||||
preferences: UiPreferences,
|
||||
) {
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
|
@ -97,11 +83,9 @@ private fun MangaHistoryContent(
|
|||
) { item ->
|
||||
when (item) {
|
||||
is MangaHistoryUiModel.Header -> {
|
||||
RelativeDateHeader(
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
text = relativeDateText(item.date),
|
||||
)
|
||||
}
|
||||
is MangaHistoryUiModel.Item -> {
|
||||
|
@ -138,17 +122,6 @@ internal fun HistoryScreenPreviews(
|
|||
onClickCover = {},
|
||||
onClickResume = { _, _ -> run {} },
|
||||
onDialogChange = {},
|
||||
preferences = UiPreferences(
|
||||
InMemoryPreferenceStore(
|
||||
sequenceOf(
|
||||
InMemoryPreferenceStore.InMemoryPreference(
|
||||
key = "relative_time_v2",
|
||||
data = false,
|
||||
defaultValue = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import androidx.compose.ui.text.SpanStyle
|
|||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import tachiyomi.i18n.MR
|
||||
|
@ -42,7 +42,7 @@ fun NewUpdateScreen(
|
|||
rejectText = stringResource(MR.strings.action_not_now),
|
||||
onRejectClick = onRejectUpdate,
|
||||
) {
|
||||
Material3RichText(
|
||||
RichText(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.padding.large),
|
||||
|
@ -59,7 +59,7 @@ fun NewUpdateScreen(
|
|||
modifier = Modifier.padding(top = MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.update_check_open))
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.padding.extraSmall))
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,36 +15,42 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
internal fun GuidesStep(
|
||||
onRestoreBackup: () -> Unit,
|
||||
) {
|
||||
val handler = LocalUriHandler.current
|
||||
internal class GuidesStep(
|
||||
private val onRestoreBackup: () -> Unit,
|
||||
) : OnboardingStep {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { handler.openUri(GETTING_STARTED_URL) },
|
||||
override val isComplete: Boolean = true
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val handler = LocalUriHandler.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(stringResource(MR.strings.getting_started_guide))
|
||||
}
|
||||
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { handler.openUri(GETTING_STARTED_URL) },
|
||||
) {
|
||||
Text(stringResource(MR.strings.getting_started_guide))
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
|
||||
Text(stringResource(MR.strings.onboarding_guides_returning_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onRestoreBackup,
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_restore_backup))
|
||||
Text(stringResource(MR.strings.onboarding_guides_returning_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onRestoreBackup,
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_restore_backup))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +63,6 @@ private fun GuidesStepPreview() {
|
|||
TachiyomiTheme {
|
||||
GuidesStep(
|
||||
onRestoreBackup = {},
|
||||
)
|
||||
).Content()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,15 +13,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@ -29,24 +26,21 @@ import tachiyomi.presentation.core.screens.InfoScreen
|
|||
|
||||
@Composable
|
||||
fun OnboardingScreen(
|
||||
storagePreferences: StoragePreferences,
|
||||
uiPreferences: UiPreferences,
|
||||
onComplete: () -> Unit,
|
||||
onRestoreBackup: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val slideDistance = rememberSlideDistance()
|
||||
|
||||
var currentStep by remember { mutableIntStateOf(0) }
|
||||
val steps: List<@Composable () -> Unit> = remember {
|
||||
var currentStep by rememberSaveable { mutableIntStateOf(0) }
|
||||
val steps = remember {
|
||||
listOf(
|
||||
{ ThemeStep(uiPreferences = uiPreferences) },
|
||||
{ StorageStep(storagePref = storagePreferences.baseStorageDirectory()) },
|
||||
// TODO: prompt for notification permissions when bumping target to Android 13
|
||||
{ GuidesStep(onRestoreBackup = onRestoreBackup) },
|
||||
ThemeStep(),
|
||||
StorageStep(),
|
||||
PermissionStep(),
|
||||
GuidesStep(onRestoreBackup = onRestoreBackup),
|
||||
)
|
||||
}
|
||||
val isLastStep = currentStep == steps.size - 1
|
||||
val isLastStep = currentStep == steps.lastIndex
|
||||
|
||||
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
|
||||
|
||||
|
@ -61,16 +55,12 @@ fun OnboardingScreen(
|
|||
MR.strings.onboarding_action_next
|
||||
},
|
||||
),
|
||||
canAccept = steps[currentStep].isComplete,
|
||||
onAcceptClick = {
|
||||
if (isLastStep) {
|
||||
onComplete()
|
||||
} else {
|
||||
// TODO: this is kind of janky
|
||||
if (currentStep == 1 && !storagePreferences.baseStorageDirectory().isSet()) {
|
||||
context.toast(MR.strings.onboarding_storage_selection_required)
|
||||
} else {
|
||||
currentStep++
|
||||
}
|
||||
currentStep++
|
||||
}
|
||||
},
|
||||
) {
|
||||
|
@ -91,7 +81,7 @@ fun OnboardingScreen(
|
|||
},
|
||||
label = "stepContent",
|
||||
) {
|
||||
steps[it]()
|
||||
steps[it].Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package eu.kanade.presentation.more.onboarding
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
internal interface OnboardingStep {
|
||||
|
||||
val isComplete: Boolean
|
||||
|
||||
@Composable
|
||||
fun Content()
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package eu.kanade.presentation.more.onboarding
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
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 androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
|
||||
internal class PermissionStep : OnboardingStep {
|
||||
|
||||
private var notificationGranted by mutableStateOf(false)
|
||||
private var batteryGranted by mutableStateOf(false)
|
||||
|
||||
override val isComplete: Boolean = true
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState()
|
||||
|
||||
DisposableEffect(lifecycleOwner.lifecycle) {
|
||||
val observer = object : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
batteryGranted = context.getSystemService<PowerManager>()!!
|
||||
.isIgnoringBatteryOptimizations(context.packageName)
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
PermissionItem(
|
||||
title = stringResource(MR.strings.onboarding_permission_install_apps),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
|
||||
granted = installGranted,
|
||||
onButtonClick = {
|
||||
context.launchRequestPackageInstallsPermission()
|
||||
},
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permissionRequester = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission(),
|
||||
onResult = {
|
||||
// no-op. resulting checks is being done on resume
|
||||
},
|
||||
)
|
||||
PermissionItem(
|
||||
title = stringResource(MR.strings.onboarding_permission_notifications),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
|
||||
granted = notificationGranted,
|
||||
onButtonClick = { permissionRequester.launch(Manifest.permission.POST_NOTIFICATIONS) },
|
||||
)
|
||||
}
|
||||
|
||||
PermissionItem(
|
||||
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
|
||||
granted = batteryGranted,
|
||||
onButtonClick = {
|
||||
@SuppressLint("BatteryLife")
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.secondaryItemAlpha(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionItem(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
granted: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onButtonClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
headlineContent = { Text(text = title) },
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
trailingContent = {
|
||||
OutlinedButton(
|
||||
enabled = !granted,
|
||||
onClick = onButtonClick,
|
||||
) {
|
||||
if (granted) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(MR.strings.onboarding_permission_action_grant))
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,48 +5,70 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import tachiyomi.core.preference.Preference
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Button
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
internal fun StorageStep(
|
||||
storagePref: Preference<String>,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
||||
internal class StorageStep : OnboardingStep {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
MR.strings.onboarding_storage_info,
|
||||
stringResource(MR.strings.app_name),
|
||||
SettingsDataScreen.storageLocationText(storagePref),
|
||||
),
|
||||
)
|
||||
private val storagePref = Injekt.get<StoragePreferences>().baseStorageDirectory()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
try {
|
||||
pickStorageLocation.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(MR.strings.file_picker_error)
|
||||
}
|
||||
},
|
||||
private var _isComplete by mutableStateOf(false)
|
||||
|
||||
override val isComplete: Boolean
|
||||
get() = _isComplete
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_storage_action_select))
|
||||
Text(
|
||||
stringResource(
|
||||
MR.strings.onboarding_storage_info,
|
||||
stringResource(MR.strings.app_name),
|
||||
SettingsDataScreen.storageLocationText(storagePref),
|
||||
),
|
||||
)
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
try {
|
||||
pickStorageLocation.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(MR.strings.file_picker_error)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_storage_action_select))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
storagePref.changes()
|
||||
.collectLatest { _isComplete = storagePref.isSet() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,33 +8,40 @@ import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
|||
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
internal fun ThemeStep(
|
||||
uiPreferences: UiPreferences,
|
||||
) {
|
||||
val themeModePref = uiPreferences.themeMode()
|
||||
val themeMode by themeModePref.collectAsState()
|
||||
internal class ThemeStep : OnboardingStep {
|
||||
|
||||
val appThemePref = uiPreferences.appTheme()
|
||||
val appTheme by appThemePref.collectAsState()
|
||||
override val isComplete: Boolean = true
|
||||
|
||||
val amoledPref = uiPreferences.themeDarkAmoled()
|
||||
val amoled by amoledPref.collectAsState()
|
||||
private val uiPreferences: UiPreferences = Injekt.get()
|
||||
|
||||
Column {
|
||||
AppThemeModePreferenceWidget(
|
||||
value = themeMode,
|
||||
onItemClick = {
|
||||
themeModePref.set(it)
|
||||
setAppCompatDelegateThemeMode(it)
|
||||
},
|
||||
)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val themeModePref = uiPreferences.themeMode()
|
||||
val themeMode by themeModePref.collectAsState()
|
||||
|
||||
AppThemePreferenceWidget(
|
||||
value = appTheme,
|
||||
amoled = amoled,
|
||||
onItemClick = { appThemePref.set(it) },
|
||||
)
|
||||
val appThemePref = uiPreferences.appTheme()
|
||||
val appTheme by appThemePref.collectAsState()
|
||||
|
||||
val amoledPref = uiPreferences.themeDarkAmoled()
|
||||
val amoled by amoledPref.collectAsState()
|
||||
|
||||
Column {
|
||||
AppThemeModePreferenceWidget(
|
||||
value = themeMode,
|
||||
onItemClick = {
|
||||
themeModePref.set(it)
|
||||
setAppCompatDelegateThemeMode(it)
|
||||
},
|
||||
)
|
||||
|
||||
AppThemePreferenceWidget(
|
||||
value = appTheme,
|
||||
amoled = amoled,
|
||||
onItemClick = { appThemePref.set(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.core.preference.Preference as PreferenceData
|
||||
|
@ -64,13 +66,13 @@ sealed class Preference {
|
|||
val pref: PreferenceData<T>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: T, entries: Map<T, String>) -> String? =
|
||||
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
||||
|
||||
val entries: Map<T, String>,
|
||||
val entries: ImmutableMap<T, String>,
|
||||
) : PreferenceItem<T>() {
|
||||
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
||||
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(
|
||||
|
@ -78,8 +80,8 @@ sealed class Preference {
|
|||
)
|
||||
|
||||
@Composable
|
||||
internal fun internalSubtitleProvider(value: Any?, entries: Map<out Any?, String>) =
|
||||
subtitleProvider(value as T, entries as Map<T, String>)
|
||||
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
|
||||
subtitleProvider(value as T, entries as ImmutableMap<T, String>)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,13 +91,13 @@ sealed class Preference {
|
|||
val value: String,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: String, entries: Map<String, String>) -> String? =
|
||||
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
|
||||
val entries: Map<String, String>,
|
||||
val entries: ImmutableMap<String, String>,
|
||||
) : PreferenceItem<String>()
|
||||
|
||||
/**
|
||||
|
@ -106,7 +108,10 @@ sealed class Preference {
|
|||
val pref: PreferenceData<Set<String>>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: Set<String>, entries: Map<String, String>) -> String? = { v, e ->
|
||||
val subtitleProvider: @Composable (
|
||||
value: Set<String>,
|
||||
entries: ImmutableMap<String, String>,
|
||||
) -> String? = { v, e ->
|
||||
val combined = remember(v) {
|
||||
v.map { e[it] }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
|
@ -118,7 +123,7 @@ sealed class Preference {
|
|||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
||||
|
||||
val entries: Map<String, String>,
|
||||
val entries: ImmutableMap<String, String>,
|
||||
) : PreferenceItem<Set<String>>()
|
||||
|
||||
/**
|
||||
|
@ -184,6 +189,6 @@ sealed class Preference {
|
|||
override val title: String,
|
||||
override val enabled: Boolean = true,
|
||||
|
||||
val preferenceItems: List<PreferenceItem<out Any>>,
|
||||
val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
|
||||
) : Preference()
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
|||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.presentation.core.components.SliderItem
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -172,8 +171,8 @@ internal fun PreferenceItem(
|
|||
)
|
||||
}
|
||||
is Preference.PreferenceItem.TrackerPreference -> {
|
||||
val uName by Injekt.get<PreferenceStore>()
|
||||
.getString(TrackPreferences.trackUsername(item.tracker.id))
|
||||
val uName by Injekt.get<TrackPreferences>()
|
||||
.trackUsername(item.tracker)
|
||||
.collectAsState()
|
||||
item.tracker.run {
|
||||
TrackingPreferenceWidget(
|
||||
|
|
|
@ -7,6 +7,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
||||
import kotlinx.collections.immutable.immutableMapOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -49,7 +51,7 @@ object AdvancedPlayerSettingsScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
title = context.stringResource(MR.strings.pref_debanding_title),
|
||||
pref = playerPreferences.deband(),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
0 to context.stringResource(MR.strings.pref_debanding_disabled),
|
||||
1 to context.stringResource(MR.strings.pref_debanding_cpu),
|
||||
2 to context.stringResource(MR.strings.pref_debanding_gpu),
|
||||
|
|
|
@ -11,7 +11,6 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||
/**
|
||||
* Returns a string of categories name for settings subtitle
|
||||
*/
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
fun getCategoriesLabel(
|
||||
|
|
|
@ -32,8 +32,10 @@ import eu.kanade.presentation.more.settings.screen.advanced.ClearDatabaseScreen
|
|||
import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.library.anime.AnimeMetadataUpdateJob
|
||||
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.library.manga.MangaMetadataUpdateJob
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
|
@ -51,12 +53,18 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
|||
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.immutableListOf
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import okhttp3.Headers
|
||||
|
@ -159,7 +167,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_background_activity),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_disable_battery_optimization),
|
||||
subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary),
|
||||
|
@ -200,7 +208,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_data),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_invalidate_download_cache),
|
||||
subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary),
|
||||
|
@ -236,7 +244,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_network),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_clear_cookies),
|
||||
onClick = {
|
||||
|
@ -267,7 +275,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = networkPreferences.dohProvider(),
|
||||
title = stringResource(MR.strings.pref_dns_over_https),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
||||
PREF_DOH_GOOGLE to "Google",
|
||||
|
@ -317,16 +325,17 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_library),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_refresh_library_covers),
|
||||
onClick = {
|
||||
AnimeLibraryUpdateJob.startNow(context)
|
||||
MangaLibraryUpdateJob.startNow(context)
|
||||
AnimeMetadataUpdateJob.startNow(context)
|
||||
MangaMetadataUpdateJob.startNow(context)
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
|
@ -388,12 +397,21 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
}
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_extensions),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = extensionInstallerPref,
|
||||
title = stringResource(MR.strings.ext_installer_pref),
|
||||
entries = extensionInstallerPref.entries
|
||||
.associateWith { stringResource(it.titleRes) },
|
||||
.filter {
|
||||
// TODO: allow private option in stable versions once URL handling is more fleshed out
|
||||
if (isPreviewBuildType || isDevFlavor) {
|
||||
true
|
||||
} else {
|
||||
it != BasePreferences.ExtensionInstaller.PRIVATE
|
||||
}
|
||||
}
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
onValueChanged = {
|
||||
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
|
||||
!context.isShizukuInstalled
|
||||
|
@ -416,12 +434,12 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
val dataSaver by sourcePreferences.dataSaver().collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.data_saver),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = sourcePreferences.dataSaver(),
|
||||
title = stringResource(MR.strings.data_saver),
|
||||
subtitle = stringResource(MR.strings.data_saver_summary),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
DataSaver.NONE to stringResource(MR.strings.disabled),
|
||||
DataSaver.BANDWIDTH_HERO to stringResource(MR.strings.bandwidth_hero),
|
||||
DataSaver.WSRV_NL to stringResource(MR.strings.wsrv),
|
||||
|
@ -462,7 +480,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
"80%",
|
||||
"90%",
|
||||
"95%",
|
||||
).associateBy { it.trimEnd('%').toInt() },
|
||||
).associateBy { it.trimEnd('%').toInt() }.toPersistentMap(),
|
||||
enabled = dataSaver != DataSaver.NONE,
|
||||
),
|
||||
kotlin.run {
|
||||
|
|
|
@ -26,6 +26,11 @@ import eu.kanade.tachiyomi.ui.home.HomeScreen
|
|||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
|
@ -69,7 +74,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_theme),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.CustomPreference(
|
||||
title = stringResource(MR.strings.pref_app_theme),
|
||||
) {
|
||||
|
@ -149,11 +154,11 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPrefs.bottomNavStyle(),
|
||||
title = stringResource(MR.strings.pref_bottom_nav_style),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.pref_bottom_nav_no_history),
|
||||
1 to stringResource(MR.strings.pref_bottom_nav_no_updates),
|
||||
2 to stringResource(MR.strings.pref_bottom_nav_no_manga),
|
||||
|
@ -176,7 +181,9 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.tabletUiMode(),
|
||||
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
||||
entries = TabletUiMode.entries.associateWith { stringResource(it.titleRes) },
|
||||
entries = TabletUiMode.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
onValueChanged = {
|
||||
context.stringResource(MR.strings.requires_app_restart)
|
||||
true
|
||||
|
@ -189,7 +196,8 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
.associateWith {
|
||||
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
||||
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
|
||||
},
|
||||
}
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = uiPreferences.relativeTime(),
|
||||
|
@ -203,7 +211,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
),
|
||||
)
|
||||
}
|
||||
private fun getLangs(context: Context): Map<String, String> {
|
||||
private fun getLangs(context: Context): ImmutableMap<String, String> {
|
||||
val langs = mutableListOf<Pair<String, String>>()
|
||||
val parser = context.resources.getXml(R.xml.locales_config)
|
||||
var eventType = parser.eventType
|
||||
|
@ -225,7 +233,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
langs.sortBy { it.second }
|
||||
langs.add(0, Pair("", context.stringResource(MR.strings.label_default)))
|
||||
|
||||
return langs.toMap()
|
||||
return langs.toMap().toImmutableMap()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,15 +2,23 @@ package eu.kanade.presentation.more.settings.screen
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.browse.AnimeExtensionReposScreen
|
||||
import eu.kanade.presentation.more.settings.screen.browse.MangaExtensionReposScreen
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
@ -23,11 +31,16 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
||||
val mangaReposCount by sourcePreferences.mangaExtensionRepos().collectAsState()
|
||||
val animeReposCount by sourcePreferences.animeExtensionRepos().collectAsState()
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_sources),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.hideInAnimeLibraryItems(),
|
||||
title = stringResource(MR.strings.pref_hide_in_anime_library_items),
|
||||
|
@ -36,11 +49,25 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||
pref = sourcePreferences.hideInMangaLibraryItems(),
|
||||
title = stringResource(MR.strings.pref_hide_in_manga_library_items),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.label_anime_extension_repos),
|
||||
subtitle = pluralStringResource(MR.plurals.num_repos, animeReposCount.size, animeReposCount.size),
|
||||
onClick = {
|
||||
navigator.push(AnimeExtensionReposScreen())
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.label_manga_extension_repos),
|
||||
subtitle = pluralStringResource(MR.plurals.num_repos, mangaReposCount.size, mangaReposCount.size),
|
||||
onClick = {
|
||||
navigator.push(MangaExtensionReposScreen())
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_nsfw_content),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.showNsfwSource(),
|
||||
title = stringResource(MR.strings.pref_show_nsfw_source),
|
||||
|
|
|
@ -4,25 +4,19 @@ import android.content.ActivityNotFoundException
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.text.format.Formatter
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
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
|
||||
|
@ -34,33 +28,25 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
|||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.StorageInfo
|
||||
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||
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.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.EpisodeCache
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||
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.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.core.storage.displayablePath
|
||||
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.backup.service.FLAG_CATEGORIES
|
||||
import tachiyomi.domain.backup.service.FLAG_CHAPTERS
|
||||
import tachiyomi.domain.backup.service.FLAG_EXTENSIONS
|
||||
import tachiyomi.domain.backup.service.FLAG_EXT_SETTINGS
|
||||
import tachiyomi.domain.backup.service.FLAG_HISTORY
|
||||
import tachiyomi.domain.backup.service.FLAG_SETTINGS
|
||||
import tachiyomi.domain.backup.service.FLAG_TRACK
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
|
@ -71,6 +57,8 @@ import uy.kohesive.injekt.api.get
|
|||
|
||||
object SettingsDataScreen : SearchableSettings {
|
||||
|
||||
val restorePreferenceKeyString = MR.strings.label_backup
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitleRes() = MR.strings.label_data_storage
|
||||
|
@ -80,12 +68,12 @@ object SettingsDataScreen : SearchableSettings {
|
|||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
val storagePreferences = Injekt.get<StoragePreferences>()
|
||||
|
||||
return listOf(
|
||||
return persistentListOf(
|
||||
getStorageLocationPref(storagePreferences = storagePreferences),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
|
||||
|
||||
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
||||
getDataGroup(backupPreferences = backupPreferences),
|
||||
getDataGroup(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -107,8 +95,6 @@ object SettingsDataScreen : SearchableSettings {
|
|||
UniFile.fromUri(context, uri)?.let {
|
||||
storageDirPref.set(it.uri.toString())
|
||||
}
|
||||
Injekt.get<AnimeDownloadCache>().invalidateCache()
|
||||
Injekt.get<MangaDownloadCache>().invalidateCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +112,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||
|
||||
return remember(storageDir) {
|
||||
val file = UniFile.fromUri(context, storageDir.toUri())
|
||||
file?.filePath ?: file?.uri?.toString()
|
||||
file?.displayablePath
|
||||
} ?: stringResource(MR.strings.invalid_location, storageDir)
|
||||
}
|
||||
|
||||
|
@ -153,20 +139,75 @@ object SettingsDataScreen : SearchableSettings {
|
|||
@Composable
|
||||
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
||||
|
||||
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.stringResource(MR.strings.file_select_backup))
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (it == null) {
|
||||
context.toast(MR.strings.file_null_uri_error)
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
navigator.push(RestoreBackupScreen(it.toString()))
|
||||
}
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_backup),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
// Manual actions
|
||||
getCreateBackupPref(),
|
||||
getRestoreBackupPref(),
|
||||
Preference.PreferenceItem.CustomPreference(
|
||||
title = stringResource(restorePreferenceKeyString),
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
subcomponent = {
|
||||
MultiChoiceSegmentedButtonRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = PrefsHorizontalPadding),
|
||||
) {
|
||||
SegmentedButton(
|
||||
checked = false,
|
||||
onCheckedChange = { navigator.push(CreateBackupScreen()) },
|
||||
shape = SegmentedButtonDefaults.itemShape(0, 2),
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_create_backup))
|
||||
}
|
||||
SegmentedButton(
|
||||
checked = false,
|
||||
onCheckedChange = {
|
||||
if (!BackupRestoreJob.isRunning(context)) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
context.toast(MR.strings.restore_miui_warning)
|
||||
}
|
||||
|
||||
// no need to catch because it's wrapped with a chooser
|
||||
chooseBackup.launch("*/*")
|
||||
} else {
|
||||
context.toast(MR.strings.restore_in_progress)
|
||||
}
|
||||
},
|
||||
shape = SegmentedButtonDefaults.itemShape(1, 2),
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_restore_backup))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
// Automatic backups
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = backupPreferences.backupInterval(),
|
||||
title = stringResource(MR.strings.pref_backup_interval),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.off),
|
||||
6 to stringResource(MR.strings.update_6hour),
|
||||
12 to stringResource(MR.strings.update_12hour),
|
||||
|
@ -188,181 +229,44 @@ object SettingsDataScreen : SearchableSettings {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_create_backup),
|
||||
subtitle = stringResource(MR.strings.pref_create_backup_summ),
|
||||
onClick = { navigator.push(CreateBackupScreen()) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
|
||||
private fun getDataGroup(): Preference.PreferenceGroup {
|
||||
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(MR.strings.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(MR.strings.action_copy_to_clipboard))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is MissingRestoreComponents -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
val msg = buildString {
|
||||
append(stringResource(MR.strings.backup_restore_content_full))
|
||||
if (err.sources.isNotEmpty()) {
|
||||
append("\n\n").append(
|
||||
stringResource(MR.strings.backup_restore_missing_sources),
|
||||
)
|
||||
err.sources.joinTo(
|
||||
this,
|
||||
separator = "\n- ",
|
||||
prefix = "\n- ",
|
||||
)
|
||||
}
|
||||
if (err.trackers.isNotEmpty()) {
|
||||
append("\n\n").append(
|
||||
stringResource(MR.strings.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(MR.strings.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.stringResource(MR.strings.file_select_backup),
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (it == null) {
|
||||
context.stringResource(MR.strings.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(MR.strings.pref_restore_backup),
|
||||
subtitle = stringResource(MR.strings.pref_restore_backup_summ),
|
||||
onClick = {
|
||||
if (!BackupRestoreJob.isRunning(context)) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
context.stringResource(MR.strings.restore_miui_warning, Toast.LENGTH_LONG)
|
||||
}
|
||||
// no need to catch because it's wrapped with a chooser
|
||||
chooseBackup.launch("*/*")
|
||||
} else {
|
||||
context.stringResource(MR.strings.restore_in_progress)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDataGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||
|
||||
val backupIntervalPref = backupPreferences.backupInterval()
|
||||
val backupInterval by backupIntervalPref.collectAsState()
|
||||
|
||||
val chapterCache = remember { Injekt.get<ChapterCache>() }
|
||||
val episodeCache = remember { Injekt.get<EpisodeCache>() }
|
||||
var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
|
||||
val cacheReadableMangaSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
|
||||
val cacheReadableAnimeSize = remember(cacheReadableSizeSema) { episodeCache.readableSize }
|
||||
val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.label_data),
|
||||
preferenceItems = listOf(
|
||||
getMangaStorageInfoPref(cacheReadableMangaSize),
|
||||
getAnimeStorageInfoPref(cacheReadableAnimeSize),
|
||||
title = stringResource(MR.strings.pref_storage_usage),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.CustomPreference(
|
||||
title = stringResource(MR.strings.pref_storage_usage),
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
subcomponent = {
|
||||
StorageInfo(
|
||||
modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_clear_chapter_cache),
|
||||
subtitle = stringResource(
|
||||
MR.strings.used_cache_both,
|
||||
cacheReadableAnimeSize,
|
||||
cacheReadableMangaSize,
|
||||
),
|
||||
subtitle = stringResource(MR.strings.used_cache, cacheReadableSize),
|
||||
onClick = {
|
||||
scope.launchNonCancellable {
|
||||
try {
|
||||
val deletedFiles = chapterCache.clear() + episodeCache.clear()
|
||||
val deletedFiles = chapterCache.clear()
|
||||
withUIContext {
|
||||
context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles))
|
||||
cacheReadableSizeSema++
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
withUIContext { context.stringResource(MR.strings.cache_delete_error) }
|
||||
withUIContext { context.toast(MR.strings.cache_delete_error) }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -371,93 +275,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||
pref = libraryPreferences.autoClearItemCache(),
|
||||
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = backupPreferences.backupFlags(),
|
||||
enabled = backupInterval != 0,
|
||||
title = stringResource(MR.strings.pref_backup_flags),
|
||||
subtitle = stringResource(MR.strings.pref_backup_flags_summary),
|
||||
entries = mapOf(
|
||||
FLAG_CATEGORIES to stringResource(MR.strings.general_categories),
|
||||
FLAG_CHAPTERS to stringResource(MR.strings.chapters_episodes),
|
||||
FLAG_HISTORY to stringResource(MR.strings.history),
|
||||
FLAG_TRACK to stringResource(MR.strings.track),
|
||||
FLAG_SETTINGS to stringResource(MR.strings.settings),
|
||||
FLAG_EXT_SETTINGS to stringResource(MR.strings.extension_settings),
|
||||
FLAG_EXTENSIONS to stringResource(MR.strings.label_extensions),
|
||||
),
|
||||
onValueChanged = {
|
||||
if (FLAG_SETTINGS in it || FLAG_EXT_SETTINGS in it) {
|
||||
context.stringResource(MR.strings.backup_settings_warning, Toast.LENGTH_LONG)
|
||||
}
|
||||
true
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getMangaStorageInfoPref(
|
||||
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(MR.strings.pref_manga_storage_usage),
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
title = stringResource(MR.strings.pref_manga_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
|
||||
fun getAnimeStorageInfoPref(
|
||||
episodeCacheReadableSize: 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(MR.strings.pref_anime_storage_usage),
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
title = stringResource(MR.strings.pref_anime_storage_usage),
|
||||
subcomponent = {
|
||||
// TODO: downloads, SD cards, bar representation?, i18n
|
||||
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
|
||||
Text(text = "Available: $available / $total (Episode cache: $episodeCacheReadableSize)")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
|
|
|
@ -13,6 +13,11 @@ import eu.kanade.domain.base.BasePreferences
|
|||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
|
||||
|
@ -62,7 +67,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.numberOfDownloads(),
|
||||
title = stringResource(MR.strings.pref_download_slots),
|
||||
entries = (1..5).associateWith { it.toString() },
|
||||
entries = (1..5).associateWith { it.toString() }.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_slots_info)),
|
||||
getDeleteChaptersGroup(
|
||||
|
@ -89,7 +94,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_delete_chapters),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.removeAfterMarkedAsRead(),
|
||||
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
|
||||
|
@ -97,7 +102,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.removeAfterReadSlots(),
|
||||
title = stringResource(MR.strings.pref_remove_after_read),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
0 to stringResource(MR.strings.last_read_chapter),
|
||||
1 to stringResource(MR.strings.second_to_last),
|
||||
|
@ -126,7 +131,9 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = downloadPreferences.removeExcludeCategories(),
|
||||
title = stringResource(MR.strings.pref_remove_exclude_categories_manga),
|
||||
entries = categories().associate { it.id.toString() to it.visualName },
|
||||
entries = categories()
|
||||
.associate { it.id.toString() to it.visualName }
|
||||
.toImmutableMap(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -198,7 +205,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_auto_download),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadNewEpisodesPref,
|
||||
title = stringResource(MR.strings.pref_download_new_episodes),
|
||||
|
@ -237,36 +244,32 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.download_ahead),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.autoDownloadWhileReading(),
|
||||
title = stringResource(MR.strings.auto_download_while_reading),
|
||||
entries = listOf(0, 2, 3, 5, 10).associateWith {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.disabled)
|
||||
} else {
|
||||
pluralStringResource(
|
||||
MR.plurals.next_unread_chapters,
|
||||
count = it,
|
||||
it,
|
||||
)
|
||||
entries = listOf(0, 2, 3, 5, 10)
|
||||
.associateWith {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.disabled)
|
||||
} else {
|
||||
pluralStringResource(MR.plurals.next_unread_chapters, count = it, it)
|
||||
}
|
||||
}
|
||||
},
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.autoDownloadWhileWatching(),
|
||||
title = stringResource(MR.strings.auto_download_while_watching),
|
||||
entries = listOf(0, 2, 3, 5, 10).associateWith {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.disabled)
|
||||
} else {
|
||||
pluralStringResource(
|
||||
MR.plurals.next_unseen_episodes,
|
||||
count = it,
|
||||
it,
|
||||
)
|
||||
entries = listOf(0, 2, 3, 5, 10)
|
||||
.associateWith {
|
||||
if (it == 0) {
|
||||
stringResource(MR.strings.disabled)
|
||||
} else {
|
||||
pluralStringResource(MR.plurals.next_unseen_episodes, count = it, it)
|
||||
}
|
||||
}
|
||||
},
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(
|
||||
stringResource(MR.strings.download_ahead_info),
|
||||
|
@ -299,12 +302,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
.map { pm.getApplicationLabel(it.applicationInfo).toString() }
|
||||
|
||||
val packageNamesMap: Map<String, String> =
|
||||
packageNames.zip(packageNamesReadable)
|
||||
.toMap()
|
||||
mapOf("" to "None") + packageNames.zip(packageNamesReadable).toMap()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_external_downloader),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = useExternalDownloader,
|
||||
title = stringResource(MR.strings.pref_use_external_downloader),
|
||||
|
@ -312,7 +314,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = externalDownloaderPreference,
|
||||
title = stringResource(MR.strings.pref_external_downloader_selection),
|
||||
entries = mapOf("" to "None") + packageNamesMap,
|
||||
entries = packageNamesMap.toPersistentMap(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -21,6 +21,9 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
|||
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.ui.category.CategoriesTab
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||
|
@ -78,7 +81,6 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
allAnimeCategories: List<Category>,
|
||||
libraryPreferences: LibraryPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
||||
val userAnimeCategoriesCount = allAnimeCategories.filterNot(Category::isSystemCategory).size
|
||||
|
@ -96,13 +98,13 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
allAnimeCategories.fastMap { it.id.toInt() }
|
||||
|
||||
val mangaLabels = listOf(stringResource(MR.strings.default_category_summary)) +
|
||||
allCategories.fastMap { it.visualName(context) }
|
||||
allCategories.fastMap { it.visualName }
|
||||
val animeLabels = listOf(stringResource(MR.strings.default_category_summary)) +
|
||||
allAnimeCategories.fastMap { it.visualName(context) }
|
||||
allAnimeCategories.fastMap { it.visualName }
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.general_categories),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.action_edit_anime_categories),
|
||||
subtitle = pluralStringResource(
|
||||
|
@ -117,7 +119,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
title = stringResource(MR.strings.default_anime_category),
|
||||
subtitle = selectedAnimeCategory?.visualName
|
||||
?: stringResource(MR.strings.default_category_summary),
|
||||
entries = animeIds.zip(animeLabels).toMap(),
|
||||
entries = animeIds.zip(animeLabels).toMap().toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.action_edit_manga_categories),
|
||||
|
@ -131,9 +133,8 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.defaultMangaCategory(),
|
||||
title = stringResource(MR.strings.default_manga_category),
|
||||
subtitle = selectedCategory?.visualName
|
||||
?: stringResource(MR.strings.default_category_summary),
|
||||
entries = mangaIds.zip(mangaLabels).toMap(),
|
||||
subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
|
||||
entries = mangaIds.zip(mangaLabels).toMap().toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.categorizedDisplaySettings(),
|
||||
|
@ -222,11 +223,11 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_library_update),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = autoUpdateIntervalPref,
|
||||
title = stringResource(MR.strings.pref_library_update_interval),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.update_never),
|
||||
12 to stringResource(MR.strings.update_12hour),
|
||||
24 to stringResource(MR.strings.update_24hour),
|
||||
|
@ -245,7 +246,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
enabled = autoUpdateInterval > 0,
|
||||
title = stringResource(MR.strings.pref_library_update_restriction),
|
||||
subtitle = stringResource(MR.strings.restrictions),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
|
||||
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
|
||||
DEVICE_CHARGING to stringResource(MR.strings.charging),
|
||||
|
@ -284,18 +285,12 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryPreferences.autoUpdateItemRestrictions(),
|
||||
title = stringResource(MR.strings.pref_library_update_manga_restriction),
|
||||
entries = mapOf(
|
||||
ENTRY_HAS_UNVIEWED to stringResource(
|
||||
MR.strings.pref_update_only_completely_read,
|
||||
),
|
||||
title = stringResource(MR.strings.pref_library_update_smart_update),
|
||||
entries = persistentMapOf(
|
||||
ENTRY_HAS_UNVIEWED to stringResource(MR.strings.pref_update_only_completely_read),
|
||||
ENTRY_NON_VIEWED to stringResource(MR.strings.pref_update_only_started),
|
||||
ENTRY_NON_COMPLETED to stringResource(
|
||||
MR.strings.pref_update_only_non_completed,
|
||||
),
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(
|
||||
MR.strings.pref_update_only_in_release_period,
|
||||
),
|
||||
ENTRY_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
|
@ -312,41 +307,33 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_chapter_swipe),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeChapterStartAction(),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
||||
entries = mapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(
|
||||
MR.strings.action_disable,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(
|
||||
MR.strings.action_bookmark,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(
|
||||
MR.strings.action_mark_as_read,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
|
||||
MR.strings.action_download,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
||||
stringResource(MR.strings.action_bookmark),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to
|
||||
stringResource(MR.strings.action_mark_as_read),
|
||||
LibraryPreferences.ChapterSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeChapterEndAction(),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
||||
entries = mapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(
|
||||
MR.strings.action_disable,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(
|
||||
MR.strings.action_bookmark,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(
|
||||
MR.strings.action_mark_as_read,
|
||||
),
|
||||
LibraryPreferences.ChapterSwipeAction.Download to stringResource(
|
||||
MR.strings.action_download,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
|
||||
stringResource(MR.strings.action_bookmark),
|
||||
LibraryPreferences.ChapterSwipeAction.ToggleRead to
|
||||
stringResource(MR.strings.action_mark_as_read),
|
||||
LibraryPreferences.ChapterSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -359,41 +346,33 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_episode_swipe),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeEpisodeStartAction(),
|
||||
title = stringResource(MR.strings.pref_episode_swipe_start),
|
||||
entries = mapOf(
|
||||
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(
|
||||
MR.strings.action_disable,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(
|
||||
MR.strings.action_bookmark_episode,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource(
|
||||
MR.strings.action_mark_as_seen,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
|
||||
MR.strings.action_download,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.EpisodeSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
|
||||
stringResource(MR.strings.action_bookmark_episode),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
|
||||
stringResource(MR.strings.action_mark_as_seen),
|
||||
LibraryPreferences.EpisodeSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeEpisodeEndAction(),
|
||||
title = stringResource(MR.strings.pref_episode_swipe_end),
|
||||
entries = mapOf(
|
||||
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(
|
||||
MR.strings.action_disable,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(
|
||||
MR.strings.action_bookmark_episode,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to stringResource(
|
||||
MR.strings.action_mark_as_seen,
|
||||
),
|
||||
LibraryPreferences.EpisodeSwipeAction.Download to stringResource(
|
||||
MR.strings.action_download,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.EpisodeSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to
|
||||
stringResource(MR.strings.action_bookmark_episode),
|
||||
LibraryPreferences.EpisodeSwipeAction.ToggleSeen to
|
||||
stringResource(MR.strings.action_mark_as_seen),
|
||||
LibraryPreferences.EpisodeSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -33,7 +33,10 @@ import eu.kanade.tachiyomi.ui.player.VLC_PLAYER
|
|||
import eu.kanade.tachiyomi.ui.player.WEB_VIDEO_CASTER
|
||||
import eu.kanade.tachiyomi.ui.player.X_PLAYER
|
||||
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@ -57,7 +60,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = playerPreferences.progressPreference(),
|
||||
title = stringResource(MR.strings.pref_progress_mark_as_seen),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
1.00F to stringResource(MR.strings.pref_progress_100),
|
||||
0.95F to stringResource(MR.strings.pref_progress_95),
|
||||
0.90F to stringResource(MR.strings.pref_progress_90),
|
||||
|
@ -91,7 +94,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_internal_player),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = playerFullscreen,
|
||||
title = stringResource(MR.strings.pref_player_fullscreen),
|
||||
|
@ -118,7 +121,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_volume_brightness),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = enableVolumeBrightnessGestures,
|
||||
title = stringResource(MR.strings.enable_volume_brightness_gestures),
|
||||
|
@ -144,11 +147,11 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_player_orientation),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = defaultPlayerOrientationType,
|
||||
title = stringResource(MR.strings.pref_default_player_orientation),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR to stringResource(
|
||||
MR.strings.rotation_free,
|
||||
),
|
||||
|
@ -179,7 +182,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = defaultPlayerOrientationPortrait,
|
||||
title = stringResource(MR.strings.pref_default_portrait_orientation),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT to stringResource(
|
||||
MR.strings.rotation_portrait,
|
||||
),
|
||||
|
@ -194,7 +197,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = defaultPlayerOrientationLandscape,
|
||||
title = stringResource(MR.strings.pref_default_landscape_orientation),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE to stringResource(
|
||||
MR.strings.rotation_landscape,
|
||||
),
|
||||
|
@ -241,7 +244,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_player_seeking),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = enableHorizontalSeekGesture,
|
||||
title = stringResource(MR.strings.enable_horizontal_seek_gesture),
|
||||
|
@ -254,7 +257,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = skipLengthPreference,
|
||||
title = stringResource(MR.strings.pref_skip_length),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
30 to stringResource(MR.strings.pref_skip_30),
|
||||
20 to stringResource(MR.strings.pref_skip_20),
|
||||
10 to stringResource(MR.strings.pref_skip_10),
|
||||
|
@ -293,7 +296,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = waitingTimeAniSkip,
|
||||
title = stringResource(MR.strings.pref_waiting_time_aniskip),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
5 to stringResource(MR.strings.pref_waiting_time_aniskip_5),
|
||||
6 to stringResource(MR.strings.pref_waiting_time_aniskip_6),
|
||||
7 to stringResource(MR.strings.pref_waiting_time_aniskip_7),
|
||||
|
@ -318,7 +321,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_pip),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = enablePip,
|
||||
title = stringResource(MR.strings.pref_enable_pip),
|
||||
|
@ -364,7 +367,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_external_player),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = alwaysUseExternalPlayer,
|
||||
title = stringResource(MR.strings.pref_always_use_external_player),
|
||||
|
@ -372,7 +375,7 @@ object SettingsPlayerScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = externalPlayerPreference,
|
||||
title = stringResource(MR.strings.pref_external_player_preference),
|
||||
entries = mapOf("" to "None") + packageNamesMap,
|
||||
entries = (mapOf("" to "None") + packageNamesMap).toPersistentMap(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -10,6 +10,9 @@ import eu.kanade.presentation.more.settings.Preference
|
|||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
@ -31,12 +34,13 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
pref = readerPref.defaultReadingMode(),
|
||||
title = stringResource(MR.strings.pref_viewer_type),
|
||||
entries = ReadingMode.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) },
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.doubleTapAnimSpeed(),
|
||||
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.double_tap_anim_speed_0),
|
||||
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
|
||||
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
|
||||
|
@ -82,17 +86,18 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
val fullscreen by fullscreenPref.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.defaultOrientationType(),
|
||||
title = stringResource(MR.strings.pref_rotation_type),
|
||||
entries = ReaderOrientation.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) },
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerTheme(),
|
||||
title = stringResource(MR.strings.pref_reader_theme),
|
||||
entries = mapOf(
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.black_background),
|
||||
2 to stringResource(MR.strings.gray_background),
|
||||
0 to stringResource(MR.strings.white_background),
|
||||
|
@ -126,7 +131,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_reading),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.skipRead(),
|
||||
title = stringResource(MR.strings.pref_skip_read_chapters),
|
||||
|
@ -165,29 +170,26 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pager_viewer),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
entries = ReaderPreferences.TapZones
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap(),
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.pagerNavInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
entries = mapOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none),
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(
|
||||
MR.strings.tapping_inverted_horizontal,
|
||||
),
|
||||
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(
|
||||
MR.strings.tapping_inverted_vertical,
|
||||
),
|
||||
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
|
||||
MR.strings.tapping_inverted_both,
|
||||
),
|
||||
),
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
ReaderPreferences.TappingInvertMode.VERTICAL,
|
||||
ReaderPreferences.TappingInvertMode.BOTH,
|
||||
)
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
|
@ -195,14 +197,16 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
title = stringResource(MR.strings.pref_image_scale_type),
|
||||
entries = ReaderPreferences.ImageScaleType
|
||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||
.toMap(),
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.zoomStart(),
|
||||
title = stringResource(MR.strings.pref_zoom_start),
|
||||
entries = ReaderPreferences.ZoomStart
|
||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||
.toMap(),
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cropBorders(),
|
||||
|
@ -265,29 +269,26 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.webtoon_viewer),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
entries = ReaderPreferences.TapZones
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap(),
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.webtoonNavInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
entries = mapOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE to stringResource(MR.strings.none),
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(
|
||||
MR.strings.tapping_inverted_horizontal,
|
||||
),
|
||||
ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(
|
||||
MR.strings.tapping_inverted_vertical,
|
||||
),
|
||||
ReaderPreferences.TappingInvertMode.BOTH to stringResource(
|
||||
MR.strings.tapping_inverted_both,
|
||||
),
|
||||
),
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
ReaderPreferences.TappingInvertMode.VERTICAL,
|
||||
ReaderPreferences.TappingInvertMode.BOTH,
|
||||
)
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
|
@ -304,19 +305,11 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerHideThreshold(),
|
||||
title = stringResource(MR.strings.pref_hide_threshold),
|
||||
entries = mapOf(
|
||||
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(
|
||||
MR.strings.pref_highest,
|
||||
),
|
||||
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(
|
||||
MR.strings.pref_high,
|
||||
),
|
||||
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(
|
||||
MR.strings.pref_low,
|
||||
),
|
||||
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(
|
||||
MR.strings.pref_lowest,
|
||||
),
|
||||
entries = persistentMapOf(
|
||||
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
|
||||
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
|
||||
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
|
||||
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
|
@ -365,7 +358,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_reader_navigation),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readWithVolumeKeysPref,
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys),
|
||||
|
@ -383,7 +376,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_reader_actions),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.readWithLongTap(),
|
||||
title = stringResource(MR.strings.pref_read_with_long_tap),
|
||||
|
|
|
@ -10,6 +10,8 @@ import eu.kanade.presentation.more.settings.Preference
|
|||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
|
@ -59,7 +61,8 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||
it,
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
.toImmutableMap(),
|
||||
onValueChanged = {
|
||||
(context as FragmentActivity).authenticate(
|
||||
title = context.stringResource(MR.strings.lock_when_idle),
|
||||
|
@ -74,14 +77,15 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||
pref = securityPreferences.secureScreen(),
|
||||
title = stringResource(MR.strings.secure_screen),
|
||||
entries = SecurityPreferences.SecureScreenMode.entries
|
||||
.associateWith { stringResource(it.titleRes) },
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val LockAfterValues = listOf(
|
||||
private val LockAfterValues = persistentListOf(
|
||||
0, // Always
|
||||
1,
|
||||
2,
|
||||
|
|
|
@ -52,6 +52,8 @@ import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
|||
import eu.kanade.tachiyomi.data.track.simkl.SimklApi
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
|
@ -135,7 +137,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.services),
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.myAnimeList.name,
|
||||
tracker = trackerManager.myAnimeList,
|
||||
|
@ -208,16 +210,17 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.enhanced_services),
|
||||
preferenceItems = enhancedMangaTrackers.first
|
||||
.map { service ->
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = service.name,
|
||||
tracker = service,
|
||||
login = { (service as EnhancedMangaTracker).loginNoop() },
|
||||
logout = service::logout,
|
||||
)
|
||||
} + listOf(Preference.PreferenceItem.InfoPreference(enhancedMangaTrackerInfo)),
|
||||
|
||||
preferenceItems = (
|
||||
enhancedMangaTrackers.first
|
||||
.map { service ->
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = service.name,
|
||||
tracker = service,
|
||||
login = { (service as EnhancedMangaTracker).loginNoop() },
|
||||
logout = service::logout,
|
||||
)
|
||||
} + listOf(Preference.PreferenceItem.InfoPreference(enhancedMangaTrackerInfo))
|
||||
).toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -355,7 +358,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall)) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onDismissRequest,
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
package eu.kanade.presentation.more.settings.screen.about
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import tachiyomi.i18n.MR
|
||||
|
@ -32,12 +30,6 @@ class OpenSourceLicensesScreen : Screen() {
|
|||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
colors = LibraryDefaults.libraryColors(
|
||||
backgroundColor = MaterialTheme.colorScheme.background,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
|
||||
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
),
|
||||
onLibraryClick = {
|
||||
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
||||
name = it.library.name,
|
||||
|
|
|
@ -3,19 +3,14 @@ package eu.kanade.presentation.more.settings.screen.advanced
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
|
@ -50,6 +45,7 @@ import tachiyomi.domain.source.manga.interactor.GetMangaSourcesWithNonLibraryMan
|
|||
import tachiyomi.domain.source.manga.model.MangaSourceWithCount
|
||||
import tachiyomi.domain.source.manga.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
|
@ -114,7 +110,7 @@ class ClearDatabaseScreen : Screen() {
|
|||
onClick = model::selectAll,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_all),
|
||||
title = stringResource(MR.strings.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = model::invertSelection,
|
||||
),
|
||||
|
@ -132,40 +128,18 @@ class ClearDatabaseScreen : Screen() {
|
|||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxSize(),
|
||||
LazyColumnWithAction(
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(MR.strings.action_delete),
|
||||
actionEnabled = s.selection.isNotEmpty(),
|
||||
onClickAction = model::showConfirmation,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
items(s.items) { sourceWithCount ->
|
||||
ClearDatabaseItem(
|
||||
source = sourceWithCount.source,
|
||||
count = sourceWithCount.count,
|
||||
isSelected = s.selection.contains(sourceWithCount.id),
|
||||
onClickSelect = {
|
||||
model.toggleSelection(
|
||||
sourceWithCount.source,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
onClick = model::showConfirmation,
|
||||
enabled = s.selection.isNotEmpty(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.action_delete),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
items(s.items) { sourceWithCount ->
|
||||
ClearDatabaseItem(
|
||||
source = sourceWithCount.source,
|
||||
count = sourceWithCount.count,
|
||||
isSelected = s.selection.contains(sourceWithCount.id),
|
||||
onClickSelect = { model.toggleSelection(sourceWithCount.source) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
||||
class AnimeExtensionReposScreen : Screen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel = rememberScreenModel { AnimeExtensionReposScreenModel() }
|
||||
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
if (state is RepoScreenState.Loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val successState = state as RepoScreenState.Success
|
||||
|
||||
ExtensionReposScreen(
|
||||
state = successState,
|
||||
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
||||
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
|
||||
when (val dialog = successState.dialog) {
|
||||
null -> {}
|
||||
RepoDialog.Create -> {
|
||||
ExtensionRepoCreateDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(it) },
|
||||
categories = successState.repos,
|
||||
)
|
||||
}
|
||||
is RepoDialog.Delete -> {
|
||||
ExtensionRepoDeleteDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onDelete = { screenModel.deleteRepo(dialog.repo) },
|
||||
repo = dialog.repo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
screenModel.events.collectLatest { event ->
|
||||
if (event is RepoEvent.LocalizedMessage) {
|
||||
context.toast(event.stringRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.domain.source.anime.interactor.CreateAnimeSourceRepo
|
||||
import eu.kanade.domain.source.anime.interactor.DeleteAnimeSourceRepo
|
||||
import eu.kanade.domain.source.anime.interactor.GetAnimeSourceRepos
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AnimeExtensionReposScreenModel(
|
||||
private val getSourceRepos: GetAnimeSourceRepos = Injekt.get(),
|
||||
private val createSourceRepo: CreateAnimeSourceRepo = Injekt.get(),
|
||||
private val deleteSourceRepo: DeleteAnimeSourceRepo = Injekt.get(),
|
||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||
|
||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||
val events = _events.receiveAsFlow()
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
getSourceRepos.subscribe()
|
||||
.collectLatest { repos ->
|
||||
mutableState.update {
|
||||
RepoScreenState.Success(
|
||||
repos = repos.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds a new repo to the database.
|
||||
*
|
||||
* @param name The name of the repo to create.
|
||||
*/
|
||||
fun createRepo(name: String) {
|
||||
screenModelScope.launchIO {
|
||||
when (createSourceRepo.await(name)) {
|
||||
is CreateAnimeSourceRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given repo from the database.
|
||||
*
|
||||
* @param repo The repo to delete.
|
||||
*/
|
||||
fun deleteRepo(repo: String) {
|
||||
screenModelScope.launchIO {
|
||||
deleteSourceRepo.await(repo)
|
||||
}
|
||||
}
|
||||
|
||||
fun showDialog(dialog: RepoDialog) {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
RepoScreenState.Loading -> it
|
||||
is RepoScreenState.Success -> it.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissDialog() {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
RepoScreenState.Loading -> it
|
||||
is RepoScreenState.Success -> it.copy(dialog = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
||||
class MangaExtensionReposScreen : Screen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel = rememberScreenModel { MangaExtensionReposScreenModel() }
|
||||
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
if (state is RepoScreenState.Loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val successState = state as RepoScreenState.Success
|
||||
|
||||
ExtensionReposScreen(
|
||||
state = successState,
|
||||
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
||||
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
|
||||
when (val dialog = successState.dialog) {
|
||||
null -> {}
|
||||
RepoDialog.Create -> {
|
||||
ExtensionRepoCreateDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(it) },
|
||||
categories = successState.repos,
|
||||
)
|
||||
}
|
||||
is RepoDialog.Delete -> {
|
||||
ExtensionRepoDeleteDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onDelete = { screenModel.deleteRepo(dialog.repo) },
|
||||
repo = dialog.repo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
screenModel.events.collectLatest { event ->
|
||||
if (event is RepoEvent.LocalizedMessage) {
|
||||
context.toast(event.stringRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.source.manga.interactor.CreateMangaSourceRepo
|
||||
import eu.kanade.domain.source.manga.interactor.DeleteMangaSourceRepo
|
||||
import eu.kanade.domain.source.manga.interactor.GetMangaSourceRepos
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MangaExtensionReposScreenModel(
|
||||
private val getSourceRepos: GetMangaSourceRepos = Injekt.get(),
|
||||
private val createSourceRepo: CreateMangaSourceRepo = Injekt.get(),
|
||||
private val deleteSourceRepo: DeleteMangaSourceRepo = Injekt.get(),
|
||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||
|
||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||
val events = _events.receiveAsFlow()
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
getSourceRepos.subscribe()
|
||||
.collectLatest { repos ->
|
||||
mutableState.update {
|
||||
RepoScreenState.Success(
|
||||
repos = repos.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds a new repo to the database.
|
||||
*
|
||||
* @param name The name of the repo to create.
|
||||
*/
|
||||
fun createRepo(name: String) {
|
||||
screenModelScope.launchIO {
|
||||
when (createSourceRepo.await(name)) {
|
||||
is CreateMangaSourceRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given repo from the database.
|
||||
*
|
||||
* @param repo The repo to delete.
|
||||
*/
|
||||
fun deleteRepo(repo: String) {
|
||||
screenModelScope.launchIO {
|
||||
deleteSourceRepo.await(repo)
|
||||
}
|
||||
}
|
||||
|
||||
fun showDialog(dialog: RepoDialog) {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
RepoScreenState.Loading -> it
|
||||
is RepoScreenState.Success -> it.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissDialog() {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
RepoScreenState.Loading -> it
|
||||
is RepoScreenState.Success -> it.copy(dialog = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RepoEvent {
|
||||
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
|
||||
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
|
||||
}
|
||||
|
||||
sealed class RepoDialog {
|
||||
data object Create : RepoDialog()
|
||||
data class Delete(val repo: String) : RepoDialog()
|
||||
}
|
||||
|
||||
sealed class RepoScreenState {
|
||||
|
||||
@Immutable
|
||||
data object Loading : RepoScreenState()
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val repos: ImmutableList<String>,
|
||||
val dialog: RepoDialog? = null,
|
||||
) : RepoScreenState() {
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = repos.isEmpty()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun ExtensionReposContent(
|
||||
repos: ImmutableList<String>,
|
||||
lazyListState: LazyListState,
|
||||
paddingValues: PaddingValues,
|
||||
onClickDelete: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(repos) { repo ->
|
||||
ExtensionRepoListItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
repo = repo,
|
||||
onDelete = { onClickDelete(repo) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionRepoListItem(
|
||||
repo: String,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Text(text = repo, modifier = Modifier.padding(start = MaterialTheme.padding.medium))
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: (String) -> Unit,
|
||||
categories: ImmutableList<String>,
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = name.isNotEmpty() && !nameAlreadyExists,
|
||||
onClick = {
|
||||
onCreate(name)
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_add))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_add_repo))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(text = stringResource(MR.strings.action_add_repo_message))
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.label_add_repo_input))
|
||||
},
|
||||
supportingText = {
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||
MR.strings.error_repo_exists
|
||||
} else {
|
||||
MR.strings.information_required_plain
|
||||
}
|
||||
Text(text = stringResource(msgRes))
|
||||
},
|
||||
isError = name.isNotEmpty() && nameAlreadyExists,
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(focusRequester) {
|
||||
// TODO: https://issuetracker.google.com/issues/204502668
|
||||
delay(0.1.seconds)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoDeleteDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
repo: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onDelete()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_delete_repo))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.delete_repo_confirmation, repo))
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
@file:JvmName("ExtensionReposScreenKt")
|
||||
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
@Composable
|
||||
fun ExtensionReposScreen(
|
||||
state: RepoScreenState.Success,
|
||||
onClickCreate: () -> Unit,
|
||||
onClickDelete: (String) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
navigateUp = navigateUp,
|
||||
title = stringResource(MR.strings.label_extension_repos),
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
CategoryFloatingActionButton(
|
||||
lazyListState = lazyListState,
|
||||
onCreate = onClickCreate,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
MR.strings.information_empty_repos,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
ExtensionReposContent(
|
||||
repos = state.repos,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues + topSmallPaddingValues +
|
||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
onClickDelete = onClickDelete,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -4,45 +4,34 @@ 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.foundation.layout.ColumnScope
|
||||
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.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.components.WarningBanner
|
||||
import eu.kanade.presentation.util.Screen
|
||||
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.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import kotlinx.collections.immutable.PersistentSet
|
||||
import kotlinx.collections.immutable.minus
|
||||
import kotlinx.collections.immutable.plus
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
import tachiyomi.presentation.core.components.SectionCard
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
class CreateBackupScreen : Screen() {
|
||||
|
@ -77,97 +66,84 @@ class CreateBackupScreen : Screen() {
|
|||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = MaterialTheme.padding.medium),
|
||||
) {
|
||||
item {
|
||||
LabeledCheckbox(
|
||||
label = stringResource(MR.strings.entries),
|
||||
checked = true,
|
||||
onCheckedChange = {},
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
BackupChoices.forEach { (k, v) ->
|
||||
item {
|
||||
LabeledCheckbox(
|
||||
label = stringResource(v),
|
||||
checked = state.flags.contains(k),
|
||||
onCheckedChange = {
|
||||
model.toggleFlag(k)
|
||||
},
|
||||
)
|
||||
LazyColumnWithAction(
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(MR.strings.action_create),
|
||||
actionEnabled = state.options.anyEnabled(),
|
||||
onClickAction = {
|
||||
if (!BackupCreateJob.isManualJobRunning(context)) {
|
||||
try {
|
||||
chooseBackupDir.launch(BackupCreator.getFilename())
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(MR.strings.file_picker_error)
|
||||
}
|
||||
} else {
|
||||
context.toast(MR.strings.backup_in_progress)
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
item {
|
||||
WarningBanner(MR.strings.restore_miui_warning)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
item {
|
||||
SectionCard(MR.strings.label_library) {
|
||||
Options(BackupOptions.libraryOptions, state, model)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
onClick = {
|
||||
if (!BackupCreateJob.isManualJobRunning(context)) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
context.stringResource(MR.strings.restore_miui_warning, Toast.LENGTH_LONG)
|
||||
}
|
||||
try {
|
||||
chooseBackupDir.launch(Backup.getFilename())
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.stringResource(MR.strings.file_picker_error)
|
||||
}
|
||||
} else {
|
||||
context.stringResource(MR.strings.backup_in_progress)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.action_create),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
item {
|
||||
SectionCard(MR.strings.label_settings) {
|
||||
Options(BackupOptions.settingsOptions, state, model)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SectionCard(MR.strings.label_extensions) {
|
||||
Options(BackupOptions.extensionOptions, state, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Options(
|
||||
options: ImmutableList<BackupOptions.Entry>,
|
||||
state: CreateBackupScreenModel.State,
|
||||
model: CreateBackupScreenModel,
|
||||
) {
|
||||
options.forEach { option ->
|
||||
LabeledCheckbox(
|
||||
label = stringResource(option.label),
|
||||
checked = option.getter(state.options),
|
||||
onCheckedChange = {
|
||||
model.toggle(option.setter, it)
|
||||
},
|
||||
enabled = option.enabled(state.options),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
|
||||
|
||||
fun toggleFlag(flag: Int) {
|
||||
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
|
||||
mutableState.update {
|
||||
if (it.flags.contains(flag)) {
|
||||
it.copy(flags = it.flags - flag)
|
||||
} else {
|
||||
it.copy(flags = it.flags + flag)
|
||||
}
|
||||
it.copy(
|
||||
options = setter(it.options, enabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
BackupCreateJob.startNow(context, uri, state.value.options)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val flags: PersistentSet<Int> = BackupChoices.keys.toPersistentSet(),
|
||||
val options: BackupOptions = BackupOptions(),
|
||||
)
|
||||
}
|
||||
|
||||
private val BackupChoices = mapOf(
|
||||
BackupCreateFlags.BACKUP_CATEGORY to MR.strings.general_categories,
|
||||
BackupCreateFlags.BACKUP_CHAPTER to MR.strings.chapters_episodes,
|
||||
BackupCreateFlags.BACKUP_TRACK to MR.strings.track,
|
||||
BackupCreateFlags.BACKUP_HISTORY to MR.strings.history,
|
||||
BackupCreateFlags.BACKUP_PREFS to MR.strings.settings,
|
||||
BackupCreateFlags.BACKUP_EXT_PREFS to MR.strings.extension_settings,
|
||||
BackupCreateFlags.BACKUP_EXTENSIONS to MR.strings.label_extensions,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
package eu.kanade.presentation.more.settings.screen.data
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
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.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.core.net.toUri
|
||||
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.components.WarningBanner
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
import tachiyomi.presentation.core.components.SectionCard
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
class RestoreBackupScreen(
|
||||
private val uri: String,
|
||||
) : Screen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) }
|
||||
val state by model.state.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = stringResource(MR.strings.pref_restore_backup),
|
||||
navigateUp = navigator::pop,
|
||||
scrollBehavior = it,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
LazyColumnWithAction(
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(MR.strings.action_restore),
|
||||
actionEnabled = state.canRestore && state.options.anyEnabled(),
|
||||
onClickAction = {
|
||||
model.startRestore()
|
||||
navigator.pop()
|
||||
},
|
||||
) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
item {
|
||||
WarningBanner(MR.strings.restore_miui_warning)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.canRestore) {
|
||||
item {
|
||||
SectionCard {
|
||||
RestoreOptions.options.forEach { option ->
|
||||
LabeledCheckbox(
|
||||
label = stringResource(option.label),
|
||||
checked = option.getter(state.options),
|
||||
onCheckedChange = {
|
||||
model.toggle(option.setter, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.error != null) {
|
||||
errorMessageItem(state.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.errorMessageItem(
|
||||
error: Any?,
|
||||
) {
|
||||
item {
|
||||
SectionCard {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
val msg = buildAnnotatedString {
|
||||
when (error) {
|
||||
is MissingRestoreComponents -> {
|
||||
appendLine(stringResource(MR.strings.backup_restore_content_full))
|
||||
if (error.sources.isNotEmpty()) {
|
||||
appendLine()
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
appendLine(stringResource(MR.strings.backup_restore_missing_sources))
|
||||
}
|
||||
error.sources.joinTo(
|
||||
this,
|
||||
separator = "\n- ",
|
||||
prefix = "- ",
|
||||
)
|
||||
}
|
||||
if (error.trackers.isNotEmpty()) {
|
||||
appendLine()
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
appendLine(stringResource(MR.strings.backup_restore_missing_trackers))
|
||||
}
|
||||
error.trackers.joinTo(
|
||||
this,
|
||||
separator = "\n- ",
|
||||
prefix = "- ",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is InvalidRestore -> {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
appendLine(stringResource(MR.strings.invalid_backup_file))
|
||||
}
|
||||
appendLine(error.uri.toString())
|
||||
|
||||
appendLine()
|
||||
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
appendLine(stringResource(MR.strings.invalid_backup_file_error))
|
||||
}
|
||||
appendLine(error.message)
|
||||
}
|
||||
|
||||
else -> {
|
||||
appendLine(error.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SelectionContainer {
|
||||
Text(text = msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RestoreBackupScreenModel(
|
||||
private val context: Context,
|
||||
private val uri: String,
|
||||
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
|
||||
|
||||
init {
|
||||
validate(uri.toUri())
|
||||
}
|
||||
|
||||
fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) {
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
options = setter(it.options, enabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun startRestore() {
|
||||
BackupRestoreJob.start(
|
||||
context = context,
|
||||
uri = uri.toUri(),
|
||||
options = state.value.options,
|
||||
)
|
||||
}
|
||||
|
||||
private fun validate(uri: Uri) {
|
||||
val results = try {
|
||||
BackupFileValidator(context).validate(uri)
|
||||
} catch (e: Exception) {
|
||||
setError(
|
||||
error = InvalidRestore(uri, e.message.toString()),
|
||||
canRestore = false,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (results.missingSources.isNotEmpty() || results.missingTrackers.isNotEmpty()) {
|
||||
setError(
|
||||
error = MissingRestoreComponents(uri, results.missingSources, results.missingTrackers),
|
||||
canRestore = true,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setError(error = null, canRestore = true)
|
||||
}
|
||||
|
||||
private fun setError(error: Any?, canRestore: Boolean) {
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
error = error,
|
||||
canRestore = canRestore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val error: Any? = null,
|
||||
val canRestore: Boolean = false,
|
||||
val options: RestoreOptions = RestoreOptions(),
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
|
@ -0,0 +1,75 @@
|
|||
package eu.kanade.presentation.more.settings.screen.data
|
||||
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.theme.header
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun StorageInfo(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val storages = remember { DiskUtil.getExternalStorages(context) }
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
storages.forEach {
|
||||
StorageInfo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StorageInfo(
|
||||
file: File,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val available = remember(file) { DiskUtil.getAvailableStorageSpace(file) }
|
||||
val availableText = remember(available) { Formatter.formatFileSize(context, available) }
|
||||
val total = remember(file) { DiskUtil.getTotalStorageSpace(file) }
|
||||
val totalText = remember(total) { Formatter.formatFileSize(context, total) }
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
Text(
|
||||
text = file.absolutePath,
|
||||
style = MaterialTheme.typography.header,
|
||||
)
|
||||
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.fillMaxWidth()
|
||||
.height(12.dp),
|
||||
progress = { (1 - (available / total.toFloat())) },
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(MR.strings.available_disk_space_info, availableText, totalText),
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,8 @@ import eu.kanade.presentation.more.settings.screen.about.AboutScreen
|
|||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.guava.await
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
|
@ -47,7 +49,7 @@ class DebugInfoScreen : Screen() {
|
|||
private fun getAppInfoGroup(): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = "App info",
|
||||
preferenceItems = listOf(
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = "Version",
|
||||
subtitle = AboutScreen.getVersionName(false),
|
||||
|
@ -108,8 +110,8 @@ class DebugInfoScreen : Screen() {
|
|||
}
|
||||
|
||||
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
|
||||
val items = buildList {
|
||||
add(
|
||||
val items = persistentListOf<Preference.PreferenceItem<out Any>>().mutate {
|
||||
it.add(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = "Model",
|
||||
subtitle = "${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})",
|
||||
|
@ -117,14 +119,14 @@ class DebugInfoScreen : Screen() {
|
|||
)
|
||||
|
||||
if (DeviceUtil.oneUiVersion != null) {
|
||||
add(
|
||||
it.add(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = "OneUI version",
|
||||
subtitle = "${DeviceUtil.oneUiVersion}",
|
||||
),
|
||||
)
|
||||
} else if (DeviceUtil.miuiMajorVersion != null) {
|
||||
add(
|
||||
it.add(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = "MIUI version",
|
||||
subtitle = "${DeviceUtil.miuiMajorVersion}",
|
||||
|
@ -139,7 +141,7 @@ class DebugInfoScreen : Screen() {
|
|||
} else {
|
||||
Build.VERSION.RELEASE
|
||||
}
|
||||
add(
|
||||
it.add(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = "Android version",
|
||||
subtitle = "$androidVersion (${Build.DISPLAY})",
|
||||
|
|
|
@ -114,6 +114,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
|
|||
} else {
|
||||
tween(200)
|
||||
},
|
||||
label = "highlight",
|
||||
)
|
||||
Modifier.background(color = highlight)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
|
@ -18,10 +19,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.presentation.more.stats.components.StatsItem
|
||||
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
|
||||
import eu.kanade.presentation.more.stats.components.StatsSection
|
||||
import eu.kanade.presentation.more.stats.data.StatsData
|
||||
import eu.kanade.presentation.util.toDurationString
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SectionCard
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.util.Locale
|
||||
|
@ -55,7 +56,7 @@ fun AnimeStatsScreenContent(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OverviewSection(
|
||||
private fun LazyItemScope.OverviewSection(
|
||||
data: StatsData.AnimeOverview,
|
||||
) {
|
||||
val none = stringResource(MR.strings.none)
|
||||
|
@ -65,7 +66,7 @@ private fun OverviewSection(
|
|||
.toDuration(DurationUnit.MILLISECONDS)
|
||||
.toDurationString(context, fallback = none)
|
||||
}
|
||||
StatsSection(MR.strings.label_overview_section) {
|
||||
SectionCard(MR.strings.label_overview_section) {
|
||||
Row(
|
||||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
) {
|
||||
|
@ -89,10 +90,10 @@ private fun OverviewSection(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun TitlesStats(
|
||||
private fun LazyItemScope.TitlesStats(
|
||||
data: StatsData.AnimeTitles,
|
||||
) {
|
||||
StatsSection(MR.strings.label_titles_section) {
|
||||
SectionCard(MR.strings.label_titles_section) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.globalUpdateItemCount.toString(),
|
||||
|
@ -111,10 +112,10 @@ private fun TitlesStats(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun EpisodeStats(
|
||||
private fun LazyItemScope.EpisodeStats(
|
||||
data: StatsData.Episodes,
|
||||
) {
|
||||
StatsSection(MR.strings.episodes) {
|
||||
SectionCard(MR.strings.episodes) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.totalEpisodeCount.toString(),
|
||||
|
@ -133,19 +134,19 @@ private fun EpisodeStats(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackerStats(
|
||||
private fun LazyItemScope.TrackerStats(
|
||||
data: StatsData.Trackers,
|
||||
) {
|
||||
val notApplicable = stringResource(MR.strings.not_applicable)
|
||||
val meanScoreStr = remember(data.trackedTitleCount, data.meanScore) {
|
||||
if (data.trackedTitleCount > 0 && !data.meanScore.isNaN()) {
|
||||
// All other numbers are stringResourced in English
|
||||
String.format(Locale.ENGLISH, "%.2f ★", data.meanScore)
|
||||
// All other numbers are localized in English
|
||||
"%.2f ★".format(Locale.ENGLISH, data.meanScore)
|
||||
} else {
|
||||
notApplicable
|
||||
}
|
||||
}
|
||||
StatsSection(MR.strings.label_tracker_section) {
|
||||
SectionCard(MR.strings.label_tracker_section) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.trackedTitleCount.toString(),
|
||||
|
|
|
@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.material.icons.outlined.LocalLibrary
|
||||
|
@ -18,10 +18,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.presentation.more.stats.components.StatsItem
|
||||
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
|
||||
import eu.kanade.presentation.more.stats.components.StatsSection
|
||||
import eu.kanade.presentation.more.stats.data.StatsData
|
||||
import eu.kanade.presentation.util.toDurationString
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SectionCard
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.util.Locale
|
||||
|
@ -33,9 +33,7 @@ fun MangaStatsScreenContent(
|
|||
state: StatsScreenState.SuccessManga,
|
||||
paddingValues: PaddingValues,
|
||||
) {
|
||||
val statListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
state = statListState,
|
||||
contentPadding = paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
|
@ -55,7 +53,7 @@ fun MangaStatsScreenContent(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OverviewSection(
|
||||
private fun LazyItemScope.OverviewSection(
|
||||
data: StatsData.MangaOverview,
|
||||
) {
|
||||
val none = stringResource(MR.strings.none)
|
||||
|
@ -65,7 +63,7 @@ private fun OverviewSection(
|
|||
.toDuration(DurationUnit.MILLISECONDS)
|
||||
.toDurationString(context, fallback = none)
|
||||
}
|
||||
StatsSection(MR.strings.label_overview_section) {
|
||||
SectionCard(MR.strings.label_overview_section) {
|
||||
Row(
|
||||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
) {
|
||||
|
@ -89,10 +87,10 @@ private fun OverviewSection(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun TitlesStats(
|
||||
private fun LazyItemScope.TitlesStats(
|
||||
data: StatsData.MangaTitles,
|
||||
) {
|
||||
StatsSection(MR.strings.label_titles_section) {
|
||||
SectionCard(MR.strings.label_titles_section) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.globalUpdateItemCount.toString(),
|
||||
|
@ -111,10 +109,10 @@ private fun TitlesStats(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ChapterStats(
|
||||
private fun LazyItemScope.ChapterStats(
|
||||
data: StatsData.Chapters,
|
||||
) {
|
||||
StatsSection(MR.strings.chapters) {
|
||||
SectionCard(MR.strings.chapters) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.totalChapterCount.toString(),
|
||||
|
@ -133,19 +131,19 @@ private fun ChapterStats(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackerStats(
|
||||
private fun LazyItemScope.TrackerStats(
|
||||
data: StatsData.Trackers,
|
||||
) {
|
||||
val notApplicable = stringResource(MR.strings.not_applicable)
|
||||
val meanScoreStr = remember(data.trackedTitleCount, data.meanScore) {
|
||||
if (data.trackedTitleCount > 0 && !data.meanScore.isNaN()) {
|
||||
// All other numbers are stringResourced in English
|
||||
String.format(Locale.ENGLISH, "%.2f ★", data.meanScore)
|
||||
// All other numbers are localized in English
|
||||
"%.2f ★".format(Locale.ENGLISH, data.meanScore)
|
||||
} else {
|
||||
notApplicable
|
||||
}
|
||||
}
|
||||
StatsSection(MR.strings.label_tracker_section) {
|
||||
SectionCard(MR.strings.label_tracker_section) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.trackedTitleCount.toString(),
|
||||
|
|
|
@ -34,12 +34,12 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.data.database.models.manga.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.manga.toDomainChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import tachiyomi.domain.items.chapter.service.calculateChapterGap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
@ -51,8 +51,8 @@ fun ChapterTransition(
|
|||
currChapterDownloaded: Boolean,
|
||||
goingToChapterDownloaded: Boolean,
|
||||
) {
|
||||
val currChapter = transition.from.chapter
|
||||
val goingToChapter = transition.to?.chapter
|
||||
val currChapter = transition.from.chapter.toDomainChapter()
|
||||
val goingToChapter = transition.to?.chapter?.toDomainChapter()
|
||||
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
when (transition) {
|
||||
|
@ -65,10 +65,7 @@ fun ChapterTransition(
|
|||
bottomChapter = currChapter,
|
||||
bottomChapterDownloaded = currChapterDownloaded,
|
||||
fallbackLabel = stringResource(MR.strings.transition_no_previous),
|
||||
chapterGap = calculateChapterGap(
|
||||
currChapter.toDomainChapter(),
|
||||
goingToChapter?.toDomainChapter(),
|
||||
),
|
||||
chapterGap = calculateChapterGap(currChapter, goingToChapter),
|
||||
)
|
||||
}
|
||||
is ChapterTransition.Next -> {
|
||||
|
@ -80,10 +77,7 @@ fun ChapterTransition(
|
|||
bottomChapter = goingToChapter,
|
||||
bottomChapterDownloaded = goingToChapterDownloaded,
|
||||
fallbackLabel = stringResource(MR.strings.transition_no_next),
|
||||
chapterGap = calculateChapterGap(
|
||||
goingToChapter?.toDomainChapter(),
|
||||
currChapter.toDomainChapter(),
|
||||
),
|
||||
chapterGap = calculateChapterGap(goingToChapter, currChapter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -245,7 +239,7 @@ private fun ChapterText(
|
|||
maxLines = 5,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
inlineContent = mapOf(
|
||||
inlineContent = persistentMapOf(
|
||||
DownloadedIconContentId to InlineTextContent(
|
||||
Placeholder(
|
||||
width = 22.sp,
|
||||
|
@ -285,24 +279,23 @@ private val CardColor: CardColors
|
|||
private val VerticalSpacerSize = 24.dp
|
||||
private const val DownloadedIconContentId = "downloaded"
|
||||
|
||||
private fun previewChapter(name: String, scanlator: String, chapterNumber: Float) = ChapterImpl().apply {
|
||||
this.name = name
|
||||
this.scanlator = scanlator
|
||||
this.chapter_number = chapterNumber
|
||||
|
||||
this.id = 0
|
||||
this.manga_id = 0
|
||||
this.url = ""
|
||||
}
|
||||
private fun previewChapter(name: String, scanlator: String, chapterNumber: Double) = Chapter.create().copy(
|
||||
id = 0L,
|
||||
mangaId = 0L,
|
||||
url = "",
|
||||
name = name,
|
||||
scanlator = scanlator,
|
||||
chapterNumber = chapterNumber,
|
||||
)
|
||||
private val FakeChapter = previewChapter(
|
||||
name = "Vol.1, Ch.1 - Fake Chapter Title",
|
||||
scanlator = "Scanlator Name",
|
||||
chapterNumber = 1f,
|
||||
chapterNumber = 1.0,
|
||||
)
|
||||
private val FakeGapChapter = previewChapter(
|
||||
name = "Vol.5, Ch.44 - Fake Gap Chapter Title",
|
||||
scanlator = "Scanlator Name",
|
||||
chapterNumber = 44f,
|
||||
chapterNumber = 44.0,
|
||||
)
|
||||
private val FakeChapterLongTitle = previewChapter(
|
||||
name = "Vol.1, Ch.0 - The Mundane Musings of a Metafictional Manga: A Chapter About a Chapter, Featuring" +
|
||||
|
@ -311,7 +304,7 @@ private val FakeChapterLongTitle = previewChapter(
|
|||
"Fictional Realities and Reality-Bending Fiction, Where the Fourth Wall is Always in Danger of Being Broken " +
|
||||
"and the Line Between Author and Character is Forever Blurred.",
|
||||
scanlator = "Long Long Funny Scanlator Sniper Group Name Reborn",
|
||||
chapterNumber = 1f,
|
||||
chapterNumber = 1.0,
|
||||
)
|
||||
|
||||
@PreviewLightDark
|
||||
|
|
|
@ -30,7 +30,7 @@ fun DisplayRefreshHost(
|
|||
val currentDisplayRefresh = hostState.currentDisplayRefresh
|
||||
LaunchedEffect(currentDisplayRefresh) {
|
||||
if (currentDisplayRefresh) {
|
||||
delay(200)
|
||||
delay(1500)
|
||||
hostState.currentDisplayRefresh = false
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ fun DisplayRefreshHost(
|
|||
Canvas(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
drawRect(Color.White)
|
||||
drawRect(Color.Black)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
|||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
private val animationSpec = tween<IntOffset>(200)
|
||||
|
@ -156,7 +157,7 @@ fun ReaderAppBars(
|
|||
) {
|
||||
Column(
|
||||
modifier = modifierWithInsetsPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
ChapterNavigator(
|
||||
isRtl = isRtl,
|
||||
|
|
|
@ -10,6 +10,7 @@ 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.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
|
@ -21,6 +22,7 @@ import androidx.compose.ui.unit.dp
|
|||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
|
@ -50,7 +52,7 @@ fun ModeSelectionDialog(
|
|||
onClick = onApply,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
|
|||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
|
@ -248,7 +249,7 @@ private fun TrackStatusSelectorPreviews() {
|
|||
TrackStatusSelector(
|
||||
selection = 1,
|
||||
onSelectionChange = {},
|
||||
selections = mapOf(
|
||||
selections = persistentMapOf(
|
||||
// Anilist values
|
||||
1 to MR.strings.reading,
|
||||
2 to MR.strings.plan_to_read,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue