Migrate to context parameters

This commit is contained in:
renovate[bot] authored and GitHub committed 2025-05-23 04:57:48 +00:00
1 parent a8b6adfba2
commit 30c659110f
60 files changed
+1186 -1031

No files matched your search

+1 -2
View File
@@ -282,8 +282,7 @@ kotlin {
"splitties.preferences.DataStorePreferencesPreview",
)
freeCompilerArgs.addAll(
"-Xcontext-receivers",
"-Xsuppress-warning=CONTEXT_RECEIVERS_DEPRECATED",
"-Xcontext-parameters",
"-Xannotation-default-target=param-property",
)
}
@@ -23,7 +23,6 @@ import arrow.core.Either
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.ui.screen.implicit
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess
@@ -66,10 +65,10 @@ object EhTagDatabase : CoroutineScope {
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? = tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
context(Context)
context(ctx: Context)
fun suggestion(rawKeyword: String, expectTranslate: Boolean): Sequence<Pair<String, String?>> {
if (!initialized) return emptySequence()
val translate = expectTranslate && isTranslatable(implicit<Context>())
val translate = expectTranslate && translatable
val keyword = PREFIXES.fold(rawKeyword) { kwd, pfx -> kwd.removePrefix(pfx) }
val prefix = rawKeyword.dropLast(keyword.length)
val ns = keyword.substringBefore(':')
@@ -107,7 +106,9 @@ object EhTagDatabase : CoroutineScope {
private fun metadata(context: Context): Array<String> = context.resources.getStringArray(R.array.tag_translation_metadata)
fun isTranslatable(context: Context): Boolean = context.resources.getBoolean(R.bool.tag_translatable)
context(ctx: Context)
val translatable
get() = ctx.resources.getBoolean(R.bool.tag_translatable)
private suspend fun fetch(url: String) = ktorClient.prepareGet(url).executeSafely {
it.status.ensureSuccess()
@@ -179,7 +179,7 @@ object EhUtils {
return title.substringBeforeLast('|').trim().ifEmpty { null }
}
context(Context)
context(ctx: Context)
suspend fun downloadArchive(galleryDetail: GalleryDetail, archive: Archive) {
val gid = galleryDetail.gid
EhEngine.downloadArchive(gid, galleryDetail.token, archive.res, archive.isHAtH)?.let {
@@ -190,7 +190,7 @@ object EhUtils {
}
val name = "$gid-${getSuitableTitle(galleryDetail)}.zip"
try {
startActivity(intent)
ctx.startActivity(intent)
withUIContext { addTextToClipboard(name, true) }
} catch (_: ActivityNotFoundException) {
val r = DownloadManager.Request(uri)
@@ -90,16 +90,18 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
private val sizeResolver = SizeResolver(Size(targetWidth, Dimension.Undefined))
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
val request = appCtx.imageRequest {
onLeft { data(it.source) }
onRight { data(it.source.toUri()) }
size(sizeResolver)
precision(Precision.INEXACT)
allowHardware(false)
hardwareThreshold(Settings.hardwareBitmapThreshold)
maybeCropBorder(Settings.cropBorder.value)
detectQrCode(checkExtraneousAds)
memoryCachePolicy(CachePolicy.DISABLED)
val request = with(appCtx) {
imageRequest {
onLeft { data(it.source) }
onRight { data(it.source.toUri()) }
size(sizeResolver)
precision(Precision.INEXACT)
allowHardware(false)
hardwareThreshold(Settings.hardwareBitmapThreshold)
maybeCropBorder(Settings.cropBorder.value)
detectQrCode(checkExtraneousAds)
memoryCachePolicy(CachePolicy.DISABLED)
}
}
return when (val result = request.execute()) {
is SuccessResult -> result.image
@@ -12,9 +12,12 @@ import com.hippo.ehviewer.coil.ehUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
inline fun Context.imageRequest(preview: GalleryPreview, builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(this).ehPreview(preview).apply(builder).build()
inline fun Context.imageRequest(info: GalleryInfo, builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(this).ehUrl(info).apply(builder).build()
inline fun Context.imageRequest(builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(this).apply(builder).build()
context(ctx: Context)
inline fun imageRequest(preview: GalleryPreview, builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(ctx).ehPreview(preview).apply(builder).build()
context(ctx: Context)
inline fun imageRequest(info: GalleryInfo, builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(ctx).ehUrl(info).apply(builder).build()
context(ctx: Context)
inline fun imageRequest(builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(ctx).apply(builder).build()
inline fun diskCache(builder: DiskCache.Builder.() -> Unit) = DiskCache.Builder().apply(builder).build()
inline fun Context.imageLoader(builder: ImageLoader.Builder.() -> Unit) = ImageLoader.Builder(this).apply(builder).build()
suspend fun ImageRequest.execute() = context.imageLoader.execute(this)
@@ -110,7 +110,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
Settings.edit(true) {
enableCronet.value = false
}
config.context.restartApplication()
with(config.context) { restartApplication() }
}
if (continuation.isActive) {
continuation.resumeWithException(error)
@@ -16,7 +16,6 @@ import com.hippo.ehviewer.client.data.TagNamespace.Male
import com.hippo.ehviewer.client.data.TagNamespace.Mixed
import com.hippo.ehviewer.client.data.TagNamespace.Other
import com.hippo.ehviewer.client.data.TagNamespace.Parody
import com.hippo.ehviewer.ui.screen.implicit
import com.hippo.files.read
import com.hippo.files.write
import kotlinx.serialization.KSerializer
@@ -107,11 +106,7 @@ fun ComicInfo.toSimpleTags() = listOfNotNull(
teams,
).flatten().ifEmpty { null }
fun ComicInfo.write(file: Path) {
file.write {
xml.encodeToSink(this, implicit<ComicInfo>())
}
}
fun writeComicInfo(info: ComicInfo, file: Path) = file.write { xml.encodeToSink(this, info) }
fun readComicInfo(file: Path): ComicInfo? = runCatching {
file.read {
@@ -338,7 +338,7 @@ class SpiderDen(val info: GalleryInfo) {
EhEngine.fillGalleryListByApi(listOf(info))
}
info.getComicInfo().apply {
write(it)
writeComicInfo(this, it)
DownloadManager.getDownloadInfo(gid)?.let { downloadInfo ->
downloadInfo.artistInfoList = DownloadArtist.from(gid, penciller.orEmpty())
EhDB.putDownloadArtist(gid, downloadInfo.artistInfoList)
@@ -4,7 +4,6 @@ import com.hippo.ehviewer.coil.edit
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.legacy.readLegacySpiderInfo
import com.hippo.ehviewer.ui.screen.implicit
import com.hippo.files.read
import com.hippo.files.write
import eu.kanade.tachiyomi.util.system.logcat
@@ -37,9 +36,7 @@ private val cbor = Cbor {
ignoreUnknownKeys = true
}
fun SpiderInfo.write(file: Path) {
file.write { write(cbor.encodeToByteArray(implicit<SpiderInfo>())) }
}
fun SpiderInfo.write(file: Path) = file.write { write(cbor.encodeToByteArray(contextOf<SpiderInfo>())) }
fun SpiderInfo.saveToCache() {
runSuspendCatching {
@@ -58,9 +58,15 @@ import com.hippo.ehviewer.ui.destinations.ReaderScreenDestination
import com.hippo.ehviewer.ui.reader.ReaderScreenArgs
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitResult
import com.hippo.ehviewer.ui.tools.awaitSelectDate
import com.hippo.ehviewer.ui.tools.awaitSelectItem
import com.hippo.ehviewer.ui.tools.awaitSelectItemWithCheckBox
import com.hippo.ehviewer.ui.tools.awaitSelectItemWithIcon
import com.hippo.ehviewer.ui.tools.awaitSelectItemWithIconAndTextField
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.bgWork
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.util.isAtLeastT
import com.hippo.ehviewer.util.mapToLongArray
import com.hippo.ehviewer.util.requestPermission
@@ -73,7 +79,6 @@ import com.hippo.files.isDirectory
import com.hippo.files.write
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
@@ -83,6 +88,8 @@ import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
import kotlinx.datetime.todayIn
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.string
import moe.tarsin.tip
import okio.Path
import splitties.init.appCtx
@@ -110,11 +117,8 @@ suspend fun keepNoMediaFileStatus(downloadDir: Path = downloadLocation) {
fun getFavoriteIcon(favorited: Boolean) = if (favorited) Icons.Default.Favorite else Icons.Default.FavoriteBorder
suspend fun DialogState.startDownload(
context: Context,
forceDefault: Boolean,
vararg galleryInfos: BaseGalleryInfo,
) = with(context) {
context(_: DialogState, context: MainActivity)
suspend fun startDownload(forceDefault: Boolean, vararg galleryInfos: BaseGalleryInfo) {
if (isAtLeastT) {
requestPermission(Manifest.permission.POST_NOTIFICATIONS)
}
@@ -127,9 +131,7 @@ suspend fun DialogState.startDownload(
ContextCompat.startForegroundService(context, intent)
}
if (toAdd.isEmpty()) {
return with(findActivity<MainActivity>()) {
showTip(R.string.added_to_download_list)
}
return tip(R.string.added_to_download_list)
}
var justStart = forceDefault
var label: String? = null
@@ -153,14 +155,12 @@ suspend fun DialogState.startDownload(
ContextCompat.startForegroundService(context, intent)
}
// Notify
with(findActivity<MainActivity>()) {
showTip(R.string.added_to_download_list)
}
tip(R.string.added_to_download_list)
} else {
// Let use chose label
val list = DownloadManager.labelList
val items = buildList {
add(getString(R.string.default_download_label_name))
add(string(R.string.default_download_label_name))
list.forEach {
add(it.label)
}
@@ -186,13 +186,12 @@ suspend fun DialogState.startDownload(
} else {
Settings.hasDefaultDownloadLabel = false
}
with(context.findActivity<MainActivity>()) {
showTip(R.string.added_to_download_list)
}
tip(R.string.added_to_download_list)
}
}
suspend fun DialogState.modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
context(_: DialogState)
suspend fun modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
val localFavorited = EhDB.containLocalFavorites(galleryInfo.gid)
if (Settings.hasSignedIn.value) {
val isFavorited = galleryInfo.favoriteSlot != NOT_FAVORITED
@@ -283,13 +282,16 @@ suspend fun removeFromFavorites(galleryInfo: BaseGalleryInfo) = doModifyFavorite
localFavorited = EhDB.containLocalFavorites(galleryInfo.gid),
)
fun DestinationsNavigator.navToReader(info: BaseGalleryInfo, page: Int = -1) = navToReader(ReaderScreenArgs.Gallery(info, page))
context(_: DestinationsNavigator)
fun navToReader(info: BaseGalleryInfo, page: Int = -1) = navToReader(ReaderScreenArgs.Gallery(info, page))
fun DestinationsNavigator.navToReader(uri: Uri) = navToReader(ReaderScreenArgs.Archive(uri))
context(_: DestinationsNavigator)
fun navToReader(uri: Uri) = navToReader(ReaderScreenArgs.Archive(uri))
private fun DestinationsNavigator.navToReader(args: ReaderScreenArgs) = navigate(ReaderScreenDestination(args)) { launchSingleTop = true }
context(nav: DestinationsNavigator)
private fun navToReader(args: ReaderScreenArgs) = nav.navigate(ReaderScreenDestination(args)) { launchSingleTop = true }
context(DialogState, Context, DestinationsNavigator)
context(_: DialogState, _: MainActivity, _: DestinationsNavigator)
suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
val downloaded = DownloadManager.getDownloadState(info.gid) != DownloadInfo.STATE_INVALID
val favorited = info.favoriteSlot != NOT_FAVORITED
@@ -312,46 +314,42 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
}
}
val selected = awaitSelectItemWithIcon(items, EhUtils.getSuitableTitle(info))
with(findActivity<MainActivity>()) {
when (selected) {
0 -> {
EhDB.putHistoryInfo(info)
navToReader(info)
}
1 -> withUIContext {
if (downloaded) {
confirmRemoveDownload(info)
} else {
startDownload(this@with, false, info)
}
}
2 -> if (favorited) {
runSuspendCatching {
removeFromFavorites(info)
showTip(R.string.remove_from_favorite_success)
}.onFailure {
showTip(R.string.remove_from_favorite_failure)
}
} else {
runSuspendCatching {
modifyFavorites(info)
showTip(R.string.add_to_favorite_success)
}.onFailure {
showTip(R.string.add_to_favorite_failure)
}
}
3 -> showMoveDownloadLabel(info)
when (selected) {
0 -> {
EhDB.putHistoryInfo(info)
navToReader(info)
}
true
1 -> if (downloaded) {
confirmRemoveDownload(info)
} else {
startDownload(false, info)
}
2 -> if (favorited) {
runSuspendCatching {
removeFromFavorites(info)
tip(R.string.remove_from_favorite_success)
}.onFailure {
tip(R.string.remove_from_favorite_failure)
}
} else {
runSuspendCatching {
modifyFavorites(info)
tip(R.string.add_to_favorite_success)
}.onFailure {
tip(R.string.add_to_favorite_failure)
}
}
3 -> showMoveDownloadLabel(info)
}
}
private const val MAX_FAVNOTE_CHAR = 200
private suspend fun DialogState.confirmRemoveDownload(text: String): Boolean {
context(_: DialogState)
private suspend fun confirmRemoveDownload(text: String): Boolean {
val checked = awaitResult(
initial = Settings.removeImageFiles,
title = R.string.download_remove_dialog_title,
@@ -375,7 +373,8 @@ private suspend fun DialogState.confirmRemoveDownload(text: String): Boolean {
return checked
}
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo) {
context(_: DialogState)
suspend fun confirmRemoveDownload(info: GalleryInfo) {
val text = appCtx.getString(R.string.download_remove_dialog_message, EhUtils.getSuitableTitle(info))
val checked = confirmRemoveDownload(text)
withIOContext {
@@ -383,7 +382,8 @@ suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo) {
}
}
suspend fun DialogState.confirmRemoveDownloadRange(list: Collection<DownloadInfo>) {
context(_: DialogState)
suspend fun confirmRemoveDownloadRange(list: Collection<DownloadInfo>) {
val text = appCtx.getString(R.string.download_remove_dialog_message_2, list.size)
val checked = confirmRemoveDownload(text)
withIOContext {
@@ -402,7 +402,8 @@ suspend fun DialogState.confirmRemoveDownloadRange(list: Collection<DownloadInfo
}
}
suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) {
context(_: DialogState)
suspend fun showMoveDownloadLabel(info: GalleryInfo) {
val defaultLabel = appCtx.getString(R.string.default_download_label_name)
val labels = buildList {
add(defaultLabel)
@@ -416,7 +417,8 @@ suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) {
DownloadManager.changeLabel(listOf(downloadInfo), label)
}
suspend fun DialogState.showMoveDownloadLabelList(list: Collection<DownloadInfo>): String? {
context(_: DialogState)
suspend fun showMoveDownloadLabelList(list: Collection<DownloadInfo>): String? {
val defaultLabel = appCtx.getString(R.string.default_download_label_name)
val labels = buildList {
add(defaultLabel)
@@ -430,7 +432,8 @@ suspend fun DialogState.showMoveDownloadLabelList(list: Collection<DownloadInfo>
return label
}
suspend fun DialogState.awaitSelectDate(): String? {
context(_: DialogState)
suspend fun awaitSelectDate(): String? {
val initial = LocalDate(2007, 3, 21)
val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY)
val initialMillis = initial.toEpochMillis()
@@ -447,8 +450,8 @@ suspend fun DialogState.awaitSelectDate(): String? {
return date
}
context(Context)
suspend fun DialogState.showRestartDialog() {
context(_: Context, _: DialogState)
suspend fun showRestartDialog() {
awaitConfirmationOrCancel {
Text(stringResource(R.string.settings_restart))
}
@@ -10,7 +10,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.hippo.ehviewer.ui.screen.implicit
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LocalGlobalDialogState
import com.hippo.ehviewer.ui.tools.NoopTransitionsVisibilityScope
@@ -20,15 +19,16 @@ import com.hippo.ehviewer.util.findActivity
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.CoroutineScope
context(scope: AnimatedVisibilityScope)
@Composable
inline fun <R> AnimatedVisibilityScope.Screen(
inline fun <R> Screen(
navigator: DestinationsNavigator,
block: @Composable context(MainActivity, SnackbarHostState, DialogState, SharedTransitionScope, TransitionsVisibilityScope, DestinationsNavigator, CoroutineScope)
() -> R,
) = Box(modifier = Modifier.fillMaxSize()) {
val dialogState = with(LocalGlobalDialogState.current) { rememberLocal() }
with(NoopTransitionsVisibilityScope) {
togetherWith(implicit<AnimatedVisibilityScope>()) {
togetherWith(scope) {
block(
with(LocalContext.current) { remember { findActivity() } },
LocalSnackBarHostState.current,
@@ -62,7 +62,7 @@ private fun GalleryDetail.content() = arrayOf(
R.string.favorite_name to favoriteName,
)
context(Context, DestinationsNavigator)
context(_: Context, _: DestinationsNavigator)
@Composable
fun GalleryInfoBottomSheet(detail: GalleryDetail) {
Column(modifier = Modifier.fillMaxWidth()) {
@@ -25,13 +25,11 @@ import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
@@ -43,7 +41,6 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
@@ -55,16 +52,13 @@ import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Whatshot
import androidx.compose.material3.DrawerDefaults
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerState2
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.ModalSideDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
@@ -138,6 +132,8 @@ import com.hippo.ehviewer.ui.settings.showNewVersion
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitInputText
import com.hippo.ehviewer.updater.AppUpdater
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.addTextToClipboard
@@ -181,15 +177,6 @@ val StartDestination
get() = navItems[Settings.launchPage].first
class MainActivity : EhActivity() {
private val sideSheet = mutableStateListOf<@Composable ColumnScope.(DrawerState2) -> Unit>()
@Composable
fun ProvideSideSheetContent(content: @Composable ColumnScope.(DrawerState2) -> Unit) {
DisposableEffect(content) {
sideSheet.add(0, content)
onDispose { sideSheet.remove(content) }
}
}
private var shareUrl: String? = null
@@ -256,9 +243,7 @@ class MainActivity : EhActivity() {
if (hasNetwork) {
runSuspendCatching {
withIOContext {
AppUpdater.checkForUpdate()?.let {
showNewVersion(this@MainActivity, it)
}
AppUpdater.checkForUpdate()?.let { showNewVersion(it) }
}
}.onFailure {
snackbarState.showSnackbar(getString(R.string.update_failed, it.displayString()))
@@ -276,13 +261,11 @@ class MainActivity : EhActivity() {
Intent.ACTION_VIEW -> {
val uri = intent.data ?: return@collect
when (uri.scheme) {
SCHEME_CONTENT, SCHEME_FILE -> {
navigator.navToReader(uri)
}
SCHEME_CONTENT, SCHEME_FILE -> with(navigator) { navToReader(uri) }
else -> {
val url = uri.toString()
if (!navigator.navWithUrl(url)) {
if (!with(navigator) { navWithUrl(url) }) {
val new = awaitInputText(initial = url, title = cannotParse)
addTextToClipboard(new)
}
@@ -293,7 +276,7 @@ class MainActivity : EhActivity() {
val type = intent.type
if ("text/plain" == type) {
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
if (keyword != null && !navigator.navWithUrl(keyword)) {
if (keyword != null && !with(navigator) { navWithUrl(keyword) }) {
navigator.navigate(ListUrlBuilder(mKeyword = keyword).asDst())
}
} else if (type != null && type.startsWith("image/")) {
@@ -440,25 +423,13 @@ class MainActivity : EhActivity() {
drawerState = navDrawerState,
gesturesEnabled = drawerEnabled && sideSheetState.isClosed || navDrawerState.isOpen,
) {
val sheet = sideSheet.firstOrNull()
val radius by remember {
snapshotFlow {
val step = calculateFraction(minOffset, 0f, navDrawerState.currentOffset)
with(density) { lerp(0, 10, step).dp.toPx() }
}
}.collectAsState(0f)
ModalSideDrawer(
drawerContent = {
if (sheet != null) {
ModalDrawerSheet(
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 112).dp),
drawerShape = ShapeDefaults.Large.copy(topEnd = CornerSize(0), bottomEnd = CornerSize(0)),
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.End),
) {
sheet(sideSheetState)
}
}
},
MutableSideSheet(
modifier = Modifier.graphicsLayer {
if (radius != 0f) {
renderEffect = BlurEffect(radius, radius, TileMode.Clamp)
@@ -466,8 +437,7 @@ class MainActivity : EhActivity() {
clip = true
}
},
drawerState = sideSheetState,
gesturesEnabled = sheet != null && drawerEnabled,
enabled = drawerEnabled,
) {
SharedTransitionLayout {
CompositionLocalProvider(LocalSharedTransitionScope provides this) {
@@ -553,8 +523,6 @@ class MainActivity : EhActivity() {
}
}
fun showTip(@StringRes id: Int, useToast: Boolean = false) = showTip(getString(id), useToast)
override fun onProvideAssistContent(outContent: AssistContent?) {
super.onProvideAssistContent(outContent)
shareUrl?.let { outContent?.webUri = it.toUri() }
@@ -562,7 +530,7 @@ class MainActivity : EhActivity() {
}
val LocalNavDrawerState = compositionLocalOf<DrawerState> { error("CompositionLocal LocalNavDrawerState not present!") }
val LocalSideSheetState = compositionLocalOf<DrawerState2> { error("CompositionLocal LocalSideSheetState not present!") }
val LocalDrawerHandle = compositionLocalOf<SnapshotStateList<Long>> { error("CompositionLocal LocalDrawerHandle not present!") }
val LocalSnackBarHostState = compositionLocalOf<SnackbarHostState> { error("CompositionLocal LocalSnackBarHostState not present!") }
val LocalSnackBarFabPadding = compositionLocalOf<State<Dp>> { error("CompositionLocal LocalSnackBarFabPadding not present!") }
@@ -0,0 +1,67 @@
package com.hippo.ehviewer.ui
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material3.DrawerState2
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalSideDrawer
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.rememberDrawerState2
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.dp
private typealias Sheet = @Composable ColumnScope.(DrawerState2) -> Unit
val LocalSideSheetContainer = staticCompositionLocalOf<SnapshotStateList<Sheet>> { error("") }
val LocalSideSheetState = staticCompositionLocalOf<DrawerState2> { error("") }
@Composable
fun ProvideSideSheetContent(content: Sheet) {
val container = LocalSideSheetContainer.current
DisposableEffect(content) {
container.add(0, content)
onDispose { container.remove(content) }
}
}
@Composable
fun MutableSideSheet(modifier: Modifier, enabled: Boolean, content: @Composable () -> Unit) {
val sheet = remember { mutableStateListOf<Sheet>() }
val f = sheet.firstOrNull()
val state = rememberDrawerState2(DrawerValue.Closed)
val width = with(LocalDensity.current) { LocalWindowInfo.current.containerSize.width.toDp() }
CompositionLocalProvider(LocalSideSheetState provides state, LocalSideSheetContainer provides sheet) {
ModalSideDrawer(
drawerContent = {
if (f != null) {
ModalDrawerSheet(
modifier = Modifier.widthIn(max = width - 112.dp),
drawerShape = ShapeDefaults.Large.copy(topEnd = CornerSize(0), bottomEnd = CornerSize(0)),
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.End),
) {
f(state)
}
}
},
modifier = modifier,
drawerState = state,
gesturesEnabled = f != null && enabled,
content = content,
)
}
}
@@ -32,9 +32,10 @@ import androidx.lifecycle.LifecycleOwner
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
fun Context.isAuthenticationSupported(): Boolean {
context(ctx: Context)
fun isAuthenticationSupported(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
return BiometricManager.from(this).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
return BiometricManager.from(ctx).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
class SecurityActivity : AppCompatActivity() {
@@ -27,16 +27,18 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
private val intent = CustomTabsIntent.Builder().apply { setShowTitle(true) }.build()
fun Context.openBrowser(url: String) {
context(ctx: Context)
fun openBrowser(url: String) {
if (url.isEmpty()) return
try {
intent.launchUrl(this, url.toUri())
intent.launchUrl(ctx, url.toUri())
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, R.string.no_browser_installed, Toast.LENGTH_LONG).show()
Toast.makeText(ctx, R.string.no_browser_installed, Toast.LENGTH_LONG).show()
}
}
fun DestinationsNavigator.jumpToReaderByPage(url: String, detail: GalleryDetail): Boolean {
context(_: DestinationsNavigator)
fun jumpToReaderByPage(url: String, detail: GalleryDetail): Boolean {
GalleryPageUrlParser.parse(url)?.let {
if (it.gid == detail.gid) {
navToReader(detail.galleryInfo, it.page)
@@ -65,6 +65,7 @@ import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.destinations.WebViewSignInScreenDestination
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.isExpanded
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.displayString
@@ -72,9 +73,10 @@ import com.jamal.composeprefs3.ui.ifTrueThen
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.withUIContext
import kotlinx.coroutines.Job
import moe.tarsin.launchIO
import moe.tarsin.navigate
@Destination<RootGraph>(start = true)
@Composable
@@ -212,7 +214,7 @@ fun AnimatedVisibilityScope.SignInScreen(navigator: DestinationsNavigator) = Scr
}
Row(modifier = Modifier.padding(horizontal = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
onClick = { navigate(WebViewSignInScreenDestination) },
modifier = Modifier.weight(1f),
) {
Text(
@@ -294,7 +296,7 @@ fun AnimatedVisibilityScope.SignInScreen(navigator: DestinationsNavigator) = Scr
Spacer(modifier = Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.Center) {
TextButton(
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
onClick = { navigate(WebViewSignInScreenDestination) },
modifier = Modifier.padding(horizontal = 4.dp).width(128.dp),
) {
Text(
@@ -49,9 +49,9 @@ import com.hippo.ehviewer.client.parser.HomeParser
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.login.refreshAccountInfo
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
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
@@ -62,8 +62,9 @@ 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
import moe.tarsin.launch
import moe.tarsin.launchIO
private val limitScope = CoroutineScope(Dispatchers.IO)
private val refreshEvent = MutableSharedFlow<Unit>()
@@ -81,7 +82,7 @@ private val limitFlow: StateFlow<Result> = refreshEvent.conflate()
.let { src -> merge(src, invalidateEvent.map { none() }) }
.stateIn(limitScope, SharingStarted.Eagerly, none())
context(CoroutineScope, DialogState, SnackbarHostState, DestinationsNavigator)
context(_: CoroutineScope, _: DialogState, _: SnackbarHostState, _: DestinationsNavigator)
@Composable
fun AvatarIcon() {
val hasSignedIn by Settings.hasSignedIn.collectAsState()
@@ -96,7 +96,7 @@ private val constraintSet = ConstraintSet {
}
}
context(SharedTransitionScope, TransitionsVisibilityScope)
context(_: SharedTransitionScope, _: TransitionsVisibilityScope)
@Composable
fun DownloadCard(
onClick: () -> Unit,
@@ -18,7 +18,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil3.compose.AsyncImagePainter
import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.tools.SETNodeGenerator
@@ -29,12 +28,11 @@ import com.hippo.ehviewer.ui.tools.thenIf
@Composable
@NonRestartableComposable
fun requestOf(model: GalleryInfo): ImageRequest {
val context = LocalContext.current
return remember(model) { context.imageRequest(model) }
fun requestOf(model: GalleryInfo) = with(LocalContext.current) {
remember(model) { imageRequest(model) }
}
context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
context(_: SharedTransitionScope, _: TransitionsVisibilityScope, _: SETNodeGenerator)
@Composable
fun EhAsyncCropThumb(
key: GalleryInfo,
@@ -58,7 +56,7 @@ fun EhAsyncCropThumb(
)
}
context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
context(_: SharedTransitionScope, _: TransitionsVisibilityScope, _: SETNodeGenerator)
@Composable
fun EhThumbCard(
key: GalleryInfo,
@@ -42,8 +42,8 @@ import kotlin.time.Duration.Companion.microseconds
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.onEachLatest
import moe.tarsin.launch
@Stable
enum class FabLayoutValue { Hidden, Primary, Expand }
@@ -98,7 +98,7 @@ fun interface FabBuilder {
fun onClick(icon: ImageVector, that: suspend () -> Unit) = onClick(icon, true, that)
}
context(CoroutineScope)
context(_: CoroutineScope)
@Composable
fun FabLayout(
hidden: Boolean,
@@ -98,7 +98,7 @@ fun GalleryDetailHeaderInfoCard(
}
}
context(SharedTransitionScope, TransitionsVisibilityScope)
context(_: SharedTransitionScope, _: TransitionsVisibilityScope)
@Composable
fun GalleryDetailHeaderCard(
info: GalleryInfo,
@@ -92,7 +92,7 @@ private val constraintSet = ConstraintSet {
}
}
context(SharedTransitionScope, TransitionsVisibilityScope)
context(_: SharedTransitionScope, _: TransitionsVisibilityScope)
@Composable
fun GalleryInfoListItem(
onClick: () -> Unit,
@@ -204,7 +204,7 @@ fun GalleryInfoListItem(
}
}
context(SharedTransitionScope, TransitionsVisibilityScope)
context(_: SharedTransitionScope, _: TransitionsVisibilityScope)
@Composable
fun GalleryInfoGridItem(
onClick: () -> Unit,
@@ -67,7 +67,7 @@ import com.hippo.ehviewer.util.displayString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import moe.tarsin.launch
@Stable
operator fun PaddingValues.plus(r: PaddingValues) = object : PaddingValues {
@@ -78,7 +78,7 @@ operator fun PaddingValues.plus(r: PaddingValues) = object : PaddingValues {
override fun calculateTopPadding() = l.calculateTopPadding() + r.calculateTopPadding()
}
context(CoroutineScope, Context)
context(_: CoroutineScope, ctx: Context)
@Composable
fun GalleryList(
modifier: Modifier = Modifier,
@@ -143,7 +143,7 @@ fun GalleryList(
val info = data[index]
if (info != null) {
detailItemContent(info)
PrefetchAround(data, index, 5, ::imageRequest)
PrefetchAround(data, index, 5) { imageRequest(it) }
}
}
if (showLoadStateIndicator) {
@@ -173,7 +173,7 @@ fun GalleryList(
val info = data[index]
if (info != null) {
thumbItemContent(info)
PrefetchAround(data, index, 10, ::imageRequest)
PrefetchAround(data, index, 10) { imageRequest(it) }
}
}
if (showLoadStateIndicator) {
@@ -23,7 +23,6 @@ import androidx.compose.ui.unit.IntSize
import coil3.BitmapImage
import coil3.compose.AsyncImagePainter.State
import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.V2GalleryPreview
import com.hippo.ehviewer.ktbuilder.imageRequest
@@ -32,9 +31,8 @@ import com.hippo.ehviewer.ui.tools.shouldCrop
@Composable
@NonRestartableComposable
fun requestOf(model: GalleryPreview): ImageRequest {
val context = LocalContext.current
return remember(model) { context.imageRequest(model) }
fun requestOf(model: GalleryPreview) = with(LocalContext.current) {
remember(model) { imageRequest(model) }
}
@Composable
@@ -1,5 +1,6 @@
package com.hippo.ehviewer.ui.main
import android.content.Context
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -17,7 +18,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -30,6 +30,7 @@ import com.hippo.ehviewer.client.data.VoteStatus
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.tools.includeFontPadding
context(_: Context)
@Composable
fun GalleryTags(
tagGroups: List<GalleryTagGroup>,
@@ -37,8 +38,7 @@ fun GalleryTags(
onTagLongClick: (String, String, VoteStatus) -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(context) && EhTagDatabase.initialized
val canTranslate = Settings.showTagTranslations && EhTagDatabase.translatable && EhTagDatabase.initialized
val ehTags = EhTagDatabase.takeIf { canTranslate }
fun TagNamespace.translate() = ehTags?.getTranslation(tag = value) ?: value
fun String.translate(ns: TagNamespace) = ehTags?.getTranslation(prefix = ns.prefix, tag = this) ?: this
@@ -1,5 +1,6 @@
package com.hippo.ehviewer.ui.main
import android.content.Context
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -24,11 +25,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
@@ -42,9 +41,11 @@ import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANG_TAGS
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.DropdownFilterChip
import com.hippo.ehviewer.ui.tools.awaitResult
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.toIntOrDefault
import kotlinx.coroutines.launch
import kotlinx.coroutines.CoroutineScope
import moe.tarsin.launch
private val categoryTable = arrayOf(
EhUtils.DOUJINSHI to R.string.doujinshi,
@@ -59,7 +60,7 @@ private val categoryTable = arrayOf(
EhUtils.MISC to R.string.misc,
)
context(DialogState)
context(_: DialogState, _: CoroutineScope, _: Context)
@Composable
fun SearchFilter(
modifier: Modifier = Modifier,
@@ -70,8 +71,6 @@ fun SearchFilter(
advancedOption: AdvancedSearchOption,
onAdvancedOptionChange: (AdvancedSearchOption) -> Unit,
) = Column(modifier) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val animateItems by Settings.animateItems.collectAsState()
fun isCategoryChecked(bit: Int) = category and bit != 0
val categories = remember(category) { categoryTable.sortedBy { !isCategoryChecked(it.first) } }
@@ -98,7 +97,7 @@ fun SearchFilter(
val any = stringResource(id = R.string.any)
val languageStr = stringResource(id = R.string.key_language)
val languages = remember {
val translatable = EhTagDatabase.initialized && EhTagDatabase.isTranslatable(context)
val translatable = EhTagDatabase.initialized && EhTagDatabase.translatable
List(S_LANG_TAGS.size + 1) { i ->
if (i == 0) {
any
@@ -149,7 +148,7 @@ fun SearchFilter(
FilterChip(
selected = advancedOption.fromPage != 0 || advancedOption.toPage != 0,
onClick = {
scope.launch {
launch {
val (from, to) = awaitResult(
initial = advancedOption.fromPage to advancedOption.toPage,
title = R.string.key_pages,
@@ -31,24 +31,26 @@ import eu.kanade.tachiyomi.util.system.logcat
import java.io.File
import kotlinx.datetime.Clock
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.snackbar
import moe.tarsin.string
import splitties.systemservices.clipboardManager
context(PageLoader)
private fun Context.provideImage(index: Int): Uri? {
context(loader: PageLoader, ctx: Context)
private fun provideImage(index: Int): Uri? {
val dir = AppConfig.externalTempDir ?: return null
val name = getImageFilename(index) ?: return null
val file = (dir / name).takeIf { save(index, it) } ?: return null
return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", file.toFile())
val name = loader.getImageFilename(index) ?: return null
val file = (dir / name).takeIf { loader.save(index, it) } ?: return null
return FileProvider.getUriForFile(ctx, "$APPLICATION_ID.fileprovider", file.toFile())
}
context(SnackbarHostState, Context, PageLoader)
context(_: SnackbarHostState, ctx: Context, _: PageLoader)
suspend fun shareImage(page: Page, info: GalleryInfo? = null) {
val error = getString(R.string.error_cant_save_image)
val share = getString(R.string.share_image)
val noActivity = getString(R.string.error_cant_find_activity)
val error = string(R.string.error_cant_save_image)
val share = string(R.string.share_image)
val noActivity = string(R.string.error_cant_find_activity)
val uri = provideImage(page.index)
if (uri == null) {
showSnackbar(error)
snackbar(error)
return
}
val intent = Intent(Intent.ACTION_SEND).apply {
@@ -60,36 +62,36 @@ suspend fun shareImage(page: Page, info: GalleryInfo? = null) {
setDataAndType(uri, mimeType)
}
try {
startActivity(Intent.createChooser(intent, share))
ctx.startActivity(Intent.createChooser(intent, share))
} catch (_: ActivityNotFoundException) {
showSnackbar(noActivity)
snackbar(noActivity)
}
}
context(SnackbarHostState, Context, PageLoader)
context(_: SnackbarHostState, ctx: Context, _: PageLoader)
suspend fun copy(page: Page) {
val error = getString(R.string.error_cant_save_image)
val copied = getString(R.string.copied_to_clipboard)
val error = string(R.string.error_cant_save_image)
val copied = string(R.string.copied_to_clipboard)
val uri = provideImage(page.index)
if (uri == null) {
showSnackbar(error)
snackbar(error)
return
}
val clipData = ClipData.newUri(contentResolver, "ehviewer", uri)
val clipData = ClipData.newUri(ctx.contentResolver, "ehviewer", uri)
clipboardManager.setPrimaryClip(clipData)
if (!isAtLeastT) {
showSnackbar(copied)
snackbar(copied)
}
}
context(SnackbarHostState, Context, PageLoader)
context(_: SnackbarHostState, ctx: Context, loader: PageLoader)
suspend fun save(page: Page) {
val granted = isAtLeastQ || requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
val cannotSave = getString(R.string.error_cant_save_image)
val cannotSave = string(R.string.error_cant_save_image)
if (granted) {
val filename = getImageFilename(page.index)
val filename = loader.getImageFilename(page.index)
if (filename == null) {
showSnackbar(cannotSave)
snackbar(cannotSave)
return
}
val extension = FileUtils.getExtensionFromFilename(filename)
@@ -108,39 +110,39 @@ suspend fun save(page: Page) {
val path = File(dir, AppConfig.APP_DIRNAME)
realPath = path.toString()
if (!FileUtils.ensureDirectory(path)) {
showSnackbar(cannotSave)
snackbar(cannotSave)
return
}
values.put(MediaStore.MediaColumns.DATA, realPath + File.separator + filename)
}
val imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
val imageUri = ctx.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (imageUri != null) {
if (!save(page.index, imageUri.toOkioPath())) {
if (!loader.save(page.index, imageUri.toOkioPath())) {
try {
contentResolver.delete(imageUri, null, null)
ctx.contentResolver.delete(imageUri, null, null)
} catch (e: Exception) {
e.logcat(e)
}
showSnackbar(cannotSave)
snackbar(cannotSave)
} else if (isAtLeastQ) {
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
contentResolver.update(imageUri, contentValues, null, null)
ctx.contentResolver.update(imageUri, contentValues, null, null)
}
showSnackbar(getString(R.string.image_saved, realPath + File.separator + filename))
snackbar(string(R.string.image_saved, realPath + File.separator + filename))
} else {
showSnackbar(cannotSave)
snackbar(cannotSave)
}
} else {
showSnackbar(getString(R.string.permission_denied))
snackbar(string(R.string.permission_denied))
}
}
context(SnackbarHostState, Context, PageLoader)
context(_: SnackbarHostState, _: Context, loader: PageLoader)
suspend fun saveTo(page: Page) {
val filename = getImageFilename(page.index)
val filename = loader.getImageFilename(page.index)
if (filename == null) {
showSnackbar(getString(R.string.error_cant_save_image))
snackbar(string(R.string.error_cant_save_image))
return
}
val extension = FileUtils.getExtensionFromFilename(filename)
@@ -148,11 +150,11 @@ suspend fun saveTo(page: Page) {
page.runSuspendCatching {
val uri = awaitActivityResult(ActivityResultContracts.CreateDocument(mimeType), filename)
if (uri != null) {
save(index, uri.toOkioPath())
showSnackbar(getString(R.string.image_saved, uri.displayPath))
loader.save(index, uri.toOkioPath())
snackbar(string(R.string.image_saved, uri.displayPath))
}
}.onFailure {
it.logcat(it)
showSnackbar(getString(R.string.error_cant_find_activity))
snackbar(string(R.string.error_cant_find_activity))
}
}
@@ -78,6 +78,8 @@ import com.hippo.ehviewer.ui.theme.EhTheme
import com.hippo.ehviewer.ui.tools.Await
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.asyncInVM
import com.hippo.ehviewer.ui.tools.awaitInputText
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
@@ -97,10 +99,11 @@ import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.parcelize.Parcelize
import moe.tarsin.kt.unreachable
import moe.tarsin.launch
import moe.tarsin.string
sealed interface ReaderScreenArgs : Parcelable {
@Parcelize
@@ -175,14 +178,14 @@ fun AnimatedVisibilityScope.ReaderScreen(args: ReaderScreenArgs, navigator: Dest
}
}
context(MainActivity, SnackbarHostState, DialogState, CoroutineScope)
context(activity: MainActivity, _: SnackbarHostState, _: DialogState, _: CoroutineScope)
@Composable
fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
LaunchedEffect(Unit) {
val orientation = requestedOrientation
val orientation = activity.requestedOrientation
Settings.orientationMode.valueFlow()
.onCompletion { requestedOrientation = orientation }
.collect { setOrientation(it) }
.onCompletion { activity.requestedOrientation = orientation }
.collect { activity.setOrientation(it) }
}
LaunchedEffect(pageLoader) {
with(Settings) {
@@ -215,16 +218,8 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader, info: BaseGalle
Modifier.keyEventHandler(
enabled = { !appbarVisible },
reverse = { reverseControls },
movePrevious = {
launch {
if (isWebtoon) lazyListState.scrollUp() else pagerState.moveToPrevious()
}
},
moveNext = {
launch {
if (isWebtoon) lazyListState.scrollDown() else pagerState.moveToNext()
}
},
movePrevious = { launch { if (isWebtoon) lazyListState.scrollUp() else pagerState.moveToPrevious() } },
moveNext = { launch { if (isWebtoon) lazyListState.scrollDown() else pagerState.moveToNext() } },
).focusRequester(focusRequester).focusable().thenIf(keepScreenOn) { keepScreenOn() },
) {
LaunchedEffect(Unit) {
@@ -323,8 +318,8 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader, info: BaseGalle
if (brightness) {
LaunchedEffect(Unit) {
Settings.customBrightnessValue.valueFlow().sample(100)
.onCompletion { setCustomBrightnessValue(0) }
.collect { setCustomBrightnessValue(it) }
.onCompletion { activity.setCustomBrightnessValue(0) }
.collect { activity.setCustomBrightnessValue(it) }
}
}
val showPageNumber by Settings.showPageNumber.collectAsState()
@@ -374,7 +369,7 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader, info: BaseGalle
}
}
context(Context, DialogState, DestinationsNavigator)
context(ctx: Context, state: DialogState, nav: DestinationsNavigator)
suspend inline fun <T> usePageLoader(args: ReaderScreenArgs, crossinline block: suspend (PageLoader) -> T) = when (args) {
is ReaderScreenArgs.Gallery -> {
val info = args.info
@@ -390,12 +385,12 @@ suspend inline fun <T> usePageLoader(args: ReaderScreenArgs, crossinline block:
args.uri.toOkioPath(),
passwdProvider = { invalidator ->
awaitInputText(
title = getString(R.string.archive_need_passwd),
hint = getString(R.string.archive_passwd),
onUserDismiss = { popBackStack() },
title = string(R.string.archive_need_passwd),
hint = string(R.string.archive_passwd),
onUserDismiss = { nav.popBackStack() },
) { text ->
ensure(text.isNotBlank()) { getString(R.string.passwd_cannot_be_empty) }
ensure(invalidator(text)) { getString(R.string.passwd_wrong) }
ensure(text.isNotBlank()) { string(R.string.passwd_cannot_be_empty) }
ensure(invalidator(text)) { string(R.string.passwd_wrong) }
}
},
block = block,
@@ -1,5 +0,0 @@
package com.hippo.ehviewer.ui.screen
// https://github.com/Kotlin/KEEP/blob/context-parameters/proposals/context-parameters.md#standard-library-support
context(T)
inline fun <reified T> implicit(): T = this@T
@@ -98,6 +98,7 @@ import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.Download
import com.hippo.ehviewer.ui.DrawerHandle
import com.hippo.ehviewer.ui.LocalSideSheetState
import com.hippo.ehviewer.ui.ProvideSideSheetContent
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.confirmRemoveDownloadRange
import com.hippo.ehviewer.ui.main.DownloadCard
@@ -113,6 +114,11 @@ import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid
import com.hippo.ehviewer.ui.tools.HapticFeedbackType
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.awaitSelectAction
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.thenIf
@@ -122,13 +128,14 @@ import com.jamal.composeprefs3.ui.ifTrueThen
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 kotlin.math.roundToInt
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.onEachLatest
import moe.tarsin.launch
import moe.tarsin.launchIO
import moe.tarsin.navigate
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@@ -151,7 +158,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
DrawerHandle(!selectMode && !searchBarExpanded)
val density = LocalDensity.current
val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(implicit<Context>()) && EhTagDatabase.initialized
val canTranslate = Settings.showTagTranslations && EhTagDatabase.translatable && EhTagDatabase.initialized
val ehTags = EhTagDatabase.takeIf { canTranslate }
fun getTranslation(tag: String) = ehTags?.run {
getTranslation(TagNamespace.Artist.prefix, tag) ?: getTranslation(TagNamespace.Cosplayer.prefix, tag)
@@ -291,7 +298,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
},
)
val dialogState by rememberUpdatedState(implicit<DialogState>())
val dialogState by rememberUpdatedState(contextOf<DialogState>())
val labelsListState = rememberLazyListState()
val editEnable = DownloadsFilterMode.CUSTOM == filterMode
val hapticFeedback = rememberHapticFeedback()
@@ -343,8 +350,8 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
LaunchedEffect(dismissState) {
snapshotFlow { dismissState.currentValue }.collect {
if (it == SwipeToDismissBoxValue.EndToStart) {
runCatching {
dialogState.awaitConfirmationOrCancel(confirmText = R.string.delete) {
dialogState.runCatching {
awaitConfirmationOrCancel(confirmText = R.string.delete) {
Text(text = stringResource(R.string.delete_label, item))
}
}.onSuccess {
@@ -476,9 +483,9 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
text = { Text(text = stringResource(id = R.string.download_start_all)) },
onClick = {
expanded = false
val intent = Intent(implicit<Activity>(), DownloadService::class.java)
val intent = Intent(contextOf<Activity>(), DownloadService::class.java)
intent.action = DownloadService.ACTION_START_ALL
ContextCompat.startForegroundService(implicit<Activity>(), intent)
ContextCompat.startForegroundService(contextOf<Activity>(), intent)
},
)
DropdownMenuItem(
@@ -510,10 +517,10 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
onClick = {
expanded = false
val gidList = list.filter { it.state != DownloadInfo.STATE_FINISH }.asReversed().mapToLongArray(DownloadInfo::gid)
val intent = Intent(implicit<Activity>(), DownloadService::class.java)
val intent = Intent(contextOf<Activity>(), DownloadService::class.java)
intent.action = DownloadService.ACTION_START_RANGE
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
ContextCompat.startForegroundService(implicit<Activity>(), intent)
ContextCompat.startForegroundService(contextOf<Activity>(), intent)
},
)
}
@@ -522,7 +529,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
val height by collectListThumbSizeAsState()
val realPadding = contentPadding + PaddingValues(dimensionResource(id = R.dimen.gallery_list_margin_h), dimensionResource(id = R.dimen.gallery_list_margin_v))
val searchBarConnection = remember {
val slop = ViewConfiguration.get(implicit<Context>()).scaledTouchSlop
val slop = ViewConfiguration.get(contextOf<Context>()).scaledTouchSlop
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
@@ -595,10 +602,10 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
checkedInfoMap[info.gid] = info
},
onStart = {
val intent = Intent(implicit<Activity>(), DownloadService::class.java)
val intent = Intent(contextOf<Activity>(), DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo)
ContextCompat.startForegroundService(implicit<Activity>(), intent)
ContextCompat.startForegroundService(contextOf<Activity>(), intent)
},
onStop = { launchIO { DownloadManager.stopDownload(info.gid) } },
info = info,
@@ -660,7 +667,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
}
onClick(Icons.AutoMirrored.Default.Sort) {
val oldMode = SortMode.from(sortMode)
val sortModes = resources.getStringArray(R.array.download_sort_modes).toList()
val sortModes = contextOf<Context>().resources.getStringArray(R.array.download_sort_modes).toList()
val (selected, checked) = awaitSelectItemWithCheckBox(
sortModes,
R.string.sort_by,
@@ -674,7 +681,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
invalidateKey = !invalidateKey
}
onClick(Icons.Default.FilterList) {
val downloadStates = resources.getStringArray(R.array.download_state).toList()
val downloadStates = contextOf<Context>().resources.getStringArray(R.array.download_state).toList()
val state = awaitSingleChoice(
downloadStates,
filterState.state + 1,
@@ -689,10 +696,10 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
}
onClick(Icons.Default.PlayArrow) {
val gidList = checkedInfoMap.takeAndClear().mapToLongArray(DownloadInfo::gid)
val intent = Intent(implicit<Activity>(), DownloadService::class.java)
val intent = Intent(contextOf<Activity>(), DownloadService::class.java)
intent.action = DownloadService.ACTION_START_RANGE
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
ContextCompat.startForegroundService(implicit<Context>(), intent)
ContextCompat.startForegroundService(contextOf<Context>(), intent)
}
onClick(Icons.Default.Pause) {
val gidList = checkedInfoMap.takeAndClear().mapToLongArray(DownloadInfo::gid)
@@ -70,6 +70,7 @@ import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.filled.GoTo
import com.hippo.ehviewer.ui.DrawerHandle
import com.hippo.ehviewer.ui.LocalSideSheetState
import com.hippo.ehviewer.ui.ProvideSideSheetContent
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.awaitSelectDate
import com.hippo.ehviewer.ui.main.AvatarIcon
@@ -80,6 +81,8 @@ import com.hippo.ehviewer.ui.main.GalleryInfoListItem
import com.hippo.ehviewer.ui.main.GalleryList
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
@@ -93,10 +96,11 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlin.math.roundToInt
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
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
@@ -259,7 +263,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
val height by collectListThumbSizeAsState()
val showPages by Settings.showGalleryPages.collectAsState()
val searchBarConnection = remember {
val slop = ViewConfiguration.get(implicit<Context>()).scaledTouchSlop
val slop = ViewConfiguration.get(contextOf<Context>()).scaledTouchSlop
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
@@ -387,7 +391,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
onClick(Icons.Default.Download) {
val info = checkedInfoMap.takeAndClear()
runSwallowingWithUI {
startDownload(implicit<Context>(), false, *info.toTypedArray())
startDownload(false, *info.toTypedArray())
}
}
onClick(Icons.Default.Delete) {
@@ -98,8 +98,11 @@ import com.hippo.ehviewer.ui.jumpToReaderByPage
import com.hippo.ehviewer.ui.main.GalleryCommentCard
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitSelectAction
import com.hippo.ehviewer.ui.tools.normalizeSpan
import com.hippo.ehviewer.ui.tools.rememberBBCodeTextToolbar
import com.hippo.ehviewer.ui.tools.showNoButton
import com.hippo.ehviewer.ui.tools.snackBarPadding
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.ui.tools.toBBCode
@@ -110,13 +113,15 @@ import com.hippo.ehviewer.util.displayString
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.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
import kotlin.collections.forEach
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.launch
import moe.tarsin.launchIO
import moe.tarsin.navigate
import moe.tarsin.snackbar
private val URL_PATTERN = Regex("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+)+(:\\d{1,5})?(/[a-zA-Z0-9-_~:#@!&',;=%/*.?+$\\[\\]()]+)?/?")
@@ -222,10 +227,10 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
userComment = TextFieldValue()
commentId = -1L
comments = it
showSnackbar(msg)
snackbar(msg)
}.onFailure {
val text = if (commentId != -1L) editCommentFail else commentFail
showSnackbar(text + "\n" + it.displayString())
snackbar(text + "\n" + it.displayString())
}
}
@@ -235,7 +240,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
awaitConfirmationOrCancel { Text(text = stringResource(R.string.filter_the_commenter, commenter)) }
Filter(FilterMode.COMMENTER, commenter).remember()
comments = comments.copy(comments = comments.comments.filterNot { it.user == commenter })
showSnackbar(filterAdded)
snackbar(filterAdded)
}
suspend fun showCommentVoteStatus(comment: GalleryComment) {
@@ -357,7 +362,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
refreshComment(true)
}
}.onSuccess { result ->
showSnackbar(
snackbar(
if (isUp) {
if (0 != result.vote) voteUpSucceed else cancelVoteUpSucceed
} else {
@@ -365,7 +370,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
},
)
}.onFailure {
showSnackbar(voteFailed)
snackbar(voteFailed)
}
}
@@ -124,20 +124,24 @@ import com.hippo.ehviewer.ui.tools.GalleryDetailRating
import com.hippo.ehviewer.ui.tools.GalleryRatingBar
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitResult
import com.hippo.ehviewer.ui.tools.awaitSelectAction
import com.hippo.ehviewer.ui.tools.awaitSelectItem
import com.hippo.ehviewer.ui.tools.dialog
import com.hippo.ehviewer.ui.tools.foldToLoadResult
import com.hippo.ehviewer.ui.tools.getClippedRefreshKey
import com.hippo.ehviewer.ui.tools.getLimit
import com.hippo.ehviewer.ui.tools.getOffset
import com.hippo.ehviewer.ui.tools.isExpanded
import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.ui.tools.showNoButton
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.bgWork
import com.hippo.ehviewer.util.displayString
import com.hippo.ehviewer.util.flattenForEach
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
@@ -148,12 +152,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import moe.tarsin.async
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.coroutines.runSwallowingWithUI
import moe.tarsin.launch
import moe.tarsin.launchIO
import moe.tarsin.launchUI
import moe.tarsin.navigate
import moe.tarsin.snackbar
import moe.tarsin.string
context(CoroutineScope, DestinationsNavigator, DialogState, MainActivity, SnackbarHostState, SharedTransitionScope, TransitionsVisibilityScope)
context(_: CoroutineScope, _: DestinationsNavigator, _: DialogState, _: MainActivity, _: SnackbarHostState, _: SharedTransitionScope, _: TransitionsVisibilityScope)
@Composable
fun GalleryDetailContent(
galleryInfo: GalleryInfo,
@@ -233,13 +242,13 @@ fun GalleryDetailContent(
Text(text = stringResource(R.string.filter_the_uploader, uploader))
}
Filter(FilterMode.UPLOADER, uploader).remember()
showSnackbar(filterAdded)
snackbar(filterAdded)
}
}
fun onDownloadButtonClick() {
galleryDetail ?: return
if (DownloadManager.getDownloadState(galleryDetail.gid) == DownloadInfo.STATE_INVALID) {
launchUI { startDownload(implicit<MainActivity>(), false, galleryDetail.galleryInfo) }
launchUI { startDownload(false, galleryDetail.galleryInfo) }
} else {
launch { confirmRemoveDownload(galleryDetail) }
}
@@ -378,7 +387,7 @@ fun GalleryDetailContent(
}
}
context(Context, CoroutineScope, DestinationsNavigator, DialogState, SnackbarHostState)
context(ctx: Context, _: CoroutineScope, _: DestinationsNavigator, _: DialogState, _: SnackbarHostState)
@Composable
fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
@Composable
@@ -439,7 +448,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
}
suspend fun showNewerVersionDialog() {
val items = galleryDetail.newerVersions.map {
getString(R.string.newer_version_title, it.title, it.posted)
string(R.string.newer_version_title, it.title, it.posted)
}
val selected = awaitSelectItem(items)
val info = galleryDetail.newerVersions[selected]
@@ -486,13 +495,13 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
modifyFavorites(galleryDetail.galleryInfo)
}.onSuccess { add ->
if (add) {
showSnackbar(addSucceed)
snackbar(addSucceed)
} else {
showSnackbar(removeSucceed)
snackbar(removeSucceed)
}
}.onFailure {
// TODO: We don't know if it's add or remove
showSnackbar(addFailed)
snackbar(addFailed)
}
}
}
@@ -550,12 +559,12 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
fun showArchiveDialog() {
launchIO {
if (galleryDetail.apiUid < 0) {
showSnackbar(signInFirst)
snackbar(signInFirst)
} else {
runSuspendCatching {
val (archiveList, funds) = bgWork { archiveResult.await() }
if (archiveList.isEmpty()) {
showSnackbar(noArchive)
snackbar(noArchive)
} else {
val selected = showNoButton {
ArchiveList(
@@ -565,15 +574,15 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
)
}
EhUtils.downloadArchive(galleryDetail, selected)
showSnackbar(downloadStarted)
snackbar(downloadStarted)
}
}.onFailure {
when (it) {
is NoHAtHClientException -> showSnackbar(failureNoHath)
is EhException -> showSnackbar(it.displayString())
is NoHAtHClientException -> snackbar(failureNoHath)
is EhException -> snackbar(it.displayString())
else -> {
logcat(it)
showSnackbar(downloadFailed)
snackbar(downloadFailed)
}
}
}
@@ -599,7 +608,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
suspend fun showTorrentDialog() {
val (torrentList, key) = bgWork { torrentResult.await() }
if (torrentList.isEmpty()) {
showSnackbar(noTorrents)
snackbar(noTorrents)
} else {
val selected = showNoButton(false) {
TorrentList(
@@ -613,7 +622,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
val link = "magnet:?xt=urn:btih:$hash&dn=$name&tr=$tracker"
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
try {
startActivity(intent)
ctx.startActivity(intent)
} catch (_: ActivityNotFoundException) {
withUIContext { addTextToClipboard(link, true) }
}
@@ -625,7 +634,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
onClick = {
launchIO {
when {
galleryDetail.torrentCount <= 0 -> showSnackbar(noTorrents)
galleryDetail.torrentCount <= 0 -> snackbar(noTorrents)
else -> runSwallowingWithUI { showTorrentDialog() }
}
}
@@ -633,9 +642,9 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
)
}
Spacer(modifier = Modifier.size(keylineMargin))
fun getAllRatingText(rating: Float, ratingCount: Int): String = getString(
fun getAllRatingText(rating: Float, ratingCount: Int): String = string(
R.string.rating_text,
getString(getRatingText(rating)),
string(getRatingText(rating)),
rating,
ratingCount,
)
@@ -648,7 +657,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
fun showRateDialog() {
launchIO {
if (galleryDetail.apiUid < 0) {
showSnackbar(signInFirst)
snackbar(signInFirst)
return@launchIO
}
val pendingRating = awaitResult(galleryDetail.rating.coerceAtLeast(.5f), title = R.string.rate) {
@@ -673,10 +682,10 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
ratingCount = result.ratingCount
}
ratingText = getAllRatingText(result.rating, result.ratingCount)
showSnackbar(rateSucceed)
snackbar(rateSucceed)
}.onFailure {
logcat(it)
showSnackbar(rateFailed)
snackbar(rateFailed)
}
}
}
@@ -731,7 +740,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
onSelect(addFilter) {
awaitConfirmationOrCancel { Text(text = stringResource(R.string.filter_the_tag, tag)) }
Filter(FilterMode.TAG, tag).remember()
showSnackbar(filterAdded)
snackbar(filterAdded)
}
if (galleryDetail.apiUid >= 0) {
when (vote) {
@@ -773,7 +782,7 @@ private fun getRatingText(rating: Float): Int = when ((rating * 2).roundToInt())
private fun List<GalleryTagGroup>.artistTag() = find { (ns, _) -> ns == TagNamespace.Artist || ns == TagNamespace.Cosplayer }?.let { (ns, tags) -> "$ns:${tags[0].text}" }
context(Context)
context(_: Context)
@Composable
private fun GalleryDetail.collectPreviewItems() = rememberInVM(previewList) {
val pageSize = previewList.size
@@ -816,7 +825,7 @@ private fun GalleryDetail.collectPreviewItems() = rememberInVM(previewList) {
}.flow.cachedIn(viewModelScope)
}.collectAsLazyPagingItems()
context(Context)
context(ctx: Context)
private fun LazyGridScope.galleryPreview(detail: GalleryDetail, data: LazyPagingItems<GalleryPreview>, onClick: (Int) -> Unit) {
val isV2Thumb = detail.previewList.first() is V2GalleryPreview
items(
@@ -826,6 +835,6 @@ private fun LazyGridScope.galleryPreview(detail: GalleryDetail, data: LazyPaging
) { index ->
val item = data[index]
EhPreviewItem(item, index) { onClick(index) }
PrefetchAround(data, index, if (isV2Thumb) 20 else 6, ::imageRequest)
PrefetchAround(data, index, if (isV2Thumb) 20 else 6) { imageRequest(it) }
}
}
@@ -59,6 +59,8 @@ import com.hippo.ehviewer.ui.main.GalleryDetailErrorTip
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitSelectTags
import com.hippo.ehviewer.ui.tools.isExpanded
import com.hippo.ehviewer.ui.tools.launchInVM
import com.hippo.ehviewer.ui.tools.rememberInVM
@@ -71,11 +73,13 @@ import com.hippo.files.toOkioPath
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.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.parcelize.Parcelize
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.launchIO
import moe.tarsin.snackbar
import moe.tarsin.tip
typealias VoteTag = suspend GalleryDetail.(String, Int) -> Unit
@@ -105,7 +109,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
}
}
val galleryDetailUrl = remember(gid, token) { EhUrl.getGalleryDetailUrl(gid, token, 0, false) }
ProvideAssistContent(galleryDetailUrl)
contextOf<MainActivity>().ProvideAssistContent(galleryDetailUrl)
var galleryInfo by rememberInVM {
val casted = args as? GalleryInfoArgs
@@ -150,9 +154,9 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
val new = copy(tagGroups = result).apply { fillInfo() }
detailCache[gid] = new
galleryInfo = new
showTip(R.string.tag_vote_successfully)
tip(R.string.tag_vote_successfully)
}.onFailure { e ->
showTip(e.displayString())
tip(e.displayString())
}
}
@@ -198,7 +202,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
) {
clickableItem(
onClick = {
AppHelper.share(implicit<MainActivity>(), galleryDetailUrl)
AppHelper.share(contextOf<MainActivity>(), galleryDetailUrl)
// In case the link is copied to the clipboard
Settings.clipboardTextHashCode = galleryDetailUrl.hashCode()
},
@@ -212,7 +216,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
val detail = galleryInfo as? GalleryDetail ?: return@clickableItem
launchIO {
if (detail.apiUid < 0) {
showSnackbar(signInFirst)
snackbar(signInFirst)
} else {
val tags = awaitSelectTags()
if (tags.isNotEmpty()) {
@@ -255,7 +259,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
val key = getImageKey(gd.gid, it)
imageCache.remove(key)
}
showSnackbar(cacheCleared)
snackbar(cacheCleared)
}
},
icon = {},
@@ -299,7 +303,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
file.delete()
exportFailed
}
showSnackbar(message = msg)
snackbar(message = msg)
}
}
}
@@ -318,7 +322,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
val from = stringResource(id = R.string.read_from, args.page)
val read = stringResource(id = R.string.read)
launchInVM {
val result = showSnackbar(from, read, true)
val result = snackbar(from, read, true)
if (result == SnackbarResult.ActionPerformed) {
navToReader(gi.findBaseInfo(), args.page)
}
@@ -57,7 +57,6 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
@@ -97,6 +96,7 @@ import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.filled.GoTo
import com.hippo.ehviewer.ui.DrawerHandle
import com.hippo.ehviewer.ui.LocalSideSheetState
import com.hippo.ehviewer.ui.ProvideSideSheetContent
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.awaitSelectDate
import com.hippo.ehviewer.ui.destinations.ProgressScreenDestination
@@ -114,6 +114,9 @@ import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
import com.hippo.ehviewer.ui.tools.HapticFeedbackType
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
@@ -124,15 +127,18 @@ 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.launchIO
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 kotlinx.coroutines.launch
import moe.tarsin.coroutines.onEachLatest
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.launch
import moe.tarsin.launchIO
import moe.tarsin.navigate
import moe.tarsin.snackbar
import moe.tarsin.string
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@@ -299,7 +305,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
onClick = {
launch {
if (urlBuilder.mode == MODE_IMAGE_SEARCH) {
showSnackbar(invalidImageQuickSearch)
snackbar(invalidImageQuickSearch)
} else {
// itemCount == 0 is treated as error, so no need to check here
val firstItem = data.itemSnapshotList.items[getFirstVisibleItemIndex()]
@@ -308,7 +314,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
if (urlBuilder.equalsQuickSearch(q)) {
val nextStr = q.name.substringAfterLast('@', "")
if (nextStr.toLongOrNull() == next) {
showSnackbar(getString(R.string.duplicate_quick_search, q.name))
snackbar(string(R.string.duplicate_quick_search, q.name))
return@launch
}
}
@@ -345,7 +351,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
windowInsets = WindowInsets(),
)
Box(modifier = Modifier.fillMaxSize()) {
val dialogState by rememberUpdatedState(implicit<DialogState>())
val dialogState by rememberUpdatedState(contextOf<DialogState>())
val quickSearchListState = rememberLazyListState()
val hapticFeedback = rememberHapticFeedback()
val reorderableLazyListState = rememberReorderableLazyListState(quickSearchListState) { from, to ->
@@ -371,8 +377,8 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
LaunchedEffect(dismissState) {
snapshotFlow { dismissState.currentValue }.collect { value ->
if (value == SwipeToDismissBoxValue.EndToStart) {
runCatching {
dialogState.awaitConfirmationOrCancel(confirmText = R.string.delete) {
dialogState.runCatching {
awaitConfirmationOrCancel(confirmText = R.string.delete) {
Text(text = stringResource(R.string.delete_quick_search, item.name))
}
}.onSuccess {
@@ -408,7 +414,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
val builder = ListUrlBuilder(item).apply {
language = languageFilter
}
navigator.navigate(builder.asDst())
navigate(builder.asDst())
} else {
urlBuilder = ListUrlBuilder(item).apply {
language = languageFilter
@@ -549,7 +555,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
val height by collectListThumbSizeAsState()
val showPages by Settings.showGalleryPages.collectAsState()
val searchBarConnection = remember {
val slop = ViewConfiguration.get(implicit<Context>()).scaledTouchSlop
val slop = ViewConfiguration.get(contextOf<Context>()).scaledTouchSlop
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
@@ -630,7 +636,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
onClick(EhIcons.Default.GoTo) {
if (isTopList) {
val page = urlBuilder.jumpTo?.toIntOrNull() ?: 0
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
val hint = string(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
val text = awaitInputText(title = gotoTitle, hint = hint, isNumber = true) { oriText ->
val goto = ensureNotNull(oriText.trim().toIntOrNull()) { invalidNum } - 1
ensure(goto in 0..<TOPLIST_PAGES) { outOfRange }
@@ -656,10 +662,10 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
private const val TOPLIST_PAGES = 200
context(_: Context)
@Composable
@Stable
private fun getSuitableTitleForUrlBuilder(urlBuilder: ListUrlBuilder, appName: Boolean = true): String? {
val context = LocalContext.current
val keyword = urlBuilder.keyword
val category = urlBuilder.category
val mode = urlBuilder.mode
@@ -677,7 +683,7 @@ private fun getSuitableTitleForUrlBuilder(urlBuilder: ListUrlBuilder, appName: B
}
}
MODE_TAG -> {
val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(context) && EhTagDatabase.initialized
val canTranslate = Settings.showTagTranslations && EhTagDatabase.translatable && EhTagDatabase.initialized
wrapTagKeyword(keyword, canTranslate)
}
else -> keyword
@@ -58,6 +58,7 @@ import com.hippo.ehviewer.ui.main.GalleryInfoListItem
import com.hippo.ehviewer.ui.main.plus
import com.hippo.ehviewer.ui.tools.Await
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.FavouriteStatusRouter
@@ -67,7 +68,8 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlin.math.roundToInt
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import moe.tarsin.launch
import moe.tarsin.navigate
@Destination<RootGraph>
@Composable
@@ -40,9 +40,11 @@ import com.hippo.files.toOkioPath
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.launch
import moe.tarsin.launch
import moe.tarsin.launchSnackbar
import moe.tarsin.launchUI
import moe.tarsin.navigate
@Destination<RootGraph>
@Composable
@@ -72,7 +74,7 @@ fun AnimatedVisibilityScope.ImageSearchScreen(navigator: DestinationsNavigator)
)
}
} else {
launch { showSnackbar(selectImageFirst) }
launchSnackbar(selectImageFirst)
}
},
modifier = Modifier.snackBarPadding(),
@@ -12,6 +12,7 @@ import com.hippo.ehviewer.ui.destinations.GalleryListScreenDestination
import com.hippo.ehviewer.ui.destinations.ProgressScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.Direction
import moe.tarsin.navigate
private fun urlToDestination(url: String): Direction? {
if (url.isEmpty()) return null
@@ -24,15 +25,17 @@ private fun urlToDestination(url: String): Direction? {
return null
}
context(nav: DestinationsNavigator)
@MainThread
fun DestinationsNavigator.navWithUrl(url: String): Boolean {
fun navWithUrl(url: String): Boolean {
val dest = urlToDestination(url) ?: return false
navigate(dest)
return true
}
context(nav: DestinationsNavigator)
@MainThread
fun DestinationsNavigator.popNavigate(direction: Direction) = navigate(direction) {
fun popNavigate(direction: Direction) = nav.navigate(direction) {
popUpTo(NavGraphs.root)
}
@@ -32,6 +32,7 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withUIContext
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.navigate
@Destination<RootGraph>
@Composable
@@ -48,7 +49,7 @@ fun AnimatedVisibilityScope.ProgressScreen(gid: Long, token: String, page: Int,
}.onSuccess {
withUIContext {
navigator.popBackStack()
navigator.navigate(gid asDstPageTo page with it)
navigate(gid asDstPageTo page with it)
}
}.onFailure {
error = it.displayString()
@@ -71,6 +71,7 @@ import com.hippo.ehviewer.dao.SearchDao
import com.hippo.ehviewer.ui.LocalNavDrawerState
import com.hippo.ehviewer.ui.destinations.ImageSearchScreenDestination
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.rememberCompositionActiveState
import com.hippo.ehviewer.ui.tools.thenIf
import com.jamal.composeprefs3.ui.ifNotNullThen
@@ -84,6 +85,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import moe.tarsin.navigate
fun interface SuggestionProvider {
fun providerSuggestions(text: String): Suggestion?
@@ -99,7 +101,7 @@ abstract class Suggestion {
suspend fun SearchDao.suggestions(prefix: String, limit: Int) = (if (prefix.isBlank()) list(limit) else rawSuggestions(prefix, limit))
context(DialogState, DestinationsNavigator)
context(_: DialogState, _: DestinationsNavigator)
@Composable
fun SearchBarScreen(
onApplySearch: (String) -> Unit,
@@ -14,18 +14,13 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.fromHtml
@@ -38,6 +33,7 @@ import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.destinations.LicenseScreenDestination
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.updater.AppUpdater
import com.hippo.ehviewer.updater.Release
@@ -50,9 +46,10 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.launchSnackbar
import moe.tarsin.navigate
import moe.tarsin.string
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}"
private const val RELEASE_URL = "$REPO_URL/releases"
@@ -68,11 +65,7 @@ private fun author() = AnnotatedString.fromHtml(stringResource(R.string.settings
@Destination<RootGraph>
@Composable
fun AnimatedVisibilityScope.AboutScreen(navigator: DestinationsNavigator) = Screen(navigator) {
val context = LocalContext.current
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
Scaffold(
topBar = {
TopAppBar(
@@ -85,7 +78,6 @@ fun AnimatedVisibilityScope.AboutScreen(navigator: DestinationsNavigator) = Scre
scrollBehavior = scrollBehavior,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
Preference(
@@ -105,7 +97,7 @@ fun AnimatedVisibilityScope.AboutScreen(navigator: DestinationsNavigator) = Scre
url = REPO_URL,
)
Preference(title = stringResource(id = R.string.license)) {
navigator.navigate(LicenseScreenDestination)
navigate(LicenseScreenDestination)
}
Preference(
title = stringResource(id = R.string.settings_about_version),
@@ -127,18 +119,17 @@ fun AnimatedVisibilityScope.AboutScreen(navigator: DestinationsNavigator) = Scre
)
WorkPreference(title = stringResource(id = R.string.settings_about_check_for_updates)) {
runSuspendCatching {
AppUpdater.checkForUpdate(true)?.let {
showNewVersion(context, it)
} ?: launchSnackBar(getString(R.string.already_latest_version))
AppUpdater.checkForUpdate(true)?.let { showNewVersion(it) } ?: launchSnackbar(string(R.string.already_latest_version))
}.onFailure {
launchSnackBar(getString(R.string.update_failed, it.displayString()))
launchSnackbar(string(R.string.update_failed, it.displayString()))
}
}
}
}
}
suspend fun DialogState.showNewVersion(context: Context, release: Release) {
context(ctx: Context, _: DialogState)
suspend fun showNewVersion(release: Release) {
awaitConfirmationOrCancel(
confirmText = R.string.download,
title = R.string.new_version_available,
@@ -154,10 +145,10 @@ suspend fun DialogState.showNewVersion(context: Context, release: Release) {
}
if (Settings.backupBeforeUpdate) {
val time = ReadableTime.getFilenamableTime()
EhDB.exportDB(context, (downloadLocation / "$time.db"))
EhDB.exportDB(ctx, (downloadLocation / "$time.db"))
}
// TODO: Download in the background and show progress in notification
val path = AppConfig.tempDir / "update.apk"
AppUpdater.downloadUpdate(release.downloadLink, path.apply { delete() })
withUIContext { context.installPackage(path.toFile()) }
withUIContext { installPackage(path.toFile()) }
}
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
import android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS
import androidx.activity.result.PickVisualMediaRequest
@@ -42,7 +43,6 @@ import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.screen.implicit
import com.hippo.ehviewer.ui.showRestartDialog
import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.ui.tools.rememberedAccessor
@@ -64,7 +64,6 @@ 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.launchIO
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import java.io.File
@@ -73,14 +72,49 @@ import java.util.zip.ZipOutputStream
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.launch
import moe.tarsin.launchSnackbar
import moe.tarsin.string
context(ctx: Context)
private fun dumplog(uri: Uri): Unit = with(ctx) {
grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.openOutputStream(uri)?.use { outputStream ->
val files = ArrayList<File>()
AppConfig.externalParseErrorDir?.listFiles()?.let { files.addAll(it) }
AppConfig.externalCrashDir?.listFiles()?.let { files.addAll(it) }
ZipOutputStream(outputStream).use { zipOs ->
files.forEach { file ->
if (!file.isFile) return@forEach
val entry = ZipEntry(file.name)
zipOs.putNextEntry(entry)
file.inputStream().use { it.copyTo(zipOs) }
}
val logcatEntry = ZipEntry("logcat-" + ReadableTime.getFilenamableTime() + ".txt")
zipOs.putNextEntry(logcatEntry)
CrashHandler.collectInfo(zipOs.writer())
Runtime.getRuntime().exec("logcat -d").inputStream.use { it.copyTo(zipOs) }
}
}
}
context(ctx: Context)
private fun exportDatabase(uri: Uri) {
ctx.grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
EhDB.exportDB(ctx, uri.toOkioPath())
}
context(ctx: Context)
private suspend fun importDatabase(uri: Uri) {
ctx.grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
EhDB.importDB(ctx, uri)
}
@Destination<RootGraph>
@Composable
fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = Screen(navigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
fun launchSnackBar(content: String) = launch { showSnackbar(content) }
Scaffold(
topBar = {
TopAppBar(
@@ -136,27 +170,10 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
) { uri ->
uri?.run {
runCatching {
grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.openOutputStream(uri)?.use { outputStream ->
val files = ArrayList<File>()
AppConfig.externalParseErrorDir?.listFiles()?.let { files.addAll(it) }
AppConfig.externalCrashDir?.listFiles()?.let { files.addAll(it) }
ZipOutputStream(outputStream).use { zipOs ->
files.forEach { file ->
if (!file.isFile) return@forEach
val entry = ZipEntry(file.name)
zipOs.putNextEntry(entry)
file.inputStream().use { it.copyTo(zipOs) }
}
val logcatEntry = ZipEntry("logcat-" + ReadableTime.getFilenamableTime() + ".txt")
zipOs.putNextEntry(logcatEntry)
CrashHandler.collectInfo(zipOs.writer())
Runtime.getRuntime().exec("logcat -d").inputStream.use { it.copyTo(zipOs) }
}
launchSnackBar(getString(R.string.settings_advanced_dump_logcat_to, uri.displayPath))
}
dumplog(uri)
launchSnackbar(string(R.string.settings_advanced_dump_logcat_to, uri.displayPath))
}.onFailure {
launchSnackBar(dumpLogError)
launchSnackbar(dumpLogError)
logcat(it)
}
}
@@ -241,12 +258,11 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
) { uri ->
uri?.let {
runCatching {
grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
EhDB.exportDB(implicit<Context>(), uri.toOkioPath())
launchSnackBar(getString(R.string.settings_advanced_export_data_to, uri.displayPath))
exportDatabase(uri)
launchSnackbar(string(R.string.settings_advanced_export_data_to, uri.displayPath))
}.onFailure {
logcat(it)
launchSnackBar(exportFailed)
launchSnackbar(exportFailed)
}
}
}
@@ -260,12 +276,11 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
) { uri ->
uri?.let {
runCatching {
grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
EhDB.importDB(implicit<Context>(), uri)
launchSnackBar(importSucceed)
importDatabase(uri)
launchSnackbar(importSucceed)
}.onFailure {
logcat(it)
launchSnackBar(importFailed)
launchSnackbar(importFailed)
}
}
}
@@ -284,13 +299,13 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
tailrec suspend fun doBackup() {
val result = EhEngine.getFavorites(favListUrlBuilder.build())
if (result.galleryInfoList.isEmpty()) {
launchSnackBar(backupNothing)
launchSnackbar(backupNothing)
} else {
if (favTotal == 0) favTotal = result.countArray.sum()
favIndex += result.galleryInfoList.size
val status = "($favIndex/$favTotal)"
EhDB.putLocalFavorites(result.galleryInfoList)
launchSnackBar(getString(R.string.settings_advanced_backup_favorite_start, status))
launchSnackbar(string(R.string.settings_advanced_backup_favorite_start, status))
if (result.next != null) {
delay(Settings.downloadDelay.toLong())
favListUrlBuilder.setIndex(result.next, true)
@@ -298,34 +313,39 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
}
}
}
launchIO {
launch {
runSuspendCatching {
doBackup()
}.onSuccess {
launchSnackBar(backupSucceed)
launchSnackbar(backupSucceed)
}.onFailure {
logcat(it)
launchSnackBar(backupFailed)
launchSnackbar(backupFailed)
}
}
}
}
Preference(title = stringResource(id = R.string.open_by_default)) {
try {
@SuppressLint("InlinedApi")
val intent = Intent(
ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
"package:$packageName".toUri(),
)
startActivity(intent)
} catch (_: ActivityNotFoundException) {
val intent = Intent(
ACTION_APPLICATION_DETAILS_SETTINGS,
"package:$packageName".toUri(),
)
startActivity(intent)
}
openByDefaultSettings()
}
}
}
}
context(ctx: Context)
private fun openByDefaultSettings() = with(ctx) {
try {
@SuppressLint("InlinedApi")
val intent = Intent(
ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
"package:$packageName".toUri(),
)
startActivity(intent)
} catch (_: ActivityNotFoundException) {
val intent = Intent(
ACTION_APPLICATION_DETAILS_SETTINGS,
"package:$packageName".toUri(),
)
startActivity(intent)
}
}
@@ -2,6 +2,7 @@ package com.hippo.ehviewer.ui.settings
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
import android.os.Environment
@@ -53,6 +54,7 @@ import com.hippo.ehviewer.spider.readCompatFromPath
import com.hippo.ehviewer.spider.speedLevelToSpeed
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.ui.tools.rememberedAccessor
import com.hippo.ehviewer.util.AppConfig
@@ -71,10 +73,11 @@ import com.hippo.files.toUri
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.system.logcat
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.launchIO
import moe.tarsin.launchSnackbar
import moe.tarsin.string
import okio.Path
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
@@ -83,13 +86,12 @@ import splitties.init.appCtx
@Composable
fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = Screen(navigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
fun launchSnackBar(content: String) = launch { showSnackbar(content) }
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(id = R.string.settings_download)) },
navigationIcon = {
IconButton(onClick = { popBackStack() }) {
IconButton(onClick = { navigator.popBackStack() }) {
Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
}
},
@@ -104,14 +106,14 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
treeUri?.run {
launchIO {
runCatching {
contentResolver.takePersistableUriPermission(treeUri, FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
contextOf<Context>().contentResolver.takePersistableUriPermission(treeUri, FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
val path = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)).toOkioPath()
check(path.isDirectory) { "$path is not a directory" }
keepNoMediaFileStatus(path) // Check if the directory is writable
downloadLocationState = path
}.onFailure {
logcat(it)
launchSnackBar(cannotGetDownloadLocation)
launchSnackbar(cannotGetDownloadLocation)
}
}
}
@@ -155,7 +157,7 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
logcat(it)
}
}
launchSnackBar(cannotGetDownloadLocation)
launchSnackbar(cannotGetDownloadLocation)
}
}
}
@@ -239,10 +241,10 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
di.galleryInfo.also { SpiderDen(it, di.dirname!!).writeComicInfo(false) }
}
EhDB.updateGalleryInfo(toUpdate)
launchSnackBar(getString(R.string.settings_download_reload_metadata_successfully, toUpdate.size))
launchSnackbar(string(R.string.settings_download_reload_metadata_successfully, toUpdate.size))
}
}.onFailure {
launchSnackBar(getString(R.string.settings_download_reload_metadata_failed, it.displayString()))
launchSnackbar(string(R.string.settings_download_reload_metadata_failed, it.displayString()))
}
}
val restoreFailed = stringResource(id = R.string.settings_download_restore_failed)
@@ -283,7 +285,7 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
fillGalleryListByApi(it, EhUrl.referer)
}
if (result.isEmpty()) {
launchSnackBar(RESTORE_COUNT_MSG(restoreDirCount))
launchSnackbar(RESTORE_COUNT_MSG(restoreDirCount))
} else {
val count = result.parMap {
if (it.pages != 0) {
@@ -292,11 +294,11 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
SpiderDen(it.galleryInfo, it.dirname).writeComicInfo(false)
}
}.size
launchSnackBar(RESTORE_COUNT_MSG(count + restoreDirCount))
launchSnackbar(RESTORE_COUNT_MSG(count + restoreDirCount))
}
}.onFailure {
logcat(it)
launchSnackBar(restoreFailed)
launchSnackbar(restoreFailed)
}
}
WorkPreference(
@@ -323,7 +325,7 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
}
}
val cnt = list.count { runCatching { it.delete() }.getOrNull() != null }
launchSnackBar(FINAL_CLEAR_REDUNDANCY_MSG(cnt))
launchSnackbar(FINAL_CLEAR_REDUNDANCY_MSG(cnt))
}
}
}
@@ -1,6 +1,5 @@
package com.hippo.ehviewer.ui.settings
import android.content.Context
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.foundation.layout.Column
@@ -44,7 +43,9 @@ import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.destinations.FilterScreenDestination
import com.hippo.ehviewer.ui.destinations.MyTagsScreenDestination
import com.hippo.ehviewer.ui.destinations.UConfigScreenDestination
import com.hippo.ehviewer.ui.screen.implicit
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitSelectItem
import com.hippo.ehviewer.ui.tools.awaitSelectTime
import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.ui.tools.rememberedAccessor
import com.hippo.ehviewer.util.copyTextToClipboard
@@ -56,6 +57,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalTime
import moe.tarsin.navigate
@Destination<RootGraph>
@Composable
@@ -70,7 +72,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
TopAppBar(
title = { Text(text = stringResource(id = R.string.settings_eh)) },
navigationIcon = {
IconButton(onClick = { popBackStack() }) {
IconButton(onClick = { navigator.popBackStack() }) {
Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
}
},
@@ -134,11 +136,11 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
Preference(
title = stringResource(id = R.string.settings_u_config),
summary = stringResource(id = R.string.settings_u_config_summary),
) { navigator.navigate(UConfigScreenDestination) }
) { navigate(UConfigScreenDestination) }
Preference(
title = stringResource(id = R.string.settings_my_tags),
summary = stringResource(id = R.string.settings_my_tags_summary),
) { navigator.navigate(MyTagsScreenDestination) }
) { navigate(MyTagsScreenDestination) }
}
var defaultFavSlot by Settings::defaultFavSlot.observed
val disabled = stringResource(id = R.string.disabled_nav)
@@ -243,7 +245,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
value = Settings::commentThreshold,
)
}
if (EhTagDatabase.isTranslatable(implicit<Context>())) {
if (EhTagDatabase.translatable) {
SwitchPreference(
title = stringResource(id = R.string.settings_eh_show_tag_translations),
summary = stringResource(id = R.string.settings_eh_show_tag_translations_summary),
@@ -63,14 +63,16 @@ import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.FilterMode
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.tools.Await
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.dialog
import com.hippo.ehviewer.ui.tools.thenIf
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlin.coroutines.resume
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import moe.tarsin.async
import moe.tarsin.coroutines.groupByToObserved
import moe.tarsin.launch
@Destination<RootGraph>
@Composable
@@ -35,7 +35,7 @@ fun AnimatedVisibilityScope.LicenseScreen(navigator: DestinationsNavigator) = Sc
TopAppBar(
title = { Text(text = stringResource(id = R.string.license)) },
navigationIcon = {
IconButton(onClick = { popBackStack() }) {
IconButton(onClick = { navigator.popBackStack() }) {
Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
}
},
@@ -32,7 +32,7 @@ fun AnimatedVisibilityScope.MyTagsScreen(navigator: DestinationsNavigator) = Scr
TopAppBar(
title = { Text(text = stringResource(id = R.string.my_tags)) },
navigationIcon = {
IconButton(onClick = { popBackStack() }) {
IconButton(onClick = { navigator.popBackStack() }) {
Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
}
},
@@ -106,9 +106,8 @@ fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue -
}
@Composable
fun UrlPreference(title: String, url: String) {
val context = LocalContext.current
Preference(title, url) { context.openBrowser(url) }
fun UrlPreference(title: String, url: String) = with(LocalContext.current) {
Preference(title, url) { openBrowser(url) }
}
@Composable
@@ -9,16 +9,12 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
import com.hippo.ehviewer.R
@@ -26,32 +22,31 @@ import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.isAuthenticationSupported
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.ui.tools.rememberedAccessor
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch
import moe.tarsin.launch
import moe.tarsin.launchSnackbar
@Destination<RootGraph>
@Composable
fun AnimatedVisibilityScope.PrivacyScreen(navigator: DestinationsNavigator) = Screen(navigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val snackbarHostState = remember { SnackbarHostState() }
fun launchSnackBar(content: String) = launch { snackbarHostState.showSnackbar(content) }
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(id = R.string.settings_privacy)) },
navigationIcon = {
IconButton(onClick = { popBackStack() }) {
IconButton(onClick = { navigator.popBackStack() }) {
Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
}
},
scrollBehavior = scrollBehavior,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) {
Column(modifier = Modifier.padding(it).nestedScroll(scrollBehavior.nestedScrollConnection)) {
val security = Settings.security.asMutableState()
@@ -72,7 +67,7 @@ fun AnimatedVisibilityScope.PrivacyScreen(navigator: DestinationsNavigator) = Sc
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
summary = summary,
value = securityDelay.rememberedAccessor,
enabled = LocalContext.current.isAuthenticationSupported(),
enabled = isAuthenticationSupported(),
)
}
SwitchPreference(
@@ -91,7 +86,7 @@ fun AnimatedVisibilityScope.PrivacyScreen(navigator: DestinationsNavigator) = Sc
title = R.string.clear_search_history_confirm,
)
searchDatabase.searchDao().clear()
launchSnackBar(searchHistoryCleared)
launchSnackbar(searchHistoryCleared)
}
}
}
@@ -3,7 +3,6 @@ package com.hippo.ehviewer.ui.tools
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@@ -11,7 +10,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
@@ -28,7 +26,7 @@ import androidx.compose.ui.text.input.getTextBeforeSelection
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import com.hippo.ehviewer.R
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.util.toRangeSet
import io.github.petertrr.diffutils.diffInline
import io.github.petertrr.diffutils.patch.ChangeDelta
@@ -142,12 +140,11 @@ fun AnnotatedString.toBBCode() = buildString {
}
}
context(activity: MainActivity)
@Composable
fun rememberBBCodeTextToolbar(textFieldValue: MutableState<TextFieldValue>): TextToolbar {
var tfv by textFieldValue
val view = LocalView.current
val context = LocalContext.current
val activity = remember { context.findActivity<ComponentActivity>() }
val coroutineScope = rememberCoroutineScope()
val toolbar = remember {
object : TextToolbar {
File diff suppressed because it is too large. Load diff
@@ -25,33 +25,35 @@ val NoopTransitionsVisibilityScope = TransitionsVisibilityScope(emptySet())
@Stable
value class TransitionsVisibilityScope(val scopes: Set<AnimatedVisibilityScope>)
context(TransitionsVisibilityScope)
context(visScope: TransitionsVisibilityScope)
@Composable
inline fun <T> togetherWith(scope: AnimatedVisibilityScope, block: @Composable TransitionsVisibilityScope.() -> T) = block(remember(scopes, scope) { TransitionsVisibilityScope(scopes + scope) })
inline fun <T> togetherWith(scope: AnimatedVisibilityScope, block: @Composable TransitionsVisibilityScope.() -> T) = block(remember(visScope.scopes, scope) { TransitionsVisibilityScope(visScope.scopes + scope) })
context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
context(shareScope: SharedTransitionScope, visScope: TransitionsVisibilityScope, generator: SETNodeGenerator)
@Composable
fun Modifier.sharedBounds(
key: String,
enter: EnterTransition = fadeIn(),
exit: ExitTransition = fadeOut(),
) = remember(key) { summon(key) }.let { node ->
) = remember(key) { generator.summon(key) }.let { node ->
DisposableEffect(key) {
onDispose {
dispose(node)
generator.dispose(node)
}
}
scopes.fold(this) { modifier, scope ->
modifier.sharedBounds(
rememberSharedContentState("${node.syntheticKey} + ${scope.transition.label}"),
scope,
enter,
exit,
)
visScope.scopes.fold(this) { modifier, scope ->
with(shareScope) {
modifier.sharedBounds(
rememberSharedContentState("${node.syntheticKey} + ${scope.transition.label}"),
scope,
enter,
exit,
)
}
}
}
context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
context(_: SharedTransitionScope, _: TransitionsVisibilityScope, _: SETNodeGenerator)
@Composable
inline fun SharedElementBox(key: String, shape: Shape, crossinline content: @Composable BoxScope.() -> Unit) {
val modifier = Modifier.sharedBounds(key = key).clip(shape)
@@ -26,6 +26,7 @@ import android.view.textclassifier.TextClassifier
import androidx.core.os.persistableBundleOf
import com.hippo.ehviewer.R
import com.hippo.ehviewer.ui.MainActivity
import moe.tarsin.tip
import splitties.systemservices.clipboardManager
fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
@@ -38,12 +39,14 @@ fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
)
}
fun Context.addTextToClipboard(text: CharSequence?, useToast: Boolean = false) {
context(ctx: Context)
fun addTextToClipboard(text: CharSequence?, useToast: Boolean = false) {
copyTextToClipboard(text, false)
// Avoid double notify user since system have done that on Tiramisu above
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
val activity = findActivity<MainActivity>()
activity.showTip(R.string.copied_to_clipboard, useToast)
with(findActivity<MainActivity>()) {
tip(R.string.copied_to_clipboard, useToast)
}
}
}
@@ -5,11 +5,9 @@ import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
/**
* Find the closest Activity in a given Context.
*/
inline fun <reified T : Activity> Context.findActivity(): T {
var context = this
context(ctx: Context)
inline fun <reified T : Activity> findActivity(): T {
var context = ctx
while (context is ContextWrapper) {
if (context is Activity) return context as T
context = context.baseContext
@@ -17,7 +15,8 @@ inline fun <reified T : Activity> Context.findActivity(): T {
throw IllegalStateException("findActivity() should be called in the context of an Activity")
}
fun Context.restartApplication() {
context(ctx: Context)
fun restartApplication() = with(ctx) {
packageManager.getLaunchIntentForPackage(packageName)?.let {
startActivity(Intent.makeRestartActivityTask(it.component))
Runtime.getRuntime().exit(0)
@@ -6,7 +6,8 @@ import androidx.core.os.LocaleListCompat
import com.hippo.ehviewer.R
import org.xmlpull.v1.XmlPullParser
fun Context.getLanguages(): Map<String, String> {
context(ctx: Context)
fun getLanguages(): Map<String, String> = with(ctx) {
val languages = mutableMapOf("system" to getString(R.string.app_language_system))
resources.getXml(R.xml._generated_res_locale_config).use { parser ->
var event = parser.eventType
@@ -33,9 +33,10 @@ import kotlin.coroutines.suspendCoroutine
private val atomicInteger = AtomicInt(0)
private val Context.lifecycle: Lifecycle
context(ctx: Context)
private val lifecycle: Lifecycle
get() {
var context: Context? = this
var context: Context? = ctx
while (true) {
when (context) {
is LifecycleOwner -> return context.lifecycle
@@ -45,7 +46,8 @@ private val Context.lifecycle: Lifecycle
}
}
suspend fun <I, O> Context.awaitActivityResult(contract: ActivityResultContract<I, O>, input: I): O {
context(ctx: Context)
suspend fun <I, O> awaitActivityResult(contract: ActivityResultContract<I, O>, input: I): O {
val key = "activity_rq#${atomicInteger.fetchAndIncrement()}"
var launcher: ActivityResultLauncher<I>? = null
var observer: LifecycleEventObserver? = null
@@ -71,15 +73,18 @@ suspend fun <I, O> Context.awaitActivityResult(contract: ActivityResultContract<
}
}
suspend fun Context.requestPermission(key: String): Boolean {
if (ContextCompat.checkSelfPermission(this, key) == PackageManager.PERMISSION_GRANTED) return true
context(ctx: Context)
suspend fun requestPermission(key: String): Boolean {
if (ContextCompat.checkSelfPermission(ctx, key) == PackageManager.PERMISSION_GRANTED) return true
return awaitActivityResult(ActivityResultContracts.RequestPermission(), key)
}
suspend fun Context.pickVisualMedia(type: VisualMediaType): Uri? = awaitActivityResult(ActivityResultContracts.PickVisualMedia(), PickVisualMediaRequest(mediaType = type))
context(_: Context)
suspend fun pickVisualMedia(type: VisualMediaType): Uri? = awaitActivityResult(ActivityResultContracts.PickVisualMedia(), PickVisualMediaRequest(mediaType = type))
context(ctx: Context)
@RequiresApi(Build.VERSION_CODES.O)
suspend fun Context.requestInstallPermission(): Boolean {
suspend fun requestInstallPermission(): Boolean = with(ctx) {
if (packageManager.canRequestPackageInstalls()) return true
val granted = requestPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES)
if (!granted) {
@@ -95,7 +100,8 @@ suspend fun Context.requestInstallPermission(): Boolean {
return packageManager.canRequestPackageInstalls()
}
suspend fun Context.installPackage(file: File) {
context(ctx: Context)
suspend fun installPackage(file: File) = with(ctx) {
val canInstall = !isAtLeastO || requestInstallPermission()
check(canInstall) { getString(R.string.permission_denied) }
val contentUri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
@@ -46,14 +46,15 @@ fun ProgressDialog() {
}
}
suspend fun <R> DialogState.bgWork(work: suspend () -> R) = mutex.mutate {
context(state: DialogState)
suspend fun <R> bgWork(work: suspend () -> R) = state.mutex.mutate {
try {
suspendCoroutineUninterceptedOrReturn { cont ->
work.startCoroutineUninterceptedOrReturn(cont).also { r ->
if (r == COROUTINE_SUSPENDED) value = { ProgressDialog() }
if (r == COROUTINE_SUSPENDED) state.value = { ProgressDialog() }
}
}
} finally {
dismiss()
state.dismiss()
}
}
@@ -0,0 +1,49 @@
package moe.tarsin
import android.content.Context
import androidx.annotation.StringRes
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import com.hippo.ehviewer.ui.MainActivity
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.Direction
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
context(scope: CoroutineScope)
fun launch(context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit) = scope.launch(context, start, block)
context(scope: CoroutineScope)
fun <T> async(context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T) = scope.async(context, start, block)
context(_: CoroutineScope)
fun launchIO(block: suspend CoroutineScope.() -> Unit) = launch(Dispatchers.IO, block = block)
context(_: CoroutineScope)
fun launchUI(block: suspend CoroutineScope.() -> Unit) = launch(Dispatchers.Main, block = block)
context(nav: DestinationsNavigator)
fun navigate(direction: Direction) = nav.navigate(direction)
context(ctx: Context)
fun string(@StringRes id: Int) = ctx.getString(id)
context(ctx: Context)
fun string(@StringRes id: Int, vararg args: Any?) = ctx.getString(id, *args)
context(activity: MainActivity)
fun tip(message: String, useToast: Boolean = false) = activity.showTip(message, useToast)
context(activity: MainActivity)
fun tip(@StringRes id: Int, useToast: Boolean = false) = tip(string(id), useToast)
context(state: SnackbarHostState)
suspend fun snackbar(message: String, actionLabel: String? = null, withDismissAction: Boolean = false, duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite) = state.showSnackbar(message, actionLabel, withDismissAction, duration)
context(state: SnackbarHostState, scope: CoroutineScope)
fun launchSnackbar(message: String, actionLabel: String? = null, withDismissAction: Boolean = false, duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite) = launch { snackbar(message, actionLabel, withDismissAction, duration) }
@@ -4,7 +4,7 @@ import androidx.compose.material3.SnackbarHostState
import com.hippo.ehviewer.util.displayString
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import moe.tarsin.launchSnackbar
inline fun <T, reified E : Throwable> Result<T>.except() = onFailure { if (it is E) throw it }
@@ -12,5 +12,5 @@ inline fun <R> runSuspendCatching(block: () -> R) = runCatching(block).except<R,
inline fun <T, R> T.runSuspendCatching(block: T.() -> R) = runCatching(block).except<R, CancellationException>()
context(SnackbarHostState, CoroutineScope)
inline fun <R> runSwallowingWithUI(block: () -> R) = runSuspendCatching(block).onFailure { e -> launch { showSnackbar(e.displayString()) } }
context(state: SnackbarHostState, scope: CoroutineScope)
inline fun <R> runSwallowingWithUI(block: () -> R) = runSuspendCatching(block).onFailure { e -> launchSnackbar(e.displayString()) }