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