diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 73b56d177..b6bc75ee4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -282,8 +282,7 @@ kotlin { "splitties.preferences.DataStorePreferencesPreview", ) freeCompilerArgs.addAll( - "-Xcontext-receivers", - "-Xsuppress-warning=CONTEXT_RECEIVERS_DEPRECATED", + "-Xcontext-parameters", "-Xannotation-default-target=param-property", ) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt index 8811beefa..bf00ceede 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt @@ -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> { if (!initialized) return emptySequence() - val translate = expectTranslate && isTranslatable(implicit()) + 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 = 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() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhUtils.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhUtils.kt index 2922230db..f45b09055 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhUtils.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhUtils.kt @@ -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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt b/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt index 8de7fb820..89580544a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt @@ -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.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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ktbuilder/Coil.kt b/app/src/main/kotlin/com/hippo/ehviewer/ktbuilder/Coil.kt index 159986776..b96a02148 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ktbuilder/Coil.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ktbuilder/Coil.kt @@ -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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetEngine.kt b/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetEngine.kt index 51fa227de..d8fe9f83a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetEngine.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetEngine.kt @@ -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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt index 3e56e8be8..1d49f67e8 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt @@ -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()) - } -} +fun writeComicInfo(info: ComicInfo, file: Path) = file.write { xml.encodeToSink(this, info) } fun readComicInfo(file: Path): ComicInfo? = runCatching { file.read { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt index e60d76275..f569e8f12 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt @@ -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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt index 4ce8e98ab..9b8e47a6e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt @@ -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())) } -} +fun SpiderInfo.write(file: Path) = file.write { write(cbor.encodeToByteArray(contextOf())) } fun SpiderInfo.saveToCache() { runSuspendCatching { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt index 80a1271a7..02445eaa0 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt @@ -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()) { - 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()) { - 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()) { - 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()) { - 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) { +context(_: DialogState) +suspend fun confirmRemoveDownloadRange(list: Collection) { 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): String? { +context(_: DialogState) +suspend fun showMoveDownloadLabelList(list: Collection): 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 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)) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/ComposeDefault.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/ComposeDefault.kt index 658878ef9..9bd8ab92a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/ComposeDefault.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/ComposeDefault.kt @@ -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 AnimatedVisibilityScope.Screen( +inline fun 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()) { + togetherWith(scope) { block( with(LocalContext.current) { remember { findActivity() } }, LocalSnackBarHostState.current, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/GalleryInfoBottomSheet.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/GalleryInfoBottomSheet.kt index 5ef379c72..dc9835d9c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/GalleryInfoBottomSheet.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/GalleryInfoBottomSheet.kt @@ -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()) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt index fa55368ff..4918a0593 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt @@ -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 { error("CompositionLocal LocalNavDrawerState not present!") } -val LocalSideSheetState = compositionLocalOf { error("CompositionLocal LocalSideSheetState not present!") } + val LocalDrawerHandle = compositionLocalOf> { error("CompositionLocal LocalDrawerHandle not present!") } val LocalSnackBarHostState = compositionLocalOf { error("CompositionLocal LocalSnackBarHostState not present!") } val LocalSnackBarFabPadding = compositionLocalOf> { error("CompositionLocal LocalSnackBarFabPadding not present!") } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/MutableSideSheet.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/MutableSideSheet.kt new file mode 100644 index 000000000..c969b76bb --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/MutableSideSheet.kt @@ -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> { error("") } +val LocalSideSheetState = staticCompositionLocalOf { 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() } + 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, + ) + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/SecurityActivity.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/SecurityActivity.kt index 45083dae2..dfe3caaf8 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/SecurityActivity.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/SecurityActivity.kt @@ -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() { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/UrlOpener.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/UrlOpener.kt index d9f22e1d0..91af948a9 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/UrlOpener.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/UrlOpener.kt @@ -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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/login/SignInScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/login/SignInScreen.kt index fd63d1451..fa5b5806b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/login/SignInScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/login/SignInScreen.kt @@ -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(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( diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/AvatarIcon.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/AvatarIcon.kt index b84d57ca7..404621062 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/AvatarIcon.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/AvatarIcon.kt @@ -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() @@ -81,7 +82,7 @@ private val limitFlow: StateFlow = 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() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt index 60d59045c..072f98022 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt @@ -96,7 +96,7 @@ private val constraintSet = ConstraintSet { } } -context(SharedTransitionScope, TransitionsVisibilityScope) +context(_: SharedTransitionScope, _: TransitionsVisibilityScope) @Composable fun DownloadCard( onClick: () -> Unit, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt index 48ec74c33..5daf405ed 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt @@ -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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt index 7e812ecb9..2f688c7ef 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt @@ -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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryDetail.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryDetail.kt index d56f53e5a..5be632be8 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryDetail.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryDetail.kt @@ -98,7 +98,7 @@ fun GalleryDetailHeaderInfoCard( } } -context(SharedTransitionScope, TransitionsVisibilityScope) +context(_: SharedTransitionScope, _: TransitionsVisibilityScope) @Composable fun GalleryDetailHeaderCard( info: GalleryInfo, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt index 32ba8cc26..5d7a5a4c2 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt @@ -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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryList.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryList.kt index 889b34385..280fb8254 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryList.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryList.kt @@ -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) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryPreview.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryPreview.kt index e0185baed..de57b2124 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryPreview.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryPreview.kt @@ -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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryTag.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryTag.kt index 970d398dc..6094d8381 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryTag.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryTag.kt @@ -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, @@ -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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/SearchFilter.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/SearchFilter.kt index ac56be133..3ccced735 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/SearchFilter.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/SearchFilter.kt @@ -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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt index ccc6fa95d..c8a77f2cf 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt @@ -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)) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt index d1a056538..3b6a2d6a9 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt @@ -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 usePageLoader(args: ReaderScreenArgs, crossinline block: suspend (PageLoader) -> T) = when (args) { is ReaderScreenArgs.Gallery -> { val info = args.info @@ -390,12 +385,12 @@ suspend inline fun 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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/Context.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/Context.kt deleted file mode 100644 index 761e04ab3..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/Context.kt +++ /dev/null @@ -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 implicit(): T = this@T diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt index 1134aa238..0c738c460 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt @@ -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()) && 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()) + val dialogState by rememberUpdatedState(contextOf()) 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(), DownloadService::class.java) + val intent = Intent(contextOf(), DownloadService::class.java) intent.action = DownloadService.ACTION_START_ALL - ContextCompat.startForegroundService(implicit(), intent) + ContextCompat.startForegroundService(contextOf(), 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(), DownloadService::class.java) + val intent = Intent(contextOf(), DownloadService::class.java) intent.action = DownloadService.ACTION_START_RANGE intent.putExtra(DownloadService.KEY_GID_LIST, gidList) - ContextCompat.startForegroundService(implicit(), intent) + ContextCompat.startForegroundService(contextOf(), 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()).scaledTouchSlop + val slop = ViewConfiguration.get(contextOf()).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(), DownloadService::class.java) + val intent = Intent(contextOf(), DownloadService::class.java) intent.action = DownloadService.ACTION_START intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo) - ContextCompat.startForegroundService(implicit(), intent) + ContextCompat.startForegroundService(contextOf(), 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().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().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(), DownloadService::class.java) + val intent = Intent(contextOf(), DownloadService::class.java) intent.action = DownloadService.ACTION_START_RANGE intent.putExtra(DownloadService.KEY_GID_LIST, gidList) - ContextCompat.startForegroundService(implicit(), intent) + ContextCompat.startForegroundService(contextOf(), intent) } onClick(Icons.Default.Pause) { val gidList = checkedInfoMap.takeAndClear().mapToLongArray(DownloadInfo::gid) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt index 083018972..1e8d43937 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt @@ -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 @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()).scaledTouchSlop + val slop = ViewConfiguration.get(contextOf()).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(), false, *info.toTypedArray()) + startDownload(false, *info.toTypedArray()) } } onClick(Icons.Default.Delete) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt index 06b597d77..a14c66ea7 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt @@ -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) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt index 06db76a8c..c79ac4c22 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt @@ -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(), 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.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, 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) } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt index 0440025e5..e660a9dd7 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt @@ -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().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(), galleryDetailUrl) + AppHelper.share(contextOf(), 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) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt index 243a42eec..471498dd5 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt @@ -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()) + val dialogState by rememberUpdatedState(contextOf()) 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()).scaledTouchSlop + val slop = ViewConfiguration.get(contextOf()).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.. { - val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(context) && EhTagDatabase.initialized + val canTranslate = Settings.showTagTranslations && EhTagDatabase.translatable && EhTagDatabase.initialized wrapTagKeyword(keyword, canTranslate) } else -> keyword diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/HistoryScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/HistoryScreen.kt index 2a017f2ac..5c127e7f0 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/HistoryScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/HistoryScreen.kt @@ -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 @Composable diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/ImageSearchScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/ImageSearchScreen.kt index 50eb6d5d5..ca1b6908a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/ImageSearchScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/ImageSearchScreen.kt @@ -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 @Composable @@ -72,7 +74,7 @@ fun AnimatedVisibilityScope.ImageSearchScreen(navigator: DestinationsNavigator) ) } } else { - launch { showSnackbar(selectImageFirst) } + launchSnackbar(selectImageFirst) } }, modifier = Modifier.snackBarPadding(), diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/MainNav.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/MainNav.kt index db1681ff1..d699601ab 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/MainNav.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/MainNav.kt @@ -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) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/ProgressScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/ProgressScreen.kt index d651380ba..34f276317 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/ProgressScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/ProgressScreen.kt @@ -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 @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() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt index 8bf1a20cb..dc6d8b0d0 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt @@ -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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AboutScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AboutScreen.kt index 40a5a9467..08b7422ea 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AboutScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AboutScreen.kt @@ -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 @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()) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt index 6d186b20b..187922f8f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt @@ -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() + 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 @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() - 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(), 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(), 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) + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/DownloadScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/DownloadScreen.kt index b5f9e0794..71d6b2e51 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/DownloadScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/DownloadScreen.kt @@ -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().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)) } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt index 30aa77b7c..4dd895a81 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt @@ -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 @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())) { + 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), diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/FilterScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/FilterScreen.kt index 4f8a61164..c951d316b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/FilterScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/FilterScreen.kt @@ -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 @Composable diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/LicenseScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/LicenseScreen.kt index 068f38b29..70e48076e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/LicenseScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/LicenseScreen.kt @@ -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) } }, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/MyTagsScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/MyTagsScreen.kt index 39e46aab2..7cdd15a9c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/MyTagsScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/MyTagsScreen.kt @@ -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) } }, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/PreferenceWidgets.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/PreferenceWidgets.kt index 096bfe71e..178145be1 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/PreferenceWidgets.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/PreferenceWidgets.kt @@ -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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/PrivacyScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/PrivacyScreen.kt index 708b5f704..111953de5 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/PrivacyScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/PrivacyScreen.kt @@ -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 @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) } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextToolbar.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextToolbar.kt index 714808f69..31cdacdf9 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextToolbar.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextToolbar.kt @@ -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): TextToolbar { var tfv by textFieldValue val view = LocalView.current - val context = LocalContext.current - val activity = remember { context.findActivity() } val coroutineScope = rememberCoroutineScope() val toolbar = remember { object : TextToolbar { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt index 2c17f72a6..4f33cbf5e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt @@ -82,7 +82,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign @@ -121,228 +120,288 @@ class DialogState(val mutex: MutatorMutex = MutatorMutex()) : MutableComposable @Composable fun rememberLocal() = remember { DialogState(mutex) } - context(BoxScope) + context(scope: BoxScope) @Composable - fun Place() = value?.let { it() } + fun Place() = value?.let { it(scope) } fun dismiss() { value = null } +} - suspend inline fun dialog(crossinline block: @Composable BoxScope.(CancellableContinuation) -> Unit) = mutex.mutate { - try { - suspendCancellableCoroutine { cont -> value = { block(cont) } } - } finally { - dismiss() +context(state: DialogState) +suspend inline fun dialog(crossinline block: @Composable BoxScope.(CancellableContinuation) -> Unit) = state.mutex.mutate { + try { + suspendCancellableCoroutine { cont -> state.value = { block(cont) } } + } finally { + state.dismiss() + } +} + +context(_: DialogState) +suspend fun awaitResult( + initial: R, + @StringRes title: Int? = null, + invalidator: (suspend Raise.(R) -> Unit)? = null, + block: @Composable DialogScope.(String?) -> Unit, +): R = dialog { cont -> + val state = remember(cont) { mutableStateOf(initial) } + var errorMsg by remember(cont) { mutableStateOf(null) } + val impl = remember(cont) { + object : DialogScope { + override var expectedValue by state } } - - suspend fun awaitResult( - initial: R, - @StringRes title: Int? = null, - invalidator: (suspend Raise.(R) -> Unit)? = null, - block: @Composable DialogScope.(String?) -> Unit, - ): R = dialog { cont -> - val state = remember(cont) { mutableStateOf(initial) } - var errorMsg by remember(cont) { mutableStateOf(null) } - val impl = remember(cont) { - object : DialogScope { - override var expectedValue by state + if (invalidator != null) { + LaunchedEffect(state) { + snapshotFlow { state.value }.collectLatest { + errorMsg = either { invalidator(it) }.leftOrNull() } } - if (invalidator != null) { - LaunchedEffect(state) { - snapshotFlow { state.value }.collectLatest { - errorMsg = either { invalidator(it) }.leftOrNull() - } - } - } - AlertDialog( - onDismissRequest = { cont.cancel() }, - confirmButton = { - TextButton(onClick = { - if (invalidator == null || errorMsg == null) { - cont.resume(state.value) - } - }) { - Text(text = stringResource(id = android.R.string.ok)) - } - }, - dismissButton = { - TextButton(onClick = { - cont.cancel() - }) { - Text(text = stringResource(id = android.R.string.cancel)) - } - }, - title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) }, - text = { block(impl, errorMsg) }, - ) } - - context(Context) - suspend fun awaitSelectTags(): List = dialog { cont -> - val selected = remember { mutableStateListOf() } - val state = rememberTextFieldState() - var suggestionTranslate by rememberMutableStateInDataStore("SuggestionTranslate") { false } - PausableAlertDialog( - confirmButton = { - TextButton( - onClick = { cont.resume(selected.toList()) }, - content = { Text(text = stringResource(id = android.R.string.ok)) }, - ) - }, - dismissButton = { - TextButton( - onClick = { cont.cancel() }, - content = { Text(text = stringResource(id = android.R.string.cancel)) }, - ) - }, - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - Text(text = stringResource(id = R.string.action_add_tag)) - val context = LocalContext.current - if (EhTagDatabase.isTranslatable(context)) { - Spacer(modifier = Modifier.weight(1f)) - Text( - text = stringResource(id = R.string.translate_tag_for_tagger), - style = MaterialTheme.typography.titleSmall, - ) - Checkbox( - checked = suggestionTranslate, - onCheckedChange = { suggestionTranslate = !suggestionTranslate }, - ) - } + AlertDialog( + onDismissRequest = { cont.cancel() }, + confirmButton = { + TextButton(onClick = { + if (invalidator == null || errorMsg == null) { + cont.resume(state.value) } - }, - text = { - Column { - FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - selected.forEach { text -> - InputChip( - selected = true, - onClick = { }, - label = { Text(text = text) }, - trailingIcon = { - Icon( - imageVector = Icons.Default.Close, - contentDescription = null, - modifier = Modifier.clickable { selected -= text }, - ) - }, - ) - } - } - var expanded by remember { mutableStateOf(false) } - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { if (!it) expanded = false }, - ) { - OutlinedTextField( - modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable), - state = state, - label = { Text(text = stringResource(id = R.string.action_add_tag_tip)) }, + }) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = { + cont.cancel() + }) { + Text(text = stringResource(id = android.R.string.cancel)) + } + }, + title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) }, + text = { block(impl, errorMsg) }, + ) +} + +context(ctx: Context, state: DialogState) +suspend fun awaitSelectTags(): List = dialog { cont -> + val selected = remember { mutableStateListOf() } + val state = rememberTextFieldState() + var suggestionTranslate by rememberMutableStateInDataStore("SuggestionTranslate") { false } + PausableAlertDialog( + confirmButton = { + TextButton( + onClick = { cont.resume(selected.toList()) }, + content = { Text(text = stringResource(id = android.R.string.ok)) }, + ) + }, + dismissButton = { + TextButton( + onClick = { cont.cancel() }, + content = { Text(text = stringResource(id = android.R.string.cancel)) }, + ) + }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = stringResource(id = R.string.action_add_tag)) + if (EhTagDatabase.translatable) { + Spacer(modifier = Modifier.weight(1f)) + Text( + text = stringResource(id = R.string.translate_tag_for_tagger), + style = MaterialTheme.typography.titleSmall, + ) + Checkbox( + checked = suggestionTranslate, + onCheckedChange = { suggestionTranslate = !suggestionTranslate }, + ) + } + } + }, + text = { + Column { + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + selected.forEach { text -> + InputChip( + selected = true, + onClick = { }, + label = { Text(text = text) }, trailingIcon = { - IconButton( - onClick = { - val text = state.text.toString().trim() - if (text.isNotEmpty()) { - selected += text - state.clearText() - } - }, - content = { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - ) - }, + Icon( + imageVector = Icons.Default.Close, + contentDescription = null, + modifier = Modifier.clickable { selected -= text }, ) }, ) - val query = state.text.toString().trim().takeIf { s -> s.isNotEmpty() } - var items by remember { mutableStateOf(emptyList>()) } - LaunchedEffect(suggestionTranslate, query) { - items = query?.let { suggestion(query, suggestionTranslate).take(15).toList() }.orEmpty() - expanded = items.isNotEmpty() - } - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = {}, - modifier = Modifier.heightIn(max = 192.dp), - ) { - items.forEach { (tag, hint) -> - DropdownMenuItem( - text = { - Column { - Text(text = tag, maxLines = 1) - ProvideTextStyle(MaterialTheme.typography.bodySmall) { - if (hint != null) { - Text( - text = hint, - maxLines = 1, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + } + } + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { if (!it) expanded = false }, + ) { + OutlinedTextField( + modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable), + state = state, + label = { Text(text = stringResource(id = R.string.action_add_tag_tip)) }, + trailingIcon = { + IconButton( + onClick = { + val text = state.text.toString().trim() + if (text.isNotEmpty()) { + selected += text + state.clearText() + } + }, + content = { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + ) + }, + ) + }, + ) + val query = state.text.toString().trim().takeIf { s -> s.isNotEmpty() } + var items by remember { mutableStateOf(emptyList>()) } + LaunchedEffect(suggestionTranslate, query) { + items = query?.let { suggestion(query, suggestionTranslate).take(15).toList() }.orEmpty() + expanded = items.isNotEmpty() + } + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = {}, + modifier = Modifier.heightIn(max = 192.dp), + ) { + items.forEach { (tag, hint) -> + DropdownMenuItem( + text = { + Column { + Text(text = tag, maxLines = 1) + ProvideTextStyle(MaterialTheme.typography.bodySmall) { + if (hint != null) { + Text( + text = hint, + maxLines = 1, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } - }, - onClick = { - if (tag.endsWith(':')) { - state.setTextAndPlaceCursorAtEnd(tag) - } else { - selected += tag - state.clearText() - } - }, - contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, - ) - } + } + }, + onClick = { + if (tag.endsWith(':')) { + state.setTextAndPlaceCursorAtEnd(tag) + } else { + selected += tag + state.clearText() + } + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + ) } } } - }, - idleIcon = Icons.Default.NewLabel, - ) - } + } + }, + idleIcon = Icons.Default.NewLabel, + ) +} - suspend fun awaitInputText( - initial: String = "", - title: String? = null, - hint: String? = null, - isNumber: Boolean = false, - @StringRes confirmText: Int = android.R.string.ok, - onUserDismiss: (() -> Unit)? = null, - invalidator: (suspend Raise.(String) -> Unit)? = null, - ) = dialog { cont -> - val coroutineScope = rememberCoroutineScope() - val state = rememberTextFieldState(initial) - var error by remember(cont) { mutableStateOf(null) } - AlertDialog( - onDismissRequest = { - cont.cancel() - onUserDismiss?.invoke() - }, - confirmButton = { - TextButton(onClick = { - val text = state.text.toString() - if (invalidator == null) { - cont.resume(text) - } else { - coroutineScope.launch { - error = either { invalidator(text) }.leftOrNull() - error ?: cont.resume(text) - } +context(_: DialogState) +suspend fun awaitInputText( + initial: String = "", + title: String? = null, + hint: String? = null, + isNumber: Boolean = false, + @StringRes confirmText: Int = android.R.string.ok, + onUserDismiss: (() -> Unit)? = null, + invalidator: (suspend Raise.(String) -> Unit)? = null, +) = dialog { cont -> + val coroutineScope = rememberCoroutineScope() + val state = rememberTextFieldState(initial) + var error by remember(cont) { mutableStateOf(null) } + AlertDialog( + onDismissRequest = { + cont.cancel() + onUserDismiss?.invoke() + }, + confirmButton = { + TextButton(onClick = { + val text = state.text.toString() + if (invalidator == null) { + cont.resume(text) + } else { + coroutineScope.launch { + error = either { invalidator(text) }.leftOrNull() + error ?: cont.resume(text) } - }) { - Text(text = stringResource(id = confirmText)) } - }, - title = title.ifNotNullThen { Text(text = title!!) }, - text = { + }) { + Text(text = stringResource(id = confirmText)) + } + }, + title = title.ifNotNullThen { Text(text = title!!) }, + text = { + OutlinedTextField( + state = state, + label = hint?.let { { Text(text = it) } }, + trailingIcon = error.ifNotNullThen { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = null, + ) + }, + supportingText = error.ifNotNullThen { + Text(text = error!!) + }, + isError = error != null, + keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default, + ) + }, + ) +} + +context(_: DialogState) +suspend fun awaitInputTextWithCheckBox( + initial: String = "", + @StringRes title: Int? = null, + @StringRes hint: Int? = null, + checked: Boolean, + @StringRes checkBoxText: Int, + isNumber: Boolean = false, + invalidator: (suspend Raise.(String, Boolean) -> Unit)? = null, +): Pair = dialog { cont -> + val coroutineScope = rememberCoroutineScope() + val state = rememberTextFieldState(initial) + var error by remember(cont) { mutableStateOf(null) } + var checkedState by remember { mutableStateOf(checked) } + AlertDialog( + onDismissRequest = { cont.cancel() }, + confirmButton = { + TextButton(onClick = { + val text = state.text.toString() + if (invalidator == null) { + cont.resume(text to checkedState) + } else { + coroutineScope.launch { + error = either { invalidator(text, checkedState) }.leftOrNull() + error ?: cont.resume(text to checkedState) + } + } + }) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { OutlinedTextField( state = state, - label = hint?.let { { Text(text = it) } }, + label = hint?.let { + { Text(text = stringResource(id = it)) } + }, trailingIcon = error.ifNotNullThen { Icon( imageVector = Icons.Filled.Info, @@ -355,381 +414,335 @@ class DialogState(val mutex: MutatorMutex = MutatorMutex()) : MutableComposable isError = error != null, keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default, ) - }, - ) - } + LabeledCheckbox( + modifier = Modifier.fillMaxWidth(), + checked = checkedState, + onCheckedChange = { checkedState = !checkedState }, + label = stringResource(checkBoxText), + ) + } + }, + ) +} - suspend fun awaitInputTextWithCheckBox( - initial: String = "", - @StringRes title: Int? = null, - @StringRes hint: Int? = null, - checked: Boolean, - @StringRes checkBoxText: Int, - isNumber: Boolean = false, - invalidator: (suspend Raise.(String, Boolean) -> Unit)? = null, - ): Pair = dialog { cont -> - val coroutineScope = rememberCoroutineScope() - val state = rememberTextFieldState(initial) - var error by remember(cont) { mutableStateOf(null) } - var checkedState by remember { mutableStateOf(checked) } - AlertDialog( - onDismissRequest = { cont.cancel() }, - confirmButton = { - TextButton(onClick = { - val text = state.text.toString() - if (invalidator == null) { - cont.resume(text to checkedState) - } else { - coroutineScope.launch { - error = either { invalidator(text, checkedState) }.leftOrNull() - error ?: cont.resume(text to checkedState) - } - } - }) { - Text(text = stringResource(id = android.R.string.ok)) +context(_: DialogState) +suspend fun awaitConfirmationOrCancel( + @StringRes confirmText: Int = android.R.string.ok, + @StringRes dismissText: Int = android.R.string.cancel, + @StringRes title: Int? = null, + showConfirmButton: Boolean = true, + showCancelButton: Boolean = true, + onCancelButtonClick: () -> Unit = {}, + secure: Boolean = false, + text: @Composable (() -> Unit)? = null, +) = dialog { cont -> + AlertDialog( + onDismissRequest = { cont.cancel() }, + confirmButton = { + if (showConfirmButton) { + TextButton(onClick = { cont.resume(Unit) }) { + Text(text = stringResource(id = confirmText)) } - }, - title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - OutlinedTextField( - state = state, - label = hint?.let { - { Text(text = stringResource(id = it)) } - }, - trailingIcon = error.ifNotNullThen { - Icon( - imageVector = Icons.Filled.Info, - contentDescription = null, - ) - }, - supportingText = error.ifNotNullThen { - Text(text = error!!) - }, - isError = error != null, - keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default, - ) - LabeledCheckbox( - modifier = Modifier.fillMaxWidth(), - checked = checkedState, - onCheckedChange = { checkedState = !checkedState }, - label = stringResource(checkBoxText), - ) - } - }, - ) - } + } + }, + dismissButton = showCancelButton.ifTrueThen { + TextButton(onClick = { + onCancelButtonClick() + cont.cancel() + }) { + Text(text = stringResource(id = dismissText)) + } + }, + title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) }, + text = text, + properties = if (secure) { + DialogProperties(securePolicy = SecureFlagPolicy.SecureOn) + } else { + DialogProperties() + }, + ) +} - suspend fun awaitConfirmationOrCancel( - @StringRes confirmText: Int = android.R.string.ok, - @StringRes dismissText: Int = android.R.string.cancel, - @StringRes title: Int? = null, - showConfirmButton: Boolean = true, - showCancelButton: Boolean = true, - onCancelButtonClick: () -> Unit = {}, - secure: Boolean = false, - text: @Composable (() -> Unit)? = null, - ) = dialog { cont -> - AlertDialog( - onDismissRequest = { cont.cancel() }, - confirmButton = { - if (showConfirmButton) { - TextButton(onClick = { cont.resume(Unit) }) { - Text(text = stringResource(id = confirmText)) - } - } - }, - dismissButton = showCancelButton.ifTrueThen { - TextButton(onClick = { - onCancelButtonClick() - cont.cancel() - }) { - Text(text = stringResource(id = dismissText)) - } - }, - title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) }, - text = text, - properties = if (secure) { - DialogProperties(securePolicy = SecureFlagPolicy.SecureOn) - } else { - DialogProperties() - }, - ) - } - - suspend fun awaitSelectDate( - @StringRes title: Int, - initialSelectedDateMillis: Long? = null, - initialDisplayedMonthMillis: Long? = initialSelectedDateMillis, - yearRange: IntRange = DatePickerDefaults.YearRange, - initialDisplayMode: DisplayMode = DisplayMode.Picker, - selectableDates: SelectableDates = DatePickerDefaults.AllDates, - showModeToggle: Boolean = true, - ): Long? = dialog { cont -> - val state = rememberDatePickerState( - initialSelectedDateMillis, - initialDisplayedMonthMillis, - yearRange, - initialDisplayMode, - selectableDates, - ) - DatePickerDialog( - onDismissRequest = { cont.cancel() }, - confirmButton = { - TextButton(onClick = { cont.resume(state.selectedDateMillis) }) { - Text(text = stringResource(id = android.R.string.ok)) - } - }, - dismissButton = { - TextButton(onClick = { cont.cancel() }) { - Text(text = stringResource(id = android.R.string.cancel)) - } - }, - ) { - DatePicker( - state = state, - title = { - Text( - text = stringResource(id = title), - modifier = Modifier.padding(DatePickerTitlePadding), - ) - }, - showModeToggle = showModeToggle, - ) - } - } - - suspend fun showNoButton(respectDefaultWidth: Boolean = true, block: @Composable Continuation.() -> Unit): R = dialog { cont -> - BasicAlertDialog( - onDismissRequest = { cont.cancel() }, - properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth), - content = { - Surface( - modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) }, - shape = AlertDialogDefaults.shape, - color = AlertDialogDefaults.containerColor, - tonalElevation = AlertDialogDefaults.TonalElevation, - content = { block(cont) }, +context(_: DialogState) +suspend fun awaitSelectDate( + @StringRes title: Int, + initialSelectedDateMillis: Long? = null, + initialDisplayedMonthMillis: Long? = initialSelectedDateMillis, + yearRange: IntRange = DatePickerDefaults.YearRange, + initialDisplayMode: DisplayMode = DisplayMode.Picker, + selectableDates: SelectableDates = DatePickerDefaults.AllDates, + showModeToggle: Boolean = true, +): Long? = dialog { cont -> + val state = rememberDatePickerState( + initialSelectedDateMillis, + initialDisplayedMonthMillis, + yearRange, + initialDisplayMode, + selectableDates, + ) + DatePickerDialog( + onDismissRequest = { cont.cancel() }, + confirmButton = { + TextButton(onClick = { cont.resume(state.selectedDateMillis) }) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = { cont.cancel() }) { + Text(text = stringResource(id = android.R.string.cancel)) + } + }, + ) { + DatePicker( + state = state, + title = { + Text( + text = stringResource(id = title), + modifier = Modifier.padding(DatePickerTitlePadding), ) }, + showModeToggle = showModeToggle, ) } +} - suspend fun awaitSelectTime( - title: String, - initialHour: Int, - initialMinute: Int, - ) = dialog { cont -> - val state = rememberTimePickerState(initialHour, initialMinute) - Dialog( - onDismissRequest = { cont.cancel() }, - properties = DialogProperties(usePlatformDefaultWidth = false), - ) { +context(_: DialogState) +suspend fun showNoButton(respectDefaultWidth: Boolean = true, block: @Composable Continuation.() -> Unit): R = dialog { cont -> + BasicAlertDialog( + onDismissRequest = { cont.cancel() }, + properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth), + content = { Surface( + modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) }, + shape = AlertDialogDefaults.shape, + color = AlertDialogDefaults.containerColor, + tonalElevation = AlertDialogDefaults.TonalElevation, + content = { block(cont) }, + ) + }, + ) +} + +context(_: DialogState) +suspend fun awaitSelectTime( + title: String, + initialHour: Int, + initialMinute: Int, +) = dialog { cont -> + val state = rememberTimePickerState(initialHour, initialMinute) + Dialog( + onDismissRequest = { cont.cancel() }, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface( + shape = MaterialTheme.shapes.extraLarge, + tonalElevation = 6.dp, + modifier = Modifier.width(IntrinsicSize.Min).height(IntrinsicSize.Min).background( shape = MaterialTheme.shapes.extraLarge, - tonalElevation = 6.dp, - modifier = Modifier.width(IntrinsicSize.Min).height(IntrinsicSize.Min).background( - shape = MaterialTheme.shapes.extraLarge, - color = MaterialTheme.colorScheme.surface, - ), + color = MaterialTheme.colorScheme.surface, + ), + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp), - text = title, - style = MaterialTheme.typography.labelMedium, - ) - TimePicker(state = state) - Row(modifier = Modifier.height(40.dp).fillMaxWidth()) { - Spacer(modifier = Modifier.weight(1f)) - TextButton(onClick = { cont.cancel() }) { - Text(stringResource(id = android.R.string.cancel)) - } - TextButton(onClick = { cont.resume(state.hour to state.minute) }) { - Text(stringResource(id = android.R.string.ok)) - } + Text( + modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp), + text = title, + style = MaterialTheme.typography.labelMedium, + ) + TimePicker(state = state) + Row(modifier = Modifier.height(40.dp).fillMaxWidth()) { + Spacer(modifier = Modifier.weight(1f)) + TextButton(onClick = { cont.cancel() }) { + Text(stringResource(id = android.R.string.cancel)) + } + TextButton(onClick = { cont.resume(state.hour to state.minute) }) { + Text(stringResource(id = android.R.string.ok)) } } } } } +} - suspend fun awaitSingleChoice( - items: List, - selected: Int, - @StringRes title: Int? = null, - ): Int = showNoButton { - Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(vertical = 8.dp)) { - title?.let { - Text( - text = stringResource(id = it), - modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), - style = MaterialTheme.typography.headlineSmall, - ) - } - items.forEachIndexed { index, text -> - Row( - modifier = Modifier.clickable { resume(index) }.fillMaxWidth().padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - RadioButton(selected = index == selected, onClick = { resume(index) }) - Text(text = text) - } - } - } - } - - suspend fun awaitSelectItem( - items: List, - @StringRes title: Int? = null, - selected: Int = -1, - respectDefaultWidth: Boolean = true, - ) = awaitSelectItem(items, title?.right(), selected, respectDefaultWidth) - - suspend fun awaitSelectItem( - items: List, - title: Either?, - selected: Int = -1, - respectDefaultWidth: Boolean = true, - ): Int = showNoButton(respectDefaultWidth) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - if (title != null) { - Text( - text = title.fold({ it }, { stringResource(id = it) }), - modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), - style = MaterialTheme.typography.headlineSmall, - ) - } - FastScrollLazyColumn { - itemsIndexed(items) { index, text -> - CheckableItem( - text = text, - checked = index == selected, - modifier = Modifier.fillMaxWidth().clickable { resume(index) }.padding(horizontal = 8.dp), - ) - } - } - } - } - - suspend inline fun awaitSelectAction( - @StringRes title: Int? = null, - selected: Int = -1, - builder: ActionScope.() -> Unit, - ): suspend () -> Unit { - val (items, actions) = buildList { builder { action, that -> add(action to that) } }.unzip() - val index = awaitSelectItem(items, title, selected) - return actions[index] - } - - suspend fun awaitSelectItemWithCheckBox( - items: List, - @StringRes title: Int, - @StringRes checkBoxText: Int, - selected: Int = -1, - initialChecked: Boolean = false, - ): Pair = showNoButton { - var checked by remember { mutableStateOf(initialChecked) } - Column(modifier = Modifier.padding(vertical = 8.dp)) { +context(_: DialogState) +suspend fun awaitSingleChoice( + items: List, + selected: Int, + @StringRes title: Int? = null, +): Int = showNoButton { + Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(vertical = 8.dp)) { + title?.let { Text( - text = stringResource(id = title), + text = stringResource(id = it), modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), style = MaterialTheme.typography.headlineSmall, ) - FastScrollLazyColumn { - itemsIndexed(items) { index, text -> - CheckableItem( - text = text, - checked = index == selected, - modifier = Modifier.fillMaxWidth().clickable { resume(index to checked) }.padding(horizontal = 8.dp), - ) - } + } + items.forEachIndexed { index, text -> + Row( + modifier = Modifier.clickable { resume(index) }.fillMaxWidth().padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton(selected = index == selected, onClick = { resume(index) }) + Text(text = text) } - LabeledCheckbox( - modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), - checked = checked, - onCheckedChange = { checked = !checked }, - label = stringResource(checkBoxText), + } + } +} + +context(_: DialogState) +suspend fun awaitSelectItem( + items: List, + @StringRes title: Int? = null, + selected: Int = -1, + respectDefaultWidth: Boolean = true, +) = awaitSelectItem(items, title?.right(), selected, respectDefaultWidth) + +context(_: DialogState) +suspend fun awaitSelectItem( + items: List, + title: Either?, + selected: Int = -1, + respectDefaultWidth: Boolean = true, +): Int = showNoButton(respectDefaultWidth) { + Column(modifier = Modifier.padding(vertical = 8.dp)) { + if (title != null) { + Text( + text = title.fold({ it }, { stringResource(id = it) }), + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + style = MaterialTheme.typography.headlineSmall, + ) + } + FastScrollLazyColumn { + itemsIndexed(items) { index, text -> + CheckableItem( + text = text, + checked = index == selected, + modifier = Modifier.fillMaxWidth().clickable { resume(index) }.padding(horizontal = 8.dp), + ) + } + } + } +} + +context(_: DialogState) +suspend inline fun awaitSelectAction( + @StringRes title: Int? = null, + selected: Int = -1, + builder: ActionScope.() -> Unit, +): suspend () -> Unit { + val (items, actions) = buildList { builder { action, that -> add(action to that) } }.unzip() + val index = awaitSelectItem(items, title, selected) + return actions[index] +} + +context(_: DialogState) +suspend fun awaitSelectItemWithCheckBox( + items: List, + @StringRes title: Int, + @StringRes checkBoxText: Int, + selected: Int = -1, + initialChecked: Boolean = false, +): Pair = showNoButton { + var checked by remember { mutableStateOf(initialChecked) } + Column(modifier = Modifier.padding(vertical = 8.dp)) { + Text( + text = stringResource(id = title), + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + style = MaterialTheme.typography.headlineSmall, + ) + FastScrollLazyColumn { + itemsIndexed(items) { index, text -> + CheckableItem( + text = text, + checked = index == selected, + modifier = Modifier.fillMaxWidth().clickable { resume(index to checked) }.padding(horizontal = 8.dp), + ) + } + } + LabeledCheckbox( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), + checked = checked, + onCheckedChange = { checked = !checked }, + label = stringResource(checkBoxText), + ) + } +} + +context(_: DialogState) +suspend fun awaitSelectItemWithIcon( + items: List>, + title: String, +): Int = showNoButton { + LazyColumn { + stickyHeader { + Text(text = title, modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium) + } + itemsIndexed(items) { index, (icon, text) -> + ListItem( + headlineContent = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) }, + modifier = Modifier.clickable { resume(index) }.padding(horizontal = 8.dp), + leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), ) } } +} - suspend fun awaitSelectItemWithIcon( - items: List>, - title: String, - ): Int = showNoButton { - LazyColumn { - stickyHeader { - Text(text = title, modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium) - } - itemsIndexed(items) { index, (icon, text) -> - ListItem( - headlineContent = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) }, - modifier = Modifier.clickable { resume(index) }.padding(horizontal = 8.dp), - leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - ) - } - } - } - - suspend fun awaitSelectItemWithIconAndTextField( - items: List>, - @StringRes title: Int, - @StringRes hint: Int, - initialNote: String, - maxChar: Int, - ): Pair = showNoButton(false) { - Column { - Text(text = stringResource(id = title), modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium) - CircularLayout( - modifier = Modifier.fillMaxWidth().aspectRatio(1F), - placeFirstItemInCenter = true, - ) { - val note = rememberTextFieldState(initialNote) - TextField( - state = note, - modifier = Modifier.fillMaxWidth(0.45F).aspectRatio(1F), - label = { Text(text = stringResource(id = hint)) }, - trailingIcon = { - if (note.text.isNotEmpty()) { - IconButton(onClick = { note.clearText() }) { - Icon(imageVector = Icons.Default.Close, contentDescription = null) - } +context(_: DialogState) +suspend fun awaitSelectItemWithIconAndTextField( + items: List>, + @StringRes title: Int, + @StringRes hint: Int, + initialNote: String, + maxChar: Int, +): Pair = showNoButton(false) { + Column { + Text(text = stringResource(id = title), modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium) + CircularLayout( + modifier = Modifier.fillMaxWidth().aspectRatio(1F), + placeFirstItemInCenter = true, + ) { + val note = rememberTextFieldState(initialNote) + TextField( + state = note, + modifier = Modifier.fillMaxWidth(0.45F).aspectRatio(1F), + label = { Text(text = stringResource(id = hint)) }, + trailingIcon = { + if (note.text.isNotEmpty()) { + IconButton(onClick = { note.clearText() }) { + Icon(imageVector = Icons.Default.Close, contentDescription = null) } - }, - supportingText = { - Text( - text = "${note.text.toString().toByteArray().size} / $maxChar", - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.End, - ) - }, - shape = ShapeDefaults.ExtraSmall, - colors = TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - ) - items.forEachIndexed { index, (icon, text) -> - Column( - modifier = Modifier.clip(IconWithTextCorner).clickable { resume(index to note.text.toString()) }.fillMaxWidth(0.2F), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) - Text( - text = text, - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - style = MaterialTheme.typography.bodySmall, - ) } + }, + supportingText = { + Text( + text = "${note.text.toString().toByteArray().size} / $maxChar", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.End, + ) + }, + shape = ShapeDefaults.ExtraSmall, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + items.forEachIndexed { index, (icon, text) -> + Column( + modifier = Modifier.clip(IconWithTextCorner).clickable { resume(index to note.text.toString()) }.fillMaxWidth(0.2F), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) + Text( + text = text, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + style = MaterialTheme.typography.bodySmall, + ) } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SharedElementTransition.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SharedElementTransition.kt index 8bae823cb..d039aab7a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SharedElementTransition.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SharedElementTransition.kt @@ -25,33 +25,35 @@ val NoopTransitionsVisibilityScope = TransitionsVisibilityScope(emptySet()) @Stable value class TransitionsVisibilityScope(val scopes: Set) -context(TransitionsVisibilityScope) +context(visScope: TransitionsVisibilityScope) @Composable -inline fun togetherWith(scope: AnimatedVisibilityScope, block: @Composable TransitionsVisibilityScope.() -> T) = block(remember(scopes, scope) { TransitionsVisibilityScope(scopes + scope) }) +inline fun 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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/ClipboardUtil.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/ClipboardUtil.kt index 820c39f9b..587c3f1c6 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/ClipboardUtil.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/ClipboardUtil.kt @@ -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() - activity.showTip(R.string.copied_to_clipboard, useToast) + with(findActivity()) { + tip(R.string.copied_to_clipboard, useToast) + } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/ContextUtil.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/ContextUtil.kt index de9785e65..9d18a4e64 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/ContextUtil.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/ContextUtil.kt @@ -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 Context.findActivity(): T { - var context = this +context(ctx: Context) +inline fun 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 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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/LanguageUtils.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/LanguageUtils.kt index 662a59cd2..95a6b8693 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/LanguageUtils.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/LanguageUtils.kt @@ -6,7 +6,8 @@ import androidx.core.os.LocaleListCompat import com.hippo.ehviewer.R import org.xmlpull.v1.XmlPullParser -fun Context.getLanguages(): Map { +context(ctx: Context) +fun getLanguages(): Map = 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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/LauncherUtils.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/LauncherUtils.kt index c265a1d56..493e23a3f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/LauncherUtils.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/LauncherUtils.kt @@ -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 Context.awaitActivityResult(contract: ActivityResultContract, input: I): O { +context(ctx: Context) +suspend fun awaitActivityResult(contract: ActivityResultContract, input: I): O { val key = "activity_rq#${atomicInteger.fetchAndIncrement()}" var launcher: ActivityResultLauncher? = null var observer: LifecycleEventObserver? = null @@ -71,15 +73,18 @@ suspend fun 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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/Progress.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/Progress.kt index b9775a20f..280a9cd48 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/Progress.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/Progress.kt @@ -46,14 +46,15 @@ fun ProgressDialog() { } } -suspend fun DialogState.bgWork(work: suspend () -> R) = mutex.mutate { +context(state: DialogState) +suspend fun 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() } } diff --git a/app/src/main/kotlin/moe/tarsin/ContextBridges.kt b/app/src/main/kotlin/moe/tarsin/ContextBridges.kt new file mode 100644 index 000000000..ce43a00a2 --- /dev/null +++ b/app/src/main/kotlin/moe/tarsin/ContextBridges.kt @@ -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 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) } diff --git a/app/src/main/kotlin/moe/tarsin/coroutines/Result.kt b/app/src/main/kotlin/moe/tarsin/coroutines/Result.kt index 45b2a599f..9c33b4e30 100644 --- a/app/src/main/kotlin/moe/tarsin/coroutines/Result.kt +++ b/app/src/main/kotlin/moe/tarsin/coroutines/Result.kt @@ -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 Result.except() = onFailure { if (it is E) throw it } @@ -12,5 +12,5 @@ inline fun runSuspendCatching(block: () -> R) = runCatching(block).except T.runSuspendCatching(block: T.() -> R) = runCatching(block).except() -context(SnackbarHostState, CoroutineScope) -inline fun runSwallowingWithUI(block: () -> R) = runSuspendCatching(block).onFailure { e -> launch { showSnackbar(e.displayString()) } } +context(state: SnackbarHostState, scope: CoroutineScope) +inline fun runSwallowingWithUI(block: () -> R) = runSuspendCatching(block).onFailure { e -> launchSnackbar(e.displayString()) }