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