Add avatar (#1720)
* Load Avatar * Click Avatar to show image limit * Shared Limit among collection * Cleanup and configure heartbeat * Always refresh avatars * Refine icons * Avoid heartbeat * Fix mistake * No need to SharingStarted.WhileSubscribed * Address code review * Simply map error msg * Convert limitFlow to stateFlow * Fixes * Refine retry logic * Don't retry automatically * Don't cache error result * Animate quota changes * Logout from guest mode via avatar * No need to navigate explicitly as needSignIn is observed * Make PostLogin not suspend * More UX friendly * Fix progressIndicator not disappear * Use Either.catch.mapLeft * Refine * Add overload for awaitConfirmationOrCancel * Log error with snackbar * Refine * Fix to use collectLatest * Refine * Refine * Address cr * Refine * Animated loading * Refine * Fix * Fix * Refine image limits dialog --------- Co-authored-by: Dude so hot <djohn@fbi.gov> Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
This commit is contained in:
15 files changed
+270
-186
No files matched your search
@@ -116,6 +116,7 @@ object Settings : DataStorePreferences(null) {
|
||||
val downloadSortMode = intPref("download_sort_mode", SortMode.Default.flag)
|
||||
val downloadFilterMode = intPref("download_filter_mode", DownloadsFilterMode.Default.flag)
|
||||
val hasSignedIn = boolPref("has_signed_in", EhCookieStore.hasSignedIn())
|
||||
val needSignIn = boolPref("need_sign_in", true)
|
||||
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
|
||||
val blackDarkTheme = boolPref("black_dark_theme", false)
|
||||
val gridView = boolPref("grid_view", false)
|
||||
@@ -124,6 +125,7 @@ object Settings : DataStorePreferences(null) {
|
||||
val security = boolPref("require_unlock", false)
|
||||
val animateItems = boolPref("animate_items", true)
|
||||
val displayName = stringOrNullPref("display_name", null)
|
||||
val avatar = stringOrNullPref("avatar", null)
|
||||
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
|
||||
|
||||
var downloadScheme by stringOrNullPref("image_scheme", null)
|
||||
@@ -157,7 +159,6 @@ object Settings : DataStorePreferences(null) {
|
||||
var saveParseErrorBody by boolPref("save_parse_error_body", true)
|
||||
var saveCrashLog by boolPref("save_crash_log", true)
|
||||
var removeImageFiles by boolPref("include_pic", true)
|
||||
var needSignIn by boolPref("need_sign_in", true)
|
||||
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
||||
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
||||
var downloadOriginImage by boolPref("download_origin_image", false)
|
||||
|
||||
@@ -153,7 +153,7 @@ object EhUtils {
|
||||
Settings.displayName.value = null
|
||||
Settings.hasSignedIn.value = false
|
||||
Settings.gallerySite = EhUrl.SITE_E
|
||||
Settings.needSignIn = true
|
||||
Settings.needSignIn.value = true
|
||||
}
|
||||
|
||||
fun getSuitableTitle(gi: GalleryInfo): String = if (Settings.showJpnTitle) {
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
|
||||
@@ -35,7 +36,7 @@ object HomeParser {
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Result(val limits: Limits, val funds: Funds) : Parcelable
|
||||
data class Result(val limits: Limits, val funds: Funds, private val id: Int = Random.nextInt()) : Parcelable
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
||||
@@ -376,6 +376,7 @@ class MainActivity : EhActivity() {
|
||||
val drawerEnabled = drawerHandle.isNotEmpty()
|
||||
val density = LocalDensity.current
|
||||
val adaptiveInfo = currentWindowAdaptiveInfo()
|
||||
val needSignIn by Settings.needSignIn.collectAsState()
|
||||
CompositionLocalProvider(
|
||||
LocalNavDrawerState provides navDrawerState,
|
||||
LocalSideSheetState provides sideSheetState,
|
||||
@@ -473,7 +474,7 @@ class MainActivity : EhActivity() {
|
||||
// SharedTransitionLayout {
|
||||
CompositionLocalProvider(LocalSharedTransitionScope provides NoopSharedTransitionScope) {
|
||||
val start = when {
|
||||
Settings.needSignIn -> SignInScreenDestination
|
||||
needSignIn -> SignInScreenDestination
|
||||
hasNetwork -> StartDestination
|
||||
else -> DownloadsScreenDestination
|
||||
}
|
||||
|
||||
@@ -5,19 +5,26 @@ import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
suspend fun postLogin() = coroutineScope {
|
||||
launch {
|
||||
runCatching {
|
||||
EhEngine.getProfile().run {
|
||||
Settings.displayName.value = displayName
|
||||
}
|
||||
}.onFailure {
|
||||
logcat(it)
|
||||
}
|
||||
suspend fun CoroutineScope.refreshAccountInfo() = runSuspendCatching {
|
||||
with(EhEngine.getProfile()) {
|
||||
Settings.displayName.value = displayName
|
||||
Settings.avatar.value = avatar
|
||||
}
|
||||
}.onFailure {
|
||||
logcat(it)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun postLogin() = GlobalScope.async(Dispatchers.IO) {
|
||||
launch { refreshAccountInfo() }
|
||||
runCatching {
|
||||
// For the `star` cookie
|
||||
EhEngine.getNews(false)
|
||||
@@ -34,12 +41,14 @@ suspend fun postLogin() = coroutineScope {
|
||||
|
||||
// Sad panda check
|
||||
Settings.gallerySite = EhUrl.SITE_EX
|
||||
|
||||
// Explicitly use ex url since https://github.com/Ehviewer-Overhauled/Ehviewer/issues/1239#issuecomment-1632584525
|
||||
EhEngine.getUConfig(EhUrl.URL_UCONFIG_EX)
|
||||
EhCookieStore.flush()
|
||||
}.onFailure {
|
||||
Settings.gallerySite = EhUrl.SITE_E
|
||||
}
|
||||
|
||||
Settings.hasSignedIn.value = true
|
||||
Settings.needSignIn = false
|
||||
Settings.needSignIn.value = false
|
||||
}
|
||||
@@ -60,10 +60,8 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.ui.StartDestination
|
||||
import com.hippo.ehviewer.ui.destinations.WebViewSignInScreenDestination
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
||||
import com.hippo.ehviewer.ui.tools.autofill
|
||||
@@ -72,7 +70,6 @@ import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
@@ -116,6 +113,7 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
}.onFailure {
|
||||
withUIContext {
|
||||
focusManager.clearFocus()
|
||||
isProgressIndicatorVisible = false
|
||||
dialogState.awaitConfirmationOrCancel(
|
||||
confirmText = R.string.get_it,
|
||||
title = R.string.sign_in_failed,
|
||||
@@ -129,11 +127,9 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
)
|
||||
},
|
||||
)
|
||||
isProgressIndicatorVisible = false
|
||||
}
|
||||
}.onSuccess {
|
||||
withNonCancellableContext { postLogin() }
|
||||
withUIContext { navigator.popNavigate(StartDestination) }
|
||||
postLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,9 +232,8 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
Settings.needSignIn = false
|
||||
Settings.gallerySite = EhUrl.SITE_E
|
||||
navigator.popNavigate(StartDestination)
|
||||
Settings.needSignIn.value = false
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
@@ -319,9 +314,8 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
Settings.needSignIn = false
|
||||
Settings.gallerySite = EhUrl.SITE_E
|
||||
navigator.popNavigate(StartDestination)
|
||||
Settings.needSignIn.value = false
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp).width(128.dp),
|
||||
) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.google.accompanist.web.AccompanistWebViewClient
|
||||
import com.google.accompanist.web.WebView
|
||||
@@ -13,35 +13,26 @@ import com.google.accompanist.web.rememberWebViewState
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.ui.StartDestination
|
||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||
import com.hippo.ehviewer.ui.composing
|
||||
import com.hippo.ehviewer.util.bgWork
|
||||
import com.hippo.ehviewer.util.setDefaultSettings
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun WebViewSignInScreen(navigator: DestinationsNavigator) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun AnimatedVisibilityScope.WebViewSignInScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
val state = rememberWebViewState(url = EhUrl.URL_SIGN_IN)
|
||||
val client = remember {
|
||||
object : AccompanistWebViewClient() {
|
||||
private var present = false
|
||||
override fun onPageFinished(view: WebView, url: String?) {
|
||||
if (present) {
|
||||
view.destroy()
|
||||
return
|
||||
}
|
||||
if (EhCookieStore.hasSignedIn()) {
|
||||
present = true
|
||||
coroutineScope.launchIO {
|
||||
withNonCancellableContext { postLogin() }
|
||||
withUIContext { navigator.popNavigate(StartDestination) }
|
||||
}
|
||||
postLogin()
|
||||
view.destroy()
|
||||
launch { bgWork { awaitCancellation() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,9 +43,7 @@ fun WebViewSignInScreen(navigator: DestinationsNavigator) {
|
||||
WebView(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onCreated = {
|
||||
it.setDefaultSettings()
|
||||
},
|
||||
onCreated = { it.setDefaultSettings() },
|
||||
client = client,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.hippo.ehviewer.ui.main
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.NoAccounts
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.RestartAlt
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import arrow.core.Either
|
||||
import arrow.core.Either.Companion.catch
|
||||
import arrow.core.Option
|
||||
import arrow.core.none
|
||||
import arrow.core.some
|
||||
import coil3.compose.AsyncImage
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSwallowingWithUI
|
||||
|
||||
private val limitScope = CoroutineScope(Dispatchers.IO)
|
||||
private val refreshEvent = MutableSharedFlow<Unit>()
|
||||
private val invalidateEvent = MutableSharedFlow<Unit>()
|
||||
|
||||
typealias Result = Option<Either<String, HomeParser.Result>>
|
||||
|
||||
private val limitFlow: StateFlow<Result> = refreshEvent.conflate()
|
||||
.onEach {
|
||||
// If cached value is error, drop it when refresh
|
||||
val isError = limitFlow.value.isSome { it.isLeft() }
|
||||
if (isError) invalidateEvent.emit(Unit)
|
||||
}
|
||||
.map { catch { EhEngine.getImageLimits() }.mapLeft { e -> e.displayString() }.some() }
|
||||
.let { src -> merge(src, invalidateEvent.map { none() }) }
|
||||
.stateIn(limitScope, SharingStarted.Eagerly, none())
|
||||
|
||||
context(CoroutineScope, DialogState, SnackbarHostState, DestinationsNavigator)
|
||||
@Composable
|
||||
fun AvatarIcon() {
|
||||
val hasSignedIn by Settings.hasSignedIn.collectAsState()
|
||||
if (hasSignedIn) {
|
||||
val placeholder = stringResource(id = R.string.please_wait)
|
||||
val result by limitFlow.collectAsState()
|
||||
IconButton(
|
||||
onClick = {
|
||||
launch {
|
||||
refreshEvent.emit(Unit)
|
||||
awaitConfirmationOrCancel(
|
||||
title = R.string.image_limits,
|
||||
showConfirmButton = false,
|
||||
showCancelButton = false,
|
||||
) {
|
||||
val animatedAlpha by animateFloatAsState(if (remember { result } == result) 0.5f else 1f)
|
||||
result.onNone {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Text(text = placeholder)
|
||||
}
|
||||
}
|
||||
result.onSome { current ->
|
||||
Box(modifier = Modifier.graphicsLayer { alpha = animatedAlpha }) {
|
||||
when (current) {
|
||||
is Either.Left -> Text(text = current.value)
|
||||
is Either.Right -> Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
val (limits, funds) = current.value
|
||||
if (limits.maximum > 0) {
|
||||
val value by animateFloatAsState(limits.current.toFloat() / limits.maximum)
|
||||
LinearProgressIndicator(
|
||||
progress = { value },
|
||||
modifier = Modifier.height(12.dp).fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = when (limits.maximum) {
|
||||
-1 -> stringResource(id = R.string.image_limits_restricted)
|
||||
0 -> stringResource(id = R.string.image_limits_normal)
|
||||
else -> stringResource(id = R.string.image_limits_summary, limits.current, limits.maximum)
|
||||
},
|
||||
)
|
||||
Text(text = stringResource(id = R.string.current_funds))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
FundsItem(
|
||||
type = "GP",
|
||||
amount = funds.gp,
|
||||
)
|
||||
FundsItem(
|
||||
type = "C",
|
||||
amount = funds.credit,
|
||||
)
|
||||
}
|
||||
if (limits.resetCost > 0) {
|
||||
Button(
|
||||
onClick = {
|
||||
launchIO {
|
||||
runSwallowingWithUI {
|
||||
invalidateEvent.emit(Unit)
|
||||
EhEngine.resetImageLimits()
|
||||
refreshEvent.emit(Unit)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RestartAlt,
|
||||
contentDescription = stringResource(id = R.string.reset),
|
||||
)
|
||||
Text(text = stringResource(id = R.string.reset_cost, limits.resetCost))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
val avatar by Settings.avatar.collectAsState()
|
||||
AnimatedContent(targetState = avatar == null) { noAvatar ->
|
||||
if (noAvatar) {
|
||||
Icon(imageVector = Icons.Default.Person, contentDescription = null)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = avatar,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clip(CircleShape),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
IconButton(
|
||||
onClick = {
|
||||
launch {
|
||||
awaitConfirmationOrCancel(
|
||||
confirmText = R.string.sign_in,
|
||||
showCancelButton = false,
|
||||
text = { Text(text = stringResource(id = R.string.settings_eh_identity_cookies_guest)) },
|
||||
)
|
||||
EhUtils.signOut()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.NoAccounts, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ import com.hippo.ehviewer.ui.DrawerHandle
|
||||
import com.hippo.ehviewer.ui.LocalSideSheetState
|
||||
import com.hippo.ehviewer.ui.awaitSelectDate
|
||||
import com.hippo.ehviewer.ui.composing
|
||||
import com.hippo.ehviewer.ui.main.AvatarIcon
|
||||
import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME
|
||||
import com.hippo.ehviewer.ui.main.FabLayout
|
||||
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
|
||||
@@ -246,6 +247,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
|
||||
IconButton(onClick = { launch { sheetState.open() } }) {
|
||||
Icon(imageVector = Icons.Outlined.FolderSpecial, contentDescription = null)
|
||||
}
|
||||
AvatarIcon()
|
||||
},
|
||||
) { contentPadding ->
|
||||
val listMode by Settings.listMode.collectAsState()
|
||||
|
||||
@@ -99,6 +99,7 @@ import com.hippo.ehviewer.ui.composing
|
||||
import com.hippo.ehviewer.ui.destinations.ProgressScreenDestination
|
||||
import com.hippo.ehviewer.ui.doGalleryInfoAction
|
||||
import com.hippo.ehviewer.ui.main.AdvancedSearchOption
|
||||
import com.hippo.ehviewer.ui.main.AvatarIcon
|
||||
import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME
|
||||
import com.hippo.ehviewer.ui.main.FabLayout
|
||||
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
|
||||
@@ -522,6 +523,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
|
||||
IconButton(onClick = { launch { sheetState.open() } }) {
|
||||
Icon(imageVector = Icons.Outlined.Bookmarks, contentDescription = stringResource(id = R.string.quick_search))
|
||||
}
|
||||
AvatarIcon()
|
||||
},
|
||||
filter = {
|
||||
SearchFilter(
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package com.hippo.ehviewer.ui.settings
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -14,10 +10,8 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
@@ -26,15 +20,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -43,38 +32,28 @@ import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.asMutableState
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.ui.destinations.FilterScreenDestination
|
||||
import com.hippo.ehviewer.ui.destinations.MyTagsScreenDestination
|
||||
import com.hippo.ehviewer.ui.destinations.SignInScreenDestination
|
||||
import com.hippo.ehviewer.ui.destinations.UConfigScreenDestination
|
||||
import com.hippo.ehviewer.ui.main.FundsItem
|
||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||
import com.hippo.ehviewer.util.copyTextToClipboard
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.isAtLeastT
|
||||
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalTime
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
@@ -100,7 +79,6 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
val guestMode = stringResource(id = R.string.settings_eh_identity_cookies_guest)
|
||||
val copiedToClipboard = stringResource(id = R.string.copied_to_clipboard)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -108,21 +86,21 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(paddingValues),
|
||||
) {
|
||||
val displayName by Settings.displayName.collectAsState()
|
||||
Preference(
|
||||
title = stringResource(id = R.string.account_name),
|
||||
summary = displayName ?: guestMode,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
val cookies = EhCookieStore.getIdentityCookies()
|
||||
dialogState.awaitConfirmationOrCancel(
|
||||
confirmText = R.string.settings_eh_sign_out,
|
||||
dismissText = R.string.settings_eh_clear_igneous,
|
||||
showCancelButton = cookies.last().second != null,
|
||||
onCancelButtonClick = { EhCookieStore.clearIgneous() },
|
||||
secure = hasSignedIn,
|
||||
) {
|
||||
if (hasSignedIn) {
|
||||
if (hasSignedIn) {
|
||||
val displayName by Settings.displayName.collectAsState()
|
||||
Preference(
|
||||
title = stringResource(id = R.string.account_name),
|
||||
summary = displayName,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
val cookies = EhCookieStore.getIdentityCookies()
|
||||
dialogState.awaitConfirmationOrCancel(
|
||||
confirmText = R.string.settings_eh_sign_out,
|
||||
dismissText = R.string.settings_eh_clear_igneous,
|
||||
showCancelButton = cookies.last().second != null,
|
||||
onCancelButtonClick = { EhCookieStore.clearIgneous() },
|
||||
secure = true,
|
||||
) {
|
||||
Column {
|
||||
val warning = stringResource(id = R.string.settings_eh_identity_cookies_signed)
|
||||
val str = cookies.joinToString("\n") { (k, v) -> "$k: $v" }
|
||||
@@ -143,101 +121,8 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(text = guestMode)
|
||||
}
|
||||
}
|
||||
EhUtils.signOut()
|
||||
withUIContext {
|
||||
navigator.popNavigate(SignInScreenDestination)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasSignedIn) {
|
||||
val placeholder = stringResource(id = R.string.please_wait)
|
||||
val resetImageLimitSucceed = stringResource(id = R.string.reset_limits_succeed)
|
||||
var result by rememberSaveable { mutableStateOf<HomeParser.Result?>(null) }
|
||||
var error by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val summary by rememberUpdatedState(
|
||||
result?.run {
|
||||
when (limits.maximum) {
|
||||
-1 -> stringResource(id = R.string.image_limits_restricted)
|
||||
0 -> stringResource(id = R.string.image_limits_normal)
|
||||
else -> stringResource(id = R.string.image_limits_summary, limits.current, limits.maximum)
|
||||
}
|
||||
} ?: placeholder,
|
||||
)
|
||||
suspend fun getImageLimits() {
|
||||
result = EhEngine.getImageLimits()
|
||||
error = null
|
||||
}
|
||||
if (result == null && error == null) {
|
||||
LaunchedEffect(Unit) {
|
||||
runSuspendCatching {
|
||||
withIOContext { getImageLimits() }
|
||||
}.onFailure {
|
||||
error = it.displayString()
|
||||
}
|
||||
}
|
||||
}
|
||||
Preference(
|
||||
title = stringResource(id = R.string.image_limits),
|
||||
summary = summary,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
dialogState.awaitConfirmationOrCancel(
|
||||
confirmText = R.string.reset,
|
||||
title = R.string.image_limits,
|
||||
confirmButtonEnabled = result?.run { limits.resetCost != 0 } ?: false,
|
||||
) {
|
||||
error?.let {
|
||||
Text(text = it)
|
||||
} ?: result?.let { (limits, funds) ->
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (limits.maximum > 0) {
|
||||
LinearProgressIndicator(
|
||||
progress = { limits.current.toFloat() / limits.maximum },
|
||||
modifier = Modifier.height(12.dp).fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
Text(text = summary)
|
||||
if (limits.resetCost > 0) {
|
||||
Text(text = stringResource(id = R.string.reset_cost, limits.resetCost))
|
||||
}
|
||||
Text(text = stringResource(id = R.string.current_funds))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
FundsItem(
|
||||
type = "GP",
|
||||
amount = funds.gp,
|
||||
)
|
||||
FundsItem(
|
||||
type = "C",
|
||||
amount = funds.credit,
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Text(text = placeholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
runSuspendCatching {
|
||||
EhEngine.resetImageLimits()
|
||||
getImageLimits()
|
||||
}.onSuccess {
|
||||
launchSnackBar(resetImageLimitSucceed)
|
||||
}.onFailure {
|
||||
error = it.displayString()
|
||||
}
|
||||
EhUtils.signOut()
|
||||
}
|
||||
}
|
||||
SimpleMenuPreferenceInt(
|
||||
|
||||
@@ -276,7 +276,7 @@ class DialogState {
|
||||
@StringRes confirmText: Int = android.R.string.ok,
|
||||
@StringRes dismissText: Int = android.R.string.cancel,
|
||||
@StringRes title: Int? = null,
|
||||
confirmButtonEnabled: Boolean = true,
|
||||
showConfirmButton: Boolean = true,
|
||||
showCancelButton: Boolean = true,
|
||||
onCancelButtonClick: () -> Unit = {},
|
||||
secure: Boolean = false,
|
||||
@@ -285,8 +285,10 @@ class DialogState {
|
||||
AlertDialog(
|
||||
onDismissRequest = { cont.cancel() },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { cont.resume(Unit) }, enabled = confirmButtonEnabled) {
|
||||
Text(text = stringResource(id = confirmText))
|
||||
if (showConfirmButton) {
|
||||
TextButton(onClick = { cont.resume(Unit) }) {
|
||||
Text(text = stringResource(id = confirmText))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = showCancelButton.ifTrueThen {
|
||||
|
||||
@@ -425,8 +425,7 @@
|
||||
<string name="image_limits_summary">已用:%1$d / %2$d</string>
|
||||
<string name="image_limits_normal">无限制</string>
|
||||
<string name="image_limits_restricted">图片分辨率限制为 1280x</string>
|
||||
<string name="reset_cost">重置花费:%d GP</string>
|
||||
<string name="reset_limits_succeed">成功重置图片配额</string>
|
||||
<string name="reset_cost">花费 %d GP 重置</string>
|
||||
<string name="reset">重置</string>
|
||||
<string name="settings_privacy">隐私</string>
|
||||
<string name="settings_privacy_secure">不允许屏幕抓取</string>
|
||||
|
||||
@@ -419,8 +419,7 @@
|
||||
<string name="insufficient_funds">餘額不足</string>
|
||||
<string name="image_limits">圖片配額</string>
|
||||
<string name="image_limits_summary">已用:%1$d / %2$d</string>
|
||||
<string name="reset_cost">重置花費:%d GP</string>
|
||||
<string name="reset_limits_succeed">成功重置圖片配額</string>
|
||||
<string name="reset_cost">花費 %d GP 重置</string>
|
||||
<string name="reset">重置</string>
|
||||
<string name="settings_privacy">隱私</string>
|
||||
<string name="settings_privacy_secure">不允許螢幕擷取</string>
|
||||
|
||||
@@ -446,8 +446,7 @@
|
||||
<string name="image_limits_summary">Used: %1$d / %2$d</string>
|
||||
<string name="image_limits_normal">No restrictions</string>
|
||||
<string name="image_limits_restricted">Image resolution restricted to 1280x</string>
|
||||
<string name="reset_cost">Reset Cost: %d GP</string>
|
||||
<string name="reset_limits_succeed">Image limit was successfully reset</string>
|
||||
<string name="reset_cost">Spend %d GP to reset</string>
|
||||
<string name="reset">Reset</string>
|
||||
<string name="settings_privacy">Privacy</string>
|
||||
<string name="settings_privacy_secure">Prevent screenshots</string>
|
||||
|
||||
Reference in new issue
Block a user