Reapply "Migrate from Parcelize to kotlinx.serialization" (#2357)

* Reapply "Migrate from Parcelize to kotlinx.serialization" (#2356)

This reverts commit c2e53cc8b9.

* Fix stale states captured by lambdas in ViewModels
This commit is contained in:
FooIbar authored and GitHub committed 2025-05-25 22:27:16 +08:00
1 parent c2e53cc8b9
commit 8207be5940
29 files changed
+263 -264

No files matched your search

-1
View File
@@ -10,7 +10,6 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.spotless)
@@ -15,7 +15,6 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
@@ -23,10 +22,8 @@ import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
@Serializable
@Entity(tableName = "GALLERIES")
@TypeConverters(SimpleTagsConverter::class)
@@ -88,7 +85,7 @@ data class BaseGalleryInfo(
@Ignore
override var favoriteNote: String? = null,
) : GalleryInfo, Parcelable
) : GalleryInfo
class SimpleTagsConverter {
@TypeConverter
@@ -15,21 +15,23 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.addQueryParameter
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
import com.hippo.ehviewer.client.ehUrl
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
@Serializable
data class FavListUrlBuilder(
val favCat: Int = FAV_CAT_ALL,
val keyword: String? = null,
var jumpTo: String? = null,
var prev: String? = null,
var next: String? = null,
) : Parcelable {
) {
val isLocal
get() = favCat == FAV_CAT_LOCAL
fun setIndex(index: String?, isNext: Boolean) {
next = index.takeIf { isNext }
prev = index.takeUnless { isNext }
@@ -15,10 +15,6 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class GalleryComment(
val id: Long,
val score: Int,
@@ -33,4 +29,4 @@ data class GalleryComment(
val user: String?,
val comment: String,
val lastEdited: Long,
) : Parcelable
)
@@ -15,11 +15,7 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class GalleryCommentList(
val comments: List<GalleryComment>,
val hasMore: Boolean,
) : Parcelable
)
@@ -15,10 +15,8 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import androidx.compose.ui.util.fastFlatMap
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANGS
import kotlinx.parcelize.Parcelize
private val LANGUAGES = arrayOf(
"English",
@@ -37,7 +35,6 @@ private val LANGUAGES = arrayOf(
"Dutch",
)
@Parcelize
data class GalleryDetail(
val galleryInfo: BaseGalleryInfo = BaseGalleryInfo(),
var apiUid: Long = -1L,
@@ -55,7 +52,7 @@ data class GalleryDetail(
val tagGroups: List<GalleryTagGroup>,
var comments: GalleryCommentList,
val previewList: List<GalleryPreview>,
) : GalleryInfo by galleryInfo, Parcelable {
) : GalleryInfo by galleryInfo {
fun fillInfo() {
val index = LANGUAGES.indexOf(language)
if (index != -1) simpleLanguage = S_LANGS[index]
@@ -15,17 +15,14 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.ehviewer.client.keyToUrl
import kotlinx.parcelize.Parcelize
sealed interface GalleryPreview : Parcelable {
sealed interface GalleryPreview {
val url: String
val imageKey: String
val position: Int
}
@Parcelize
data class V1GalleryPreview(
override val imageKey: String,
override val position: Int,
@@ -33,7 +30,6 @@ data class V1GalleryPreview(
override val url get() = keyToUrl(imageKey)
}
@Parcelize
data class V2GalleryPreview(
override val url: String,
override val imageKey: String,
@@ -1,8 +1,5 @@
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
enum class VoteStatus(val display: String) {
NONE(""),
UP("+1"),
@@ -15,9 +12,8 @@ enum class PowerStatus {
WEAK,
}
@Parcelize
data class GalleryTag(
val text: String,
val power: PowerStatus,
val vote: VoteStatus,
) : Parcelable
)
@@ -15,11 +15,7 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class GalleryTagGroup(
val nameSpace: TagNamespace,
val tags: List<GalleryTag>,
) : Parcelable
)
@@ -15,7 +15,6 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
@@ -26,9 +25,9 @@ import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.ui.main.AdvanceTable
import io.ktor.http.Parameters
import kotlin.text.toIntOrNull
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
@Serializable
data class ListUrlBuilder(
var mode: Int = MODE_NORMAL,
private var prev: String? = null,
@@ -44,7 +43,7 @@ data class ListUrlBuilder(
var minRating: Int = -1,
var pageFrom: Int = -1,
var pageTo: Int = -1,
) : Parcelable {
) {
fun setIndex(index: String, isNext: Boolean = true) {
next = index.takeIf { isNext }
@@ -1,10 +1,6 @@
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
sealed class TagNamespace(val value: String, val prefix: String?) : Parcelable {
sealed class TagNamespace(val value: String, val prefix: String?) {
object Artist : TagNamespace("artist", "a")
object Cosplayer : TagNamespace("cosplayer", "cos")
object Character : TagNamespace("character", "c")
@@ -1,12 +1,9 @@
package com.hippo.ehviewer.client.parser
import android.os.Parcelable
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlin.random.Random
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
object HomeParser {
private val TorrentKeyRegex = Regex("Your current key is: <[^>]*>([^<]*)<")
@@ -35,15 +32,11 @@ object HomeParser {
throw ParseException("Parse funds error")
}
@Parcelize
data class Result(val limits: Limits, val funds: Funds, private val id: Int = Random.nextInt()) : Parcelable
data class Result(val limits: Limits, val funds: Funds, private val id: Int = Random.nextInt())
}
@Parcelize
data class Funds(val gp: Int, val credit: Int) : Parcelable
data class Funds(val gp: Int, val credit: Int)
@Parcelize
@Serializable
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
data class Limits(val current: Int, val maximum: Int, val resetCost: Int)
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Int
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.download
import android.Manifest
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
@@ -27,6 +28,9 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.savedstate.serialization.decodeFromSavedState
import androidx.savedstate.serialization.encodeToSavedState
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
@@ -34,7 +38,6 @@ import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.getParcelableExtraCompat
import com.hippo.ehviewer.util.unsafeLazy
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
@@ -95,11 +98,9 @@ class DownloadService :
private suspend fun handleIntent(intent: Intent?) {
when (intent?.action) {
ACTION_START -> {
val gi = intent.getParcelableExtraCompat<BaseGalleryInfo>(KEY_GALLERY_INFO)
val gi = intent.getBundleExtra(KEY_GALLERY_INFO) ?: return
val label = intent.getStringExtra(KEY_LABEL)
if (gi != null) {
deferredMgr.await().startDownload(gi, label)
}
deferredMgr.await().startDownload(decodeFromSavedState(gi), label)
}
ACTION_START_RANGE -> {
@@ -454,6 +455,23 @@ class DownloadService :
private var sFinishedCount = 0
private var sDownloadedCount = 0
context(ctx: Context)
inline fun startService(action: String, intentBuilder: Intent.() -> Unit = {}) {
val intent = Intent(ctx, DownloadService::class.java).setAction(action).apply(intentBuilder)
ContextCompat.startForegroundService(ctx, intent)
}
context(_: Context)
fun startDownload(info: BaseGalleryInfo, label: String? = null) = startService(ACTION_START) {
putExtra(KEY_GALLERY_INFO, encodeToSavedState(info))
label?.let { putExtra(KEY_LABEL, it) }
}
context(_: Context)
fun startRangeDownload(gidList: LongArray) = startService(ACTION_START_RANGE) {
putExtra(KEY_GID_LIST, gidList)
}
fun clear() {
sFailedCount = 0
sFinishedCount = 0
@@ -15,10 +15,8 @@
*/
package com.hippo.ehviewer.shortcuts
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.download.DownloadService
/**
@@ -27,8 +25,8 @@ import com.hippo.ehviewer.download.DownloadService
class ShortcutsActivity : AppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent?.action) {
DownloadService.ACTION_START_ALL, DownloadService.ACTION_STOP_ALL -> ContextCompat.startForegroundService(this, Intent(this, DownloadService::class.java).setAction(intent.action))
when (val action = intent?.action) {
DownloadService.ACTION_START_ALL, DownloadService.ACTION_STOP_ALL -> DownloadService.startService(action)
}
finish()
}
@@ -17,8 +17,6 @@ package com.hippo.ehviewer.ui
import android.Manifest
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -37,7 +35,6 @@ import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -124,11 +121,8 @@ suspend fun startDownload(forceDefault: Boolean, vararg galleryInfos: BaseGaller
}
val (toStart, toAdd) = galleryInfos.partition { DownloadManager.containDownloadInfo(it.gid) }
if (toStart.isNotEmpty()) {
val intent = Intent(context, DownloadService::class.java)
intent.action = DownloadService.ACTION_START_RANGE
val list = toStart.mapToLongArray(GalleryInfo::gid)
intent.putExtra(DownloadService.KEY_GID_LIST, list)
ContextCompat.startForegroundService(context, intent)
DownloadService.startRangeDownload(list)
}
if (toAdd.isEmpty()) {
return tip(R.string.added_to_download_list)
@@ -148,11 +142,7 @@ suspend fun startDownload(forceDefault: Boolean, vararg galleryInfos: BaseGaller
if (justStart) {
// Got default label
for (gi in toAdd) {
val intent = Intent(context, DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_LABEL, label)
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)
ContextCompat.startForegroundService(context, intent)
DownloadService.startDownload(gi, label)
}
// Notify
tip(R.string.added_to_download_list)
@@ -173,11 +163,7 @@ suspend fun startDownload(forceDefault: Boolean, vararg galleryInfos: BaseGaller
val label1 = if (selected == 0) null else items[selected].takeIf { DownloadManager.containLabel(it) }
// Start download
for (gi in toAdd) {
val intent = Intent(context, DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_LABEL, label1)
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)
ContextCompat.startForegroundService(context, intent)
DownloadService.startDownload(gi, label1)
}
// Save settings
if (checked) {
@@ -286,7 +272,7 @@ context(_: DestinationsNavigator)
fun navToReader(info: BaseGalleryInfo, page: Int = -1) = navToReader(ReaderScreenArgs.Gallery(info, page))
context(_: DestinationsNavigator)
fun navToReader(uri: Uri) = navToReader(ReaderScreenArgs.Archive(uri))
fun navToReader(path: String) = navToReader(ReaderScreenArgs.Archive(path))
context(nav: DestinationsNavigator)
private fun navToReader(args: ReaderScreenArgs) = nav.navigate(ReaderScreenDestination(args)) { launchSingleTop = true }
@@ -258,32 +258,32 @@ class MainActivity : EhActivity() {
LaunchedEffect(Unit) {
intentFlow.collect { intent ->
when (intent.action) {
Intent.ACTION_VIEW -> {
Intent.ACTION_VIEW -> with(navigator) {
val uri = intent.data ?: return@collect
when (uri.scheme) {
SCHEME_CONTENT, SCHEME_FILE -> with(navigator) { navToReader(uri) }
SCHEME_FILE -> navToReader(uri.path!!)
SCHEME_CONTENT -> navToReader(uri.toString())
else -> {
val url = uri.toString()
if (!with(navigator) { navWithUrl(url) }) {
if (!navWithUrl(url)) {
val new = awaitInputText(initial = url, title = cannotParse)
addTextToClipboard(new)
}
}
}
}
Intent.ACTION_SEND -> {
Intent.ACTION_SEND -> with(navigator) {
val type = intent.type
if ("text/plain" == type) {
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
if (keyword != null && !with(navigator) { navWithUrl(keyword) }) {
navigator.navigate(ListUrlBuilder(mKeyword = keyword).asDst())
if (keyword != null && !navWithUrl(keyword)) {
navigate(ListUrlBuilder(mKeyword = keyword).asDst())
}
} else if (type != null && type.startsWith("image/")) {
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
if (null != uri) {
val hash = withIOContext { uri.toOkioPath().sha1() }
navigator.navigate(
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
hash = hash,
@@ -1,7 +1,5 @@
package com.hippo.ehviewer.ui.main
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@@ -13,7 +11,6 @@ object AdvanceTable {
const val SFT = 0x10
}
@Parcelize
@Serializable
data class AdvancedSearchOption(
@Transient
@@ -21,4 +18,4 @@ data class AdvancedSearchOption(
val minRating: Int = 0,
val fromPage: Int = 0,
val toPage: Int = 0,
) : Parcelable
)
@@ -1,8 +1,6 @@
package com.hippo.ehviewer.ui.reader
import android.content.Context
import android.net.Uri
import android.os.Parcelable
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
@@ -83,7 +81,6 @@ import com.hippo.ehviewer.ui.tools.dialog
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.displayString
import com.hippo.ehviewer.util.hasAds
import com.hippo.files.toOkioPath
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -100,17 +97,19 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import moe.tarsin.kt.unreachable
import moe.tarsin.launch
import moe.tarsin.string
import okio.Path.Companion.toPath
sealed interface ReaderScreenArgs : Parcelable {
@Parcelize
@Serializable
sealed interface ReaderScreenArgs {
@Serializable
data class Gallery(val info: BaseGalleryInfo, val page: Int) : ReaderScreenArgs
@Parcelize
data class Archive(val uri: Uri) : ReaderScreenArgs
@Serializable
data class Archive(val path: String) : ReaderScreenArgs
}
@Composable
@@ -382,7 +381,7 @@ suspend inline fun <T> usePageLoader(args: ReaderScreenArgs, crossinline block:
}
}
is ReaderScreenArgs.Archive -> useArchivePageLoader(
args.uri.toOkioPath(),
args.path.toPath(),
passwdProvider = { invalidator ->
awaitInputText(
title = string(R.string.archive_need_passwd),
@@ -1,18 +1,17 @@
package com.hippo.ehviewer.ui.screen
import android.os.Parcelable
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.util.containsIgnoreCase
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
@Serializable
data class DownloadsFilterState(
val mode: DownloadsFilterMode,
val label: String?,
val state: Int = -1,
val keyword: String = "",
) : Parcelable
)
fun DownloadsFilterState.take(info: DownloadInfo) = mode.take(info, label) &&
(state == -1 || info.state == state) &&
@@ -1,8 +1,6 @@
package com.hippo.ehviewer.ui.screen
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.ViewConfiguration
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.Crossfade
@@ -79,7 +77,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import arrow.core.partially1
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
@@ -121,6 +118,7 @@ import com.hippo.ehviewer.ui.tools.awaitSelectItemWithCheckBox
import com.hippo.ehviewer.ui.tools.awaitSingleChoice
import com.hippo.ehviewer.ui.tools.rememberHapticFeedback
import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.ui.tools.rememberSerializable
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.mapToLongArray
import com.hippo.ehviewer.util.takeAndClear
@@ -145,7 +143,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
var gridView by Settings.gridView.asMutableState()
var sortMode by Settings.downloadSortMode.asMutableState()
val filterMode by Settings.downloadFilterMode.collectAsState { DownloadsFilterMode.from(it) }
var filterState by rememberSaveable { mutableStateOf(DownloadsFilterState(filterMode, Settings.recentDownloadLabel.value)) }
var filterState by rememberSerializable { mutableStateOf(DownloadsFilterState(filterMode, Settings.recentDownloadLabel.value)) }
var invalidateKey by rememberSaveable { mutableStateOf(false) }
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
var searchBarOffsetY by remember { mutableIntStateOf(0) }
@@ -483,9 +481,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
text = { Text(text = stringResource(id = R.string.download_start_all)) },
onClick = {
expanded = false
val intent = Intent(contextOf<Activity>(), DownloadService::class.java)
intent.action = DownloadService.ACTION_START_ALL
ContextCompat.startForegroundService(contextOf<Activity>(), intent)
DownloadService.startService(DownloadService.ACTION_START_ALL)
},
)
DropdownMenuItem(
@@ -517,10 +513,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
onClick = {
expanded = false
val gidList = list.filter { it.state != DownloadInfo.STATE_FINISH }.asReversed().mapToLongArray(DownloadInfo::gid)
val intent = Intent(contextOf<Activity>(), DownloadService::class.java)
intent.action = DownloadService.ACTION_START_RANGE
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
ContextCompat.startForegroundService(contextOf<Activity>(), intent)
DownloadService.startRangeDownload(gidList)
},
)
}
@@ -602,10 +595,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
checkedInfoMap[info.gid] = info
},
onStart = {
val intent = Intent(contextOf<Activity>(), DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo)
ContextCompat.startForegroundService(contextOf<Activity>(), intent)
DownloadService.startDownload(info.galleryInfo)
},
onStop = { launchIO { DownloadManager.stopDownload(info.gid) } },
info = info,
@@ -696,10 +686,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
}
onClick(Icons.Default.PlayArrow) {
val gidList = checkedInfoMap.takeAndClear().mapToLongArray(DownloadInfo::gid)
val intent = Intent(contextOf<Activity>(), DownloadService::class.java)
intent.action = DownloadService.ACTION_START_RANGE
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
ContextCompat.startForegroundService(contextOf<Context>(), intent)
DownloadService.startRangeDownload(gidList)
}
onClick(Icons.Default.Pause) {
val gidList = checkedInfoMap.takeAndClear().mapToLongArray(DownloadInfo::gid)
@@ -2,7 +2,6 @@ package com.hippo.ehviewer.ui.screen
import android.content.Context
import android.view.ViewConfiguration
import androidx.collection.MutableLongSet
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -51,14 +50,8 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.filter
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -83,28 +76,23 @@ import com.hippo.ehviewer.ui.startDownload
import com.hippo.ehviewer.ui.tools.asyncState
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitSelectItem
import com.hippo.ehviewer.ui.tools.foldToLoadResult
import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.mapToLongArray
import com.hippo.ehviewer.util.takeAndClear
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlin.math.roundToInt
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import moe.tarsin.coroutines.onEachLatest
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.coroutines.runSwallowingWithUI
import moe.tarsin.launch
import moe.tarsin.navigate
@Destination<RootGraph>
@Composable
fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) = Screen(navigator) {
fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, viewModel: FavoritesViewModel = viewModel()) = Screen(navigator) {
// Immutables
val localFavName = stringResource(R.string.local_favorites)
val cloudFavName = stringResource(R.string.cloud_favorites)
@@ -112,13 +100,12 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
val hasSignedIn by Settings.hasSignedIn.collectAsState()
// Meta State
var urlBuilder by rememberSaveable { mutableStateOf(FavListUrlBuilder(favCat = Settings.recentFavCat)) }
var urlBuilder by viewModel.urlBuilder
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
var searchBarOffsetY by remember { mutableIntStateOf(0) }
// Derived State
val keyword = urlBuilder.keyword
val isLocalFav = urlBuilder.favCat == FavListUrlBuilder.FAV_CAT_LOCAL
val favCatName = remember(urlBuilder) {
when (val favCat = urlBuilder.favCat) {
in 0..9 -> Settings.favCat[favCat]
@@ -132,58 +119,8 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
stringResource(R.string.favorites_title_2, favCatName, keyword)
}
val density = LocalDensity.current
val localFavCountFlow = rememberInVM { EhDB.localFavCount }
val searchBarHint = stringResource(R.string.search_bar_hint, favCatName)
val data = rememberInVM(isLocalFav) {
if (isLocalFav) {
Pager(PagingConfig(20, jumpThreshold = 40)) {
val keywordNow = urlBuilder.keyword.orEmpty()
if (keywordNow.isBlank()) {
EhDB.localFavLazyList
} else {
EhDB.searchLocalFav(keywordNow)
}
}
} else {
Pager(PagingConfig(25)) {
object : PagingSource<String, BaseGalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
override suspend fun load(params: LoadParams<String>) = withIOContext {
when (params) {
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
is LoadParams.Append -> urlBuilder.setIndex(params.key, isNext = true)
is LoadParams.Refresh -> {
val key = params.key
if (key.isNullOrBlank()) {
if (urlBuilder.jumpTo != null) {
urlBuilder.next ?: urlBuilder.setIndex("2", true)
}
} else {
urlBuilder.setIndex(key, false)
}
}
}
runSuspendCatching {
EhEngine.getFavorites(urlBuilder.build())
}.foldToLoadResult { result ->
Settings.favCat = result.catArray.toTypedArray()
Settings.favCount = result.countArray.toIntArray()
Settings.favCloudCount = result.countArray.sum()
urlBuilder.jumpTo = null
LoadResult.Page(result.galleryInfoList, result.prev, result.next)
}
}
}
}
}.flow.map { pagingData ->
// https://github.com/FooIbar/EhViewer/issues/1190
// Workaround for duplicate items when sorting by favorited time
val gidSet = MutableLongSet(50)
pagingData.filter {
gidSet.add(it.gid)
}
}.cachedIn(viewModelScope)
}.collectAsLazyPagingItems()
val data = viewModel.data.collectAsLazyPagingItems()
fun refresh(newUrlBuilder: FavListUrlBuilder = urlBuilder.copy(jumpTo = null, prev = null, next = null)) {
urlBuilder = newUrlBuilder
@@ -191,7 +128,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
}
ProvideSideSheetContent { sheetState ->
val localFavCount by localFavCountFlow.collectAsState(0)
val localFavCount by viewModel.localFavCount.collectAsState(0)
TopAppBar(
title = { Text(text = stringResource(id = R.string.collections)) },
windowInsets = WindowInsets(),
@@ -249,7 +186,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
},
title = title,
searchFieldHint = searchBarHint,
tagNamespace = !isLocalFav,
tagNamespace = !urlBuilder.isLocal,
searchBarOffsetY = { searchBarOffsetY },
trailingIcon = {
val sheetState = LocalSideSheetState.current
@@ -366,7 +303,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
autoCancel = !selectMode,
) {
if (!selectMode) {
if (isLocalFav) {
if (urlBuilder.isLocal) {
onClick(Icons.Default.Shuffle) {
EhDB.randomLocalFav()?.let { info ->
withUIContext { navigate(info.asDst()) }
@@ -0,0 +1,84 @@
package com.hippo.ehviewer.ui.screen
import androidx.collection.MutableLongSet
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.serialization.saved
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.paging.filter
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.ui.tools.foldToLoadResult
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import moe.tarsin.coroutines.runSuspendCatching
class FavoritesViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
val urlBuilder by savedStateHandle.saved(MutableStateSerializer()) {
mutableStateOf(FavListUrlBuilder(favCat = Settings.recentFavCat))
}
val localFavCount = EhDB.localFavCount
val data = snapshotFlow { urlBuilder.value.isLocal }.flatMapLatest { isLocalFav ->
if (isLocalFav) {
Pager(PagingConfig(20, jumpThreshold = 40)) {
val keywordNow = urlBuilder.value.keyword.orEmpty()
if (keywordNow.isBlank()) {
EhDB.localFavLazyList
} else {
EhDB.searchLocalFav(keywordNow)
}
}
} else {
Pager(PagingConfig(25)) {
object : PagingSource<String, BaseGalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
override suspend fun load(params: LoadParams<String>) = withIOContext {
val urlBuilder = urlBuilder.value
when (params) {
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
is LoadParams.Append -> urlBuilder.setIndex(params.key, isNext = true)
is LoadParams.Refresh -> {
val key = params.key
if (key.isNullOrBlank()) {
if (urlBuilder.jumpTo != null) {
urlBuilder.next ?: urlBuilder.setIndex("2", true)
}
} else {
urlBuilder.setIndex(key, false)
}
}
}
runSuspendCatching {
EhEngine.getFavorites(urlBuilder.build())
}.foldToLoadResult { result ->
Settings.favCat = result.catArray.toTypedArray()
Settings.favCount = result.countArray.toIntArray()
Settings.favCloudCount = result.countArray.sum()
urlBuilder.jumpTo = null
LoadResult.Page(result.galleryInfoList, result.prev, result.next)
}
}
}
}
}.flow.map { pagingData ->
// https://github.com/FooIbar/EhViewer/issues/1190
// Workaround for duplicate items when sorting by favorited time
val gidSet = MutableLongSet(50)
pagingData.filter { gidSet.add(it.gid) }
}
}.cachedIn(viewModelScope)
}
@@ -186,7 +186,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
val userCommentBackField = remember { mutableStateOf(TextFieldValue()) }
var userComment by userCommentBackField
var commentId by remember { mutableLongStateOf(-1) }
var comments by rememberSaveable { mutableStateOf(galleryDetail.comments) }
var comments by remember(galleryDetail) { mutableStateOf(galleryDetail.comments) }
LaunchedEffect(comments) {
galleryDetail.comments = comments
}
@@ -1,6 +1,5 @@
package com.hippo.ehviewer.ui.screen
import android.os.Parcelable
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.collection.SieveCache
import androidx.compose.animation.AnimatedVisibilityScope
@@ -75,7 +74,7 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.launchIO
import moe.tarsin.snackbar
@@ -85,14 +84,15 @@ typealias VoteTag = suspend GalleryDetail.(String, Int) -> Unit
val detailCache = SieveCache<Long, GalleryDetail>(25)
sealed interface GalleryDetailScreenArgs : Parcelable
@Serializable
sealed interface GalleryDetailScreenArgs
@Parcelize
@Serializable
data class GalleryInfoArgs(
val galleryInfo: BaseGalleryInfo,
) : GalleryDetailScreenArgs
@Parcelize
@Serializable
data class TokenArgs(
val gid: Long,
val token: String,
@@ -62,13 +62,9 @@ import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.paging.compose.collectAsLazyPagingItems
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
@@ -76,10 +72,8 @@ import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_IMAGE_SEARCH
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_NORMAL
@@ -117,9 +111,7 @@ import com.hippo.ehviewer.ui.tools.asyncState
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitInputText
import com.hippo.ehviewer.ui.tools.awaitInputTextWithCheckBox
import com.hippo.ehviewer.ui.tools.foldToLoadResult
import com.hippo.ehviewer.ui.tools.rememberHapticFeedback
import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.ui.tools.rememberMutableStateInDataStore
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.FavouriteStatusRouter
@@ -127,13 +119,11 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.Direction
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlin.math.roundToInt
import kotlin.random.Random
import kotlinx.coroutines.delay
import moe.tarsin.coroutines.onEachLatest
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.launch
import moe.tarsin.launchIO
import moe.tarsin.navigate
@@ -160,9 +150,13 @@ fun AnimatedVisibilityScope.ToplistScreen(navigator: DestinationsNavigator) = Ga
@Destination<RootGraph>
@Composable
fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = Screen(navigator) {
fun AnimatedVisibilityScope.GalleryListScreen(
lub: ListUrlBuilder,
navigator: DestinationsNavigator,
viewModel: GalleryListViewModel = viewModel { GalleryListViewModel(lub, createSavedStateHandle()) },
) = Screen(navigator) {
val searchFieldState = rememberTextFieldState()
var urlBuilder by rememberSaveable { mutableStateOf(lub) }
var urlBuilder by viewModel.urlBuilder
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
var searchBarOffsetY by remember { mutableIntStateOf(0) }
val animateItems by Settings.animateItems.collectAsState()
@@ -193,49 +187,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
val exHint = stringResource(R.string.gallery_list_search_bar_hint_exhentai)
val searchBarHint by rememberUpdatedState(if (EhUtils.isExHentai) exHint else ehHint)
val suitableTitle = getSuitableTitleForUrlBuilder(urlBuilder)
val data = rememberInVM {
Pager(PagingConfig(25)) {
object : PagingSource<String, BaseGalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
override suspend fun load(params: LoadParams<String>) = withIOContext {
if (urlBuilder.mode == MODE_TOPLIST) {
// TODO: Since we know total pages, let pager support jump
val key = (params.key ?: urlBuilder.jumpTo)?.toInt() ?: 0
val prev = (key - 1).takeIf { it > 0 }
val next = (key + 1).takeIf { it < TOPLIST_PAGES }
runSuspendCatching {
urlBuilder.setJumpTo(key)
EhEngine.getGalleryList(urlBuilder.build())
}.foldToLoadResult { result ->
LoadResult.Page(result.galleryInfoList, prev?.toString(), next?.toString())
}
} else {
when (params) {
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
is LoadParams.Append -> urlBuilder.setIndex(params.key, isNext = true)
is LoadParams.Refresh -> {
val key = params.key
if (key.isNullOrBlank()) {
if (urlBuilder.jumpTo != null) {
urlBuilder.next ?: urlBuilder.setIndex("2", true)
}
} else {
urlBuilder.setIndex(key, false)
}
}
}
runSuspendCatching {
val url = urlBuilder.build()
EhEngine.getGalleryList(url)
}.foldToLoadResult { result ->
urlBuilder.jumpTo = null
LoadResult.Page(result.galleryInfoList, result.prev, result.next)
}
}
}
}
}.flow.cachedIn(viewModelScope)
}.collectAsLazyPagingItems()
val data = viewModel.data.collectAsLazyPagingItems()
ReportDrawnWhen { data.loadState.refresh !is LoadState.Loading }
FavouriteStatusRouter.Observe(data)
val listMode by Settings.listMode.collectAsState()
@@ -660,7 +612,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
}
}
private const val TOPLIST_PAGES = 200
const val TOPLIST_PAGES = 200
context(_: Context)
@Composable
@@ -0,0 +1,69 @@
package com.hippo.ehviewer.ui.screen
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.serialization.saved
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
import com.hippo.ehviewer.ui.tools.foldToLoadResult
import eu.kanade.tachiyomi.util.lang.withIOContext
import moe.tarsin.coroutines.runSuspendCatching
class GalleryListViewModel(lub: ListUrlBuilder, savedStateHandle: SavedStateHandle) : ViewModel() {
val urlBuilder by savedStateHandle.saved(MutableStateSerializer()) {
mutableStateOf(lub)
}
val data = Pager(PagingConfig(25)) {
object : PagingSource<String, BaseGalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
override suspend fun load(params: LoadParams<String>) = withIOContext {
val urlBuilder = urlBuilder.value
if (urlBuilder.mode == MODE_TOPLIST) {
// TODO: Since we know total pages, let pager support jump
val key = (params.key ?: urlBuilder.jumpTo)?.toInt() ?: 0
val prev = (key - 1).takeIf { it > 0 }
val next = (key + 1).takeIf { it < TOPLIST_PAGES }
runSuspendCatching {
urlBuilder.setJumpTo(key)
EhEngine.getGalleryList(urlBuilder.build())
}.foldToLoadResult { result ->
LoadResult.Page(result.galleryInfoList, prev?.toString(), next?.toString())
}
} else {
when (params) {
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
is LoadParams.Append -> urlBuilder.setIndex(params.key, isNext = true)
is LoadParams.Refresh -> {
val key = params.key
if (key.isNullOrBlank()) {
if (urlBuilder.jumpTo != null) {
urlBuilder.next ?: urlBuilder.setIndex("2", true)
}
} else {
urlBuilder.setIndex(key, false)
}
}
}
runSuspendCatching {
val url = urlBuilder.build()
EhEngine.getGalleryList(url)
}.foldToLoadResult { result ->
urlBuilder.jumpTo = null
LoadResult.Page(result.galleryInfoList, result.prev, result.next)
}
}
}
}
}.flow.cachedIn(viewModelScope)
}
@@ -5,8 +5,12 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.savedstate.serialization.SavedStateConfiguration
import kotlin.reflect.KMutableProperty0
import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer
val <T> KMutableProperty0<T>.observed: MutableState<T>
@Composable
@@ -29,3 +33,11 @@ val <T> MutableState<T>.rememberedAccessor: KMutableProperty0<T>
var value by this@rememberedAccessor
}::value
}
@Composable
inline fun <reified T : Any> rememberSerializable(
vararg inputs: Any?,
stateSerializer: KSerializer<T> = serializer<T>(),
configuration: SavedStateConfiguration = SavedStateConfiguration.DEFAULT,
noinline init: () -> MutableState<T>,
) = rememberSaveable(inputs = inputs, stateSerializer, configuration, init)
-1
View File
@@ -2,7 +2,6 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.spotless) apply false
-1
View File
@@ -136,7 +136,6 @@ android-application = { id = "com.android.application", version.ref = "agp" }
composeCompilerReportGenerator = { id = "dev.shreyaspatil.compose-compiler-report-generator", version = "1.4.2" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
spotless = { id = "com.diffplug.spotless", version = "7.0.3" }