Merge branch 'upstream/master'

This commit is contained in:
jmir1 2024-01-10 04:10:35 +01:00
commit acaf5bd43e
No known key found for this signature in database
GPG key ID: 7B3B624787A072BD
408 changed files with 7315 additions and 5506 deletions

View file

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

View file

@ -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.

View file

@ -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)

View file

@ -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.** {

View file

@ -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"

View file

@ -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()) }
} }
} }

View file

@ -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),
} }
} }

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

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

View file

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

View file

@ -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()

View file

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

View file

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

View file

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

View file

@ -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 {

View file

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

View file

@ -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(),

View file

@ -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 {

View file

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

View file

@ -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(),

View file

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

View file

@ -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),

View file

@ -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),

View file

@ -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,

View file

@ -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)

View file

@ -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),

View file

@ -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,

View file

@ -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)

View file

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

View file

@ -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,
) )
} }

View file

@ -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) {

View file

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

View file

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

View file

@ -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 = {

View file

@ -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,
)
},
)
}

View file

@ -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,

View file

@ -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 = {

View file

@ -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,

View file

@ -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(

View file

@ -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) },

View file

@ -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(

View file

@ -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,
)
} }

View file

@ -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,

View file

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

View file

@ -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,

View file

@ -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) {

View file

@ -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))

View file

@ -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 = {

View file

@ -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,

View file

@ -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) },

View file

@ -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(

View file

@ -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,

View file

@ -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(

View file

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

View file

@ -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,
),
),
),
),
) )
} }
} }

View file

@ -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,
),
),
),
),
) )
} }
} }

View file

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

View file

@ -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()
} }
} }

View file

@ -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()
} }
} }
} }

View file

@ -0,0 +1,11 @@
package eu.kanade.presentation.more.onboarding
import androidx.compose.runtime.Composable
internal interface OnboardingStep {
val isComplete: Boolean
@Composable
fun Content()
}

View file

@ -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),
)
}
}

View file

@ -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() }
}
}
} }

View file

@ -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(
) )
} }
} }
}

View file

@ -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()
} }

View file

@ -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(

View file

@ -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),

View file

@ -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(

View file

@ -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 {

View file

@ -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()
} }
} }

View file

@ -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),

View file

@ -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,
)

View file

@ -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(),
), ),
), ),
) )

View file

@ -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,
),
), ),
), ),
), ),

View file

@ -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(),
), ),
), ),
) )

View file

@ -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),

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,
) )
} }
} }

View file

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

View file

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

View file

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

View file

@ -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()
}
}

View file

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

View file

@ -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))
},
)
}

View file

@ -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,
)
}
}

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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,
)
}
}

View file

@ -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})",

View file

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

View file

@ -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(),

View file

@ -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(),

View file

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

View file

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

View file

@ -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,

View file

@ -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(

View file

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