Initial draft support export as cbz (#685)
* Initial draft support export as cbz * Hint user progress * Hint user result * Emit comicInfo as well * Write ComicInfo if not exist * Use perFilename * Print stacktrace * Fix * Refine * Revert "Refine" This reverts commit 2b368180fde00c7a8cfd4e895f1f2d5c29c3cf9f. * Refine * Refine * Fix --------- Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
1 parent
576e5b1b58
commit
21016dda53
9 files changed
+201
-66
No files matched your search
@@ -296,7 +296,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
|
||||
jlong size) {
|
||||
jlong size) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
archive_ctx *ctx = NULL;
|
||||
@@ -376,7 +376,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
|
||||
jint index) {
|
||||
jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||
@@ -432,7 +432,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
jstring str) {
|
||||
jstring str) {
|
||||
EH_UNUSED(thiz);
|
||||
struct archive_entry *entry;
|
||||
archive_ctx *ctx;
|
||||
@@ -443,7 +443,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
LOGE("Allocate passwd buffer failed");
|
||||
return false;
|
||||
}
|
||||
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
|
||||
(*env)->GetStringUTFRegion(env, str, 0, len, passwd);
|
||||
archive_alloc_ctx(&ctx);
|
||||
void *tmpBuf = alloca(4096);
|
||||
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
|
||||
@@ -463,7 +463,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_getExtension(JNIEnv *env, jclass thiz,
|
||||
jint index) {
|
||||
jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
const char *ext = strrchr(entries[index].filename, '.') + 1;
|
||||
@@ -472,7 +472,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_getExtension(JNIEnv *env, jclass thiz,
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
||||
jint index, jint fd) {
|
||||
jint index, jint fd) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
@@ -488,9 +488,44 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
|
||||
jobject buffer) {
|
||||
jobject buffer) {
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
mempool_release_pages(addr, size);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_archiveFdBatch(JNIEnv *env, jclass clazz, jintArray fd_batch,
|
||||
jobjectArray names, jint arc_fd, jint size) {
|
||||
struct archive *arc = archive_write_new();
|
||||
struct stat st;
|
||||
char buff[8192];
|
||||
jint fdBatch[size];
|
||||
(*env)->GetIntArrayRegion(env, fd_batch, 0, size, fdBatch);
|
||||
archive_write_set_format_zip(arc);
|
||||
archive_write_zip_set_compression_store(arc);
|
||||
archive_write_open_fd(arc, arc_fd);
|
||||
struct archive_entry *entry = archive_entry_new();
|
||||
for (int i = 0; i < size; i++) {
|
||||
int fd = fdBatch[i];
|
||||
jobject name = (*env)->GetObjectArrayElement(env, names, i);
|
||||
const char *cname = (*env)->GetStringUTFChars(env, name, false);
|
||||
archive_entry_set_pathname(entry, cname);
|
||||
(*env)->ReleaseStringUTFChars(env, name, cname);
|
||||
fstat(fd, &st);
|
||||
archive_entry_copy_stat(entry, &st);
|
||||
archive_entry_set_perm(entry, 0644);
|
||||
archive_write_header(arc, entry);
|
||||
int len = read(fd, buff, sizeof(buff));
|
||||
while (len > 0) {
|
||||
archive_write_data(arc, buff, len);
|
||||
len = read(fd, buff, sizeof(buff));
|
||||
}
|
||||
archive_write_finish_entry(arc);
|
||||
archive_entry_clear(entry);
|
||||
}
|
||||
archive_write_close(arc);
|
||||
archive_write_free(arc);
|
||||
return true;
|
||||
}
|
||||
@@ -10,3 +10,4 @@ external fun getExtension(index: Int): String
|
||||
external fun needPassword(): Boolean
|
||||
external fun providePassword(str: String): Boolean
|
||||
external fun closeArchive()
|
||||
external fun archiveFdBatch(fdBatch: IntArray, names: Array<String>, arcFd: Int, size: Int): Boolean
|
||||
@@ -15,8 +15,13 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import arrow.fx.coroutines.autoCloseable
|
||||
import arrow.fx.coroutines.closeable
|
||||
import arrow.fx.coroutines.parMap
|
||||
import arrow.fx.coroutines.resourceScope
|
||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.ehRequest
|
||||
@@ -25,11 +30,13 @@ import com.hippo.ehviewer.coil.read
|
||||
import com.hippo.ehviewer.coil.suspendEdit
|
||||
import com.hippo.ehviewer.download.downloadLocation
|
||||
import com.hippo.ehviewer.image.UniFileSource
|
||||
import com.hippo.ehviewer.jni.archiveFdBatch
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.openOutputStream
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import io.ktor.client.plugins.onDownload
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
@@ -39,8 +46,8 @@ import io.ktor.utils.io.jvm.nio.copyTo
|
||||
import java.util.Locale
|
||||
import kotlin.io.path.readText
|
||||
|
||||
class SpiderDen(mGalleryInfo: GalleryInfo) {
|
||||
private val mGid = mGalleryInfo.gid
|
||||
class SpiderDen(val info: GalleryInfo) {
|
||||
private val gid = info.gid
|
||||
var downloadDir: UniFile? = null
|
||||
|
||||
@Volatile
|
||||
@@ -51,12 +58,12 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
|
||||
suspend fun setMode(value: Int) {
|
||||
mode = value
|
||||
if (mode == SpiderQueen.MODE_DOWNLOAD && downloadDir == null) {
|
||||
downloadDir = getGalleryDownloadDir(mGid)?.takeIf { it.ensureDir() }
|
||||
downloadDir = getGalleryDownloadDir(gid)?.takeIf { it.ensureDir() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun containInCache(index: Int): Boolean {
|
||||
val key = getImageKey(mGid, index)
|
||||
val key = getImageKey(gid, index)
|
||||
return sCache.read(key) { true } ?: false
|
||||
}
|
||||
|
||||
@@ -67,7 +74,7 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
|
||||
|
||||
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
|
||||
val dir = downloadDir ?: return false
|
||||
val key = getImageKey(mGid, index)
|
||||
val key = getImageKey(gid, index)
|
||||
return runCatching {
|
||||
sCache.read(key) {
|
||||
val extension = "." + metadata.toFile().readText()
|
||||
@@ -97,7 +104,7 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
|
||||
}
|
||||
|
||||
private fun removeFromCache(index: Int): Boolean {
|
||||
val key = getImageKey(mGid, index)
|
||||
val key = getImageKey(gid, index)
|
||||
return sCache.remove(key)
|
||||
}
|
||||
|
||||
@@ -146,7 +153,7 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
|
||||
|
||||
// Read Mode, allow save to cache
|
||||
if (mode == SpiderQueen.MODE_READ) {
|
||||
val key = getImageKey(mGid, index)
|
||||
val key = getImageKey(gid, index)
|
||||
return sCache.suspendEdit(key) {
|
||||
metadata.toFile().writeText(ext)
|
||||
fops(data.asUniFile())
|
||||
@@ -167,7 +174,7 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
|
||||
}
|
||||
|
||||
fun saveToUniFile(index: Int, file: UniFile): Boolean {
|
||||
val key = getImageKey(mGid, index)
|
||||
val key = getImageKey(gid, index)
|
||||
|
||||
// Read from diskCache first
|
||||
sCache.read(key) {
|
||||
@@ -195,14 +202,14 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
|
||||
}
|
||||
|
||||
fun getExtension(index: Int): String? {
|
||||
val key = getImageKey(mGid, index)
|
||||
val key = getImageKey(gid, index)
|
||||
return sCache.read(key) { metadata.toNioPath().readText() }
|
||||
?: downloadDir?.let { findImageFile(it, index) }?.name.let { FileUtils.getExtensionFromFilename(it) }
|
||||
}
|
||||
|
||||
fun getImageSource(index: Int): UniFileSource? {
|
||||
if (mode == SpiderQueen.MODE_READ) {
|
||||
val key = getImageKey(mGid, index)
|
||||
val key = getImageKey(gid, index)
|
||||
val snapshot = sCache.openSnapshot(key)
|
||||
if (snapshot != null) {
|
||||
return object : UniFileSource, AutoCloseable by snapshot {
|
||||
@@ -218,6 +225,41 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
|
||||
override fun close() {}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun exportAsCbz(file: UniFile) = resourceScope {
|
||||
val pages = info.pages
|
||||
val comicInfo = closeable {
|
||||
val f = downloadDir!! / COMIC_INFO_FILE
|
||||
if (!f.exists()) writeComicInfo()
|
||||
f.openFileDescriptor("r")
|
||||
}
|
||||
val (fdBatch, names) = (0 until pages).parMap { idx ->
|
||||
val ext = requireNotNull(getExtension(idx))
|
||||
val f = autoCloseable { requireNotNull(getImageSource(idx)) }
|
||||
closeable { f.source.openFileDescriptor("r") }.fd to perFilename(idx, ".$ext")
|
||||
}.run { plus(comicInfo.fd to COMIC_INFO_FILE) }.unzip()
|
||||
val arcFd = closeable { file.openFileDescriptor("rw") }
|
||||
archiveFdBatch(fdBatch.toIntArray(), names.toTypedArray(), arcFd.fd, pages + 1)
|
||||
}
|
||||
|
||||
suspend fun initDownloadDirIfExist() {
|
||||
downloadDir = getGalleryDownloadDir(gid)?.takeIf { it.isDirectory }
|
||||
}
|
||||
|
||||
suspend fun writeComicInfo() {
|
||||
downloadDir?.run {
|
||||
createFile(COMIC_INFO_FILE)?.also {
|
||||
runCatching {
|
||||
withNonCancellableContext {
|
||||
EhEngine.fillGalleryListByApi(listOf(info))
|
||||
}
|
||||
info.getComicInfo().write(it)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,7 +41,6 @@ import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -227,7 +226,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
private val prepareJob = launchIO { doPrepare() }
|
||||
|
||||
private suspend fun doPrepare() {
|
||||
mSpiderDen.downloadDir = getGalleryDownloadDir(galleryInfo.gid)?.takeIf { it.isDirectory }
|
||||
mSpiderDen.initDownloadDirIfExist()
|
||||
mSpiderInfo = readSpiderInfoFromLocal() ?: readSpiderInfoFromInternet() ?: return
|
||||
mPageStateArray = IntArray(mSpiderInfo.pages)
|
||||
notifyGetPages(mSpiderInfo.pages)
|
||||
@@ -426,21 +425,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
mSpiderInfo.saveToCache()
|
||||
}
|
||||
|
||||
private suspend fun writeComicInfo() {
|
||||
mSpiderDen.downloadDir?.run {
|
||||
createFile(COMIC_INFO_FILE)?.also {
|
||||
runCatching {
|
||||
withNonCancellableContext {
|
||||
EhEngine.fillGalleryListByApi(listOf(galleryInfo))
|
||||
}
|
||||
galleryInfo.getComicInfo().write(it)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isStateDone(state: Int): Boolean {
|
||||
return state == STATE_FINISHED || state == STATE_FAILED
|
||||
}
|
||||
@@ -507,7 +491,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
// Will create download dir if not exists
|
||||
updateMode()
|
||||
if (mode == MODE_DOWNLOAD) {
|
||||
writeComicInfo()
|
||||
mSpiderDen.writeComicInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.os.Parcelable
|
||||
import android.text.TextUtils.TruncateAt.END
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
@@ -118,6 +119,7 @@ import com.hippo.ehviewer.databinding.DialogArchiveListBinding
|
||||
import com.hippo.ehviewer.databinding.DialogTorrentListBinding
|
||||
import com.hippo.ehviewer.download.DownloadManager as EhDownloadManager
|
||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||
import com.hippo.ehviewer.spider.SpiderDen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.MODE_READ
|
||||
import com.hippo.ehviewer.ui.GalleryInfoBottomSheet
|
||||
@@ -154,9 +156,12 @@ import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.awaitActivityResult
|
||||
import com.hippo.ehviewer.util.bgWork
|
||||
import com.hippo.ehviewer.util.findActivity
|
||||
import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.requestPermission
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
@@ -1214,6 +1219,43 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
context.openBrowser(galleryDetailUrl)
|
||||
},
|
||||
)
|
||||
val exportSuccess = stringResource(id = R.string.export_as_archive_success)
|
||||
val exportFailed = stringResource(id = R.string.export_as_archive_failed)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.export_as_archive)) },
|
||||
onClick = {
|
||||
dropdown = false
|
||||
coroutineScope.launchIO {
|
||||
val canExport = EhDownloadManager.getDownloadState(gid) == DownloadInfo.STATE_FINISH
|
||||
if (!canExport) {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
showCancelButton = false,
|
||||
text = { Text(text = stringResource(id = R.string.download_gallery_first)) },
|
||||
)
|
||||
} else {
|
||||
val info = galleryInfo!!
|
||||
val uri = with(context) {
|
||||
awaitActivityResult(
|
||||
CreateDocument("application/x-cbz"),
|
||||
EhUtils.getSuitableTitle(info),
|
||||
)
|
||||
}
|
||||
if (uri != null) {
|
||||
val success = dialogState.bgWork {
|
||||
withIOContext {
|
||||
SpiderDen(info).run {
|
||||
initDownloadDirIfExist()
|
||||
exportAsCbz(uri.asUniFile())
|
||||
}
|
||||
}
|
||||
}
|
||||
val msg = if (success) exportSuccess else exportFailed
|
||||
snackbarState.showSnackbar(message = msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -11,13 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -38,12 +33,11 @@ import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import arrow.atomic.Atomic
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.ui.destinations.DirectionDestination
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.ui.settings.PreferenceTokens.PreferenceTextPadding
|
||||
import com.hippo.ehviewer.util.ProgressDialog
|
||||
import com.jamal.composeprefs3.ui.prefs.DropDownPref
|
||||
import com.jamal.composeprefs3.ui.prefs.DropDownPrefInt
|
||||
import com.jamal.composeprefs3.ui.prefs.SliderPref
|
||||
@@ -157,30 +151,6 @@ fun SimpleMenuPreference(title: String, @ArrayRes entry: Int, @ArrayRes entryVal
|
||||
DropDownPref(title = title, defaultValue = v, onValueChange = ::set, useSelectedAsSummary = true, entries = map)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressDialog() {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { },
|
||||
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.width(280.dp),
|
||||
shape = AlertDialogDefaults.shape,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(18.dp),
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.size(18.dp))
|
||||
Text(text = stringResource(id = R.string.please_wait))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WorkPreference(title: String, summary: String? = null, work: suspend CoroutineScope.() -> Unit) {
|
||||
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.hippo.ehviewer.R.string
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
|
||||
@Composable
|
||||
fun ProgressDialog() {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { },
|
||||
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.width(280.dp),
|
||||
shape = AlertDialogDefaults.shape,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(18.dp),
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.size(18.dp))
|
||||
Text(text = stringResource(id = string.please_wait))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <R> DialogState.bgWork(work: suspend () -> R) = dialog { cont ->
|
||||
ProgressDialog()
|
||||
LaunchedEffect(work) {
|
||||
val result = runCatching { work() }.onFailure { it.printStackTrace() }
|
||||
cont.resumeWith(result)
|
||||
}
|
||||
}
|
||||
@@ -509,6 +509,10 @@
|
||||
<string name="new_version_available">有新版本!</string>
|
||||
<string name="already_latest_version">已是最新版本</string>
|
||||
<string name="permission_denied">权限被拒</string>
|
||||
<string name="download_gallery_first">请先下载画廊!</string>
|
||||
<string name="export_as_archive">导出为归档文件</string>
|
||||
<string name="export_as_archive_success">导出成功</string>
|
||||
<string name="export_as_archive_failed">导出失败</string>
|
||||
<!-- Tachiyomi Reader Start-->
|
||||
<string name="action_settings">设置</string>
|
||||
<string name="action_retry">重试</string>
|
||||
|
||||
@@ -573,6 +573,10 @@
|
||||
<string name="new_version_available">New version available!</string>
|
||||
<string name="already_latest_version">Already the latest version</string>
|
||||
<string name="permission_denied">Permission denied</string>
|
||||
<string name="download_gallery_first">Please download the gallery first!</string>
|
||||
<string name="export_as_archive">Export as archive</string>
|
||||
<string name="export_as_archive_success">Export succeed</string>
|
||||
<string name="export_as_archive_failed">Export failed</string>
|
||||
<!-- Tachiyomi Reader Start-->
|
||||
<string name="pref_crop_borders">Crop borders</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
|
||||
Reference in new issue
Block a user