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:
authored and GitHub committed 2024-10-13 19:36:02 +08:00
1 parent e930ebf42f
commit 2c0d621b16
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 {
+1 -2
View File
@@ -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>
+1 -2
View File
@@ -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>
+1 -2
View File
@@ -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>