[New Reader] Implement open archive intent
This commit is contained in:
1 parent
7f8e4aa003
commit
02e36e7021
15 files changed
+311
-215
No files matched your search
@@ -70,26 +70,6 @@
|
||||
<data android:host="exhentai.org" />
|
||||
<data android:host="e-hentai.org" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/keyword_search">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/image_search">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.ui.reader.ReaderActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -123,6 +103,25 @@
|
||||
<data android:mimeType="application/x-lzma-compressed-tar" />
|
||||
<data android:mimeType="application/x-xz-compressed-tar" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/keyword_search">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/image_search">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.ui.reader.ReaderActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:launchMode="singleTask">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SecurityActivity"
|
||||
|
||||
@@ -35,6 +35,7 @@ import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -54,21 +55,19 @@ class ArchivePageLoader(
|
||||
passwdProvider: PasswdProvider? = null,
|
||||
) : PageLoader2(gid, startPage) {
|
||||
private lateinit var pfd: ParcelFileDescriptor
|
||||
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
||||
private val hostJob = async(start = CoroutineStart.LAZY) {
|
||||
logcat(DEBUG_TAG) { "Open archive ${file.toUri().displayPath}" }
|
||||
pfd = file.openFileDescriptor("r")
|
||||
size = openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name.endsWith(".zip"))
|
||||
if (size == 0) {
|
||||
return@launch
|
||||
}
|
||||
if (passwdProvider != null && needPassword()) {
|
||||
val size = openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name.endsWith(".zip"))
|
||||
if (size != 0 && passwdProvider != null && needPassword()) {
|
||||
archivePasswds?.forEach {
|
||||
it ?: return@forEach
|
||||
if (providePassword(it)) return@launch
|
||||
if (providePassword(it)) return@async size
|
||||
}
|
||||
val toAdd = passwdProvider { providePassword(it) }
|
||||
archivePasswds = archivePasswds?.toMutableSet()?.apply { add(toAdd) } ?: setOf(toAdd)
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
override var size = 0
|
||||
@@ -136,8 +135,8 @@ class ArchivePageLoader(
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
hostJob.join()
|
||||
return super.awaitReady() && size != 0
|
||||
size = hostJob.await()
|
||||
return super.awaitReady() && isReady
|
||||
}
|
||||
|
||||
override val isReady: Boolean
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
@@ -23,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.Path
|
||||
@@ -31,21 +31,19 @@ abstract class PageLoader2(private val gid: Long, var startPage: Int) :
|
||||
PageLoader(),
|
||||
CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
private val progressJob = launch(start = CoroutineStart.LAZY) {
|
||||
private val progressJob = async(start = CoroutineStart.LAZY) {
|
||||
if (startPage == -1) {
|
||||
startPage = EhDB.getReadProgress(gid)
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
progressJob.start()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
progressJob.join()
|
||||
return startPage != -1
|
||||
progressJob.await()
|
||||
return super.awaitReady() && startPage != -1
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
|
||||
@@ -50,6 +50,7 @@ import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -224,7 +225,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
check(!(mReadReference < 0 || mDownloadReference < 0)) { "Mode reference < 0" }
|
||||
}
|
||||
|
||||
private val prepareJob = launch { doPrepare() }
|
||||
private val prepareJob = async { doPrepare() }
|
||||
private val archiveJob = launch(start = CoroutineStart.LAZY) { mSpiderDen.archive() }
|
||||
|
||||
private suspend fun doPrepare() {
|
||||
@@ -235,7 +236,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
}
|
||||
|
||||
suspend fun awaitReady(): Boolean {
|
||||
prepareJob.join()
|
||||
prepareJob.await()
|
||||
return isReady
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.ui
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -54,6 +55,7 @@ import com.hippo.ehviewer.download.downloadDir
|
||||
import com.hippo.ehviewer.download.downloadLocation
|
||||
import com.hippo.ehviewer.download.tempDownloadDir
|
||||
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.util.FavouriteStatusRouter
|
||||
@@ -286,7 +288,7 @@ fun navToReader(
|
||||
page: Int = -1,
|
||||
) {
|
||||
if (Settings.newReader) {
|
||||
navigate(ReaderScreenDestination(info, page))
|
||||
navigate(ReaderScreenDestination(ReaderScreenArgs.Gallery(info, page)))
|
||||
} else {
|
||||
val intent = Intent(this@Context, ReaderActivity::class.java)
|
||||
intent.action = ReaderActivity.ACTION_EH
|
||||
@@ -296,6 +298,17 @@ fun navToReader(
|
||||
}
|
||||
}
|
||||
|
||||
fun DestinationsNavigator.navToReader(context: Context, uri: Uri) {
|
||||
if (Settings.newReader) {
|
||||
navigate(ReaderScreenDestination(ReaderScreenArgs.Archive(uri)))
|
||||
} else {
|
||||
val intent = Intent(context, ReaderActivity::class.java)
|
||||
intent.action = Intent.ACTION_VIEW
|
||||
intent.data = uri
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
context(DialogState, Context, DestinationsNavigator)
|
||||
suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
|
||||
val downloaded = DownloadManager.getDownloadState(info.gid) != DownloadInfo.STATE_INVALID
|
||||
|
||||
@@ -17,6 +17,8 @@ package com.hippo.ehviewer.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.ContentResolver.SCHEME_CONTENT
|
||||
import android.content.ContentResolver.SCHEME_FILE
|
||||
import android.content.Intent
|
||||
import android.content.pm.verify.domain.DomainVerificationManager
|
||||
import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE
|
||||
@@ -265,10 +267,19 @@ class MainActivity : EhActivity() {
|
||||
intentFlow.collect { intent ->
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW -> {
|
||||
val url = intent.data?.toString()
|
||||
if (url != null && !navigator.navWithUrl(url)) {
|
||||
val new = dialogState.awaitInputText(initial = url, title = cannotParse)
|
||||
addTextToClipboard(new)
|
||||
val uri = intent.data ?: return@collect
|
||||
when (uri.scheme) {
|
||||
SCHEME_CONTENT, SCHEME_FILE -> {
|
||||
navigator.navToReader(applicationContext, uri)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val url = uri.toString()
|
||||
if (!navigator.navWithUrl(url)) {
|
||||
val new = dialogState.awaitInputText(initial = url, title = cannotParse)
|
||||
addTextToClipboard(new)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEND -> {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.hippo.ehviewer.ui.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
@@ -21,6 +24,7 @@ import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -37,8 +41,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.ensure
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -48,8 +55,12 @@ import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.download.archiveFile
|
||||
import com.hippo.ehviewer.gallery.ArchivePageLoader
|
||||
import com.hippo.ehviewer.gallery.EhPageLoader
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.ui.composing
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.hippo.ehviewer.ui.tools.Await
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.files.toOkioPath
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
@@ -63,11 +74,51 @@ import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import moe.tarsin.kt.unreachable
|
||||
|
||||
sealed interface ReaderScreenArgs : Parcelable {
|
||||
@Parcelize
|
||||
data class Gallery(val info: BaseGalleryInfo, val page: Int) : ReaderScreenArgs
|
||||
|
||||
@Parcelize
|
||||
data class Archive(val uri: Uri) : ReaderScreenArgs
|
||||
}
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
fun ReaderScreen(args: ReaderScreenArgs, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
Await({ preparePageLoader(args) }) { pageLoader ->
|
||||
DisposableEffect(pageLoader) {
|
||||
pageLoader.start()
|
||||
onDispose {
|
||||
pageLoader.stop()
|
||||
}
|
||||
}
|
||||
val readingFailed = stringResource(R.string.error_reading_failed)
|
||||
Await({
|
||||
Either.catch {
|
||||
readingFailed.takeUnless { pageLoader.awaitReady() }
|
||||
}.fold({ it.displayString() }, { it })
|
||||
}) { error ->
|
||||
if (error == null) {
|
||||
val info = (args as? ReaderScreenArgs.Gallery)?.info
|
||||
ReaderScreen(pageLoader, info, navigator)
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = error,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReaderScreen(pageLoader: PageLoader2, info: BaseGalleryInfo?, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
ConfigureKeepScreenOn()
|
||||
LaunchedEffect(Unit) {
|
||||
val orientation = requestedOrientation
|
||||
@@ -75,21 +126,11 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
||||
.onCompletion { requestedOrientation = orientation }
|
||||
.collect { setOrientation(it) }
|
||||
}
|
||||
val pageLoader = remember {
|
||||
val archive = DownloadManager.getDownloadInfo(info.gid)?.archiveFile
|
||||
archive?.let { ArchivePageLoader(it, info.gid, page) } ?: EhPageLoader(info, page)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(pageLoader) {
|
||||
Settings.cropBorder.changesFlow().collect {
|
||||
pageLoader.restart()
|
||||
}
|
||||
}
|
||||
DisposableEffect(pageLoader) {
|
||||
pageLoader.start()
|
||||
onDispose {
|
||||
pageLoader.stop()
|
||||
}
|
||||
}
|
||||
val showSeekbar by Settings.showReaderSeekbar.collectAsState()
|
||||
val readingMode by Settings.readingMode.collectAsState { ReadingModeType.fromPreference(it) }
|
||||
val volumeKeysEnabled by Settings.readWithVolumeKeys.collectAsState()
|
||||
@@ -103,139 +144,163 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
||||
uiController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||
}
|
||||
}
|
||||
Deferred({ pageLoader.awaitReady() }) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val pagerState = rememberPagerState { pageLoader.size }
|
||||
val syncState = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
|
||||
Box {
|
||||
var appbarVisible by remember { mutableStateOf(false) }
|
||||
val bgColor by collectBackgroundColorAsState()
|
||||
syncState.Sync(ReadingModeType.isWebtoon(readingMode)) { appbarVisible = false }
|
||||
if (fullscreen) {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { appbarVisible }.collect {
|
||||
uiController.isSystemBarsVisible = it
|
||||
}
|
||||
val lazyListState = rememberLazyListState()
|
||||
val pagerState = rememberPagerState { pageLoader.size }
|
||||
val syncState = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
|
||||
Box {
|
||||
var appbarVisible by remember { mutableStateOf(false) }
|
||||
val bgColor by collectBackgroundColorAsState()
|
||||
syncState.Sync(ReadingModeType.isWebtoon(readingMode)) { appbarVisible = false }
|
||||
if (fullscreen) {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { appbarVisible }.collect {
|
||||
uiController.isSystemBarsVisible = it
|
||||
}
|
||||
}
|
||||
VolumeKeysHandler(
|
||||
enabled = { volumeKeysEnabled && !appbarVisible },
|
||||
movePrevious = { syncState.sliderScrollTo(syncState.sliderValue - 1) },
|
||||
moveNext = { syncState.sliderScrollTo(syncState.sliderValue + 1) },
|
||||
)
|
||||
GalleryPager(
|
||||
type = readingMode,
|
||||
pagerState = pagerState,
|
||||
lazyListState = lazyListState,
|
||||
pageLoader = pageLoader,
|
||||
onSelectPage = { page ->
|
||||
if (Settings.readerLongTapAction.value) {
|
||||
launch {
|
||||
dialog { cont ->
|
||||
fun dispose() = cont.resume(Unit)
|
||||
val state = rememberModalBottomSheetState()
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { dispose() },
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
|
||||
sheetState = state,
|
||||
contentWindowInsets = { WindowInsets(0) },
|
||||
) {
|
||||
ReaderPageSheetMeta(
|
||||
retry = { pageLoader.retryPage(page.index) },
|
||||
retryOrigin = { pageLoader.retryPage(page.index, true) },
|
||||
share = { launchIO { with(pageLoader) { shareImage(page, info) } } },
|
||||
copy = { launchIO { with(pageLoader) { copy(page) } } },
|
||||
save = { launchIO { with(pageLoader) { save(page) } } },
|
||||
saveTo = { launchIO { with(pageLoader) { saveTo(page) } } },
|
||||
dismiss = { launch { state.hide().also { dispose() } } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onMenuRegionClick = { appbarVisible = !appbarVisible },
|
||||
modifier = Modifier.background(bgColor),
|
||||
contentPadding = if (fullscreen) {
|
||||
if (cutoutShort) {
|
||||
PaddingValues(0.dp)
|
||||
} else {
|
||||
WindowInsets.displayCutout.asPaddingValues()
|
||||
}
|
||||
} else {
|
||||
WindowInsets.systemBars.asPaddingValues()
|
||||
},
|
||||
)
|
||||
val brightness by Settings.customBrightness.collectAsState()
|
||||
val brightnessValue by Settings.customBrightnessValue.collectAsState()
|
||||
val colorOverlayEnabled by Settings.colorFilter.collectAsState()
|
||||
val colorOverlay by Settings.colorFilterValue.collectAsState()
|
||||
val colorOverlayMode by Settings.colorFilterMode.collectAsState {
|
||||
when (it) {
|
||||
0 -> BlendMode.SrcOver
|
||||
1 -> BlendMode.Multiply
|
||||
2 -> BlendMode.Screen
|
||||
3 -> BlendMode.Overlay
|
||||
4 -> BlendMode.Lighten
|
||||
5 -> BlendMode.Darken
|
||||
else -> unreachable()
|
||||
}
|
||||
}
|
||||
ReaderContentOverlay(
|
||||
brightness = { brightnessValue }.takeIf { brightness && brightnessValue < 0 },
|
||||
color = { colorOverlay }.takeIf { colorOverlayEnabled },
|
||||
colorBlendMode = colorOverlayMode,
|
||||
)
|
||||
if (brightness) {
|
||||
LaunchedEffect(Unit) {
|
||||
Settings.customBrightnessValue.valueFlow().sample(100)
|
||||
.onCompletion { setCustomBrightnessValue(0) }
|
||||
.collect { setCustomBrightnessValue(it) }
|
||||
}
|
||||
}
|
||||
val showPageNumber by Settings.showPageNumber.collectAsState()
|
||||
if (showPageNumber && !appbarVisible) {
|
||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
|
||||
PageIndicatorText(
|
||||
currentPage = syncState.sliderValue,
|
||||
totalPages = pageLoader.size,
|
||||
modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
ReaderAppBars(
|
||||
visible = appbarVisible,
|
||||
isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT,
|
||||
showSeekBar = showSeekbar,
|
||||
currentPage = syncState.sliderValue,
|
||||
totalPages = pageLoader.size,
|
||||
onSliderValueChange = { syncState.sliderScrollTo(it + 1) },
|
||||
onClickSettings = {
|
||||
}
|
||||
VolumeKeysHandler(
|
||||
enabled = { volumeKeysEnabled && !appbarVisible },
|
||||
movePrevious = { syncState.sliderScrollTo(syncState.sliderValue - 1) },
|
||||
moveNext = { syncState.sliderScrollTo(syncState.sliderValue + 1) },
|
||||
)
|
||||
GalleryPager(
|
||||
type = readingMode,
|
||||
pagerState = pagerState,
|
||||
lazyListState = lazyListState,
|
||||
pageLoader = pageLoader,
|
||||
onSelectPage = { page ->
|
||||
if (Settings.readerLongTapAction.value) {
|
||||
launch {
|
||||
dialog { cont ->
|
||||
fun dispose() = cont.resume(Unit)
|
||||
var isColorFilter by remember { mutableStateOf(false) }
|
||||
val scrim by animateColorAsState(
|
||||
targetValue = if (isColorFilter) Color.Transparent else BottomSheetDefaults.ScrimColor,
|
||||
label = "ScrimColor",
|
||||
)
|
||||
val state = rememberModalBottomSheetState()
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { dispose() },
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
|
||||
// Yeah, I know color state should not be read here, but we have to do it...
|
||||
scrimColor = scrim,
|
||||
dragHandle = null,
|
||||
sheetState = state,
|
||||
contentWindowInsets = { WindowInsets(0) },
|
||||
) {
|
||||
SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
|
||||
isColorFilter = page == 2
|
||||
appbarVisible = !isColorFilter
|
||||
}
|
||||
ReaderPageSheetMeta(
|
||||
retry = { pageLoader.retryPage(page.index) },
|
||||
retryOrigin = { pageLoader.retryPage(page.index, true) },
|
||||
share = { launchIO { with(pageLoader) { shareImage(page, info) } } },
|
||||
copy = { launchIO { with(pageLoader) { copy(page) } } },
|
||||
save = { launchIO { with(pageLoader) { save(page) } } },
|
||||
saveTo = { launchIO { with(pageLoader) { saveTo(page) } } },
|
||||
dismiss = { launch { state.hide().also { dispose() } } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
onMenuRegionClick = { appbarVisible = !appbarVisible },
|
||||
modifier = Modifier.background(bgColor),
|
||||
contentPadding = if (fullscreen) {
|
||||
if (cutoutShort) {
|
||||
PaddingValues(0.dp)
|
||||
} else {
|
||||
WindowInsets.displayCutout.asPaddingValues()
|
||||
}
|
||||
} else {
|
||||
WindowInsets.systemBars.asPaddingValues()
|
||||
},
|
||||
)
|
||||
val brightness by Settings.customBrightness.collectAsState()
|
||||
val brightnessValue by Settings.customBrightnessValue.collectAsState()
|
||||
val colorOverlayEnabled by Settings.colorFilter.collectAsState()
|
||||
val colorOverlay by Settings.colorFilterValue.collectAsState()
|
||||
val colorOverlayMode by Settings.colorFilterMode.collectAsState {
|
||||
when (it) {
|
||||
0 -> BlendMode.SrcOver
|
||||
1 -> BlendMode.Multiply
|
||||
2 -> BlendMode.Screen
|
||||
3 -> BlendMode.Overlay
|
||||
4 -> BlendMode.Lighten
|
||||
5 -> BlendMode.Darken
|
||||
else -> unreachable()
|
||||
}
|
||||
}
|
||||
ReaderContentOverlay(
|
||||
brightness = { brightnessValue }.takeIf { brightness && brightnessValue < 0 },
|
||||
color = { colorOverlay }.takeIf { colorOverlayEnabled },
|
||||
colorBlendMode = colorOverlayMode,
|
||||
)
|
||||
if (brightness) {
|
||||
LaunchedEffect(Unit) {
|
||||
Settings.customBrightnessValue.valueFlow().sample(100)
|
||||
.onCompletion { setCustomBrightnessValue(0) }
|
||||
.collect { setCustomBrightnessValue(it) }
|
||||
}
|
||||
}
|
||||
val showPageNumber by Settings.showPageNumber.collectAsState()
|
||||
if (showPageNumber && !appbarVisible) {
|
||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
|
||||
PageIndicatorText(
|
||||
currentPage = syncState.sliderValue,
|
||||
totalPages = pageLoader.size,
|
||||
modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
ReaderAppBars(
|
||||
visible = appbarVisible,
|
||||
isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT,
|
||||
showSeekBar = showSeekbar,
|
||||
currentPage = syncState.sliderValue,
|
||||
totalPages = pageLoader.size,
|
||||
onSliderValueChange = { syncState.sliderScrollTo(it + 1) },
|
||||
onClickSettings = {
|
||||
launch {
|
||||
dialog { cont ->
|
||||
fun dispose() = cont.resume(Unit)
|
||||
var isColorFilter by remember { mutableStateOf(false) }
|
||||
val scrim by animateColorAsState(
|
||||
targetValue = if (isColorFilter) Color.Transparent else BottomSheetDefaults.ScrimColor,
|
||||
label = "ScrimColor",
|
||||
)
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { dispose() },
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
|
||||
// Yeah, I know color state should not be read here, but we have to do it...
|
||||
scrimColor = scrim,
|
||||
dragHandle = null,
|
||||
contentWindowInsets = { WindowInsets(0) },
|
||||
) {
|
||||
SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
|
||||
isColorFilter = page == 2
|
||||
appbarVisible = !isColorFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context(Context, DialogState, DestinationsNavigator)
|
||||
private suspend fun preparePageLoader(args: ReaderScreenArgs) = when (args) {
|
||||
is ReaderScreenArgs.Gallery -> {
|
||||
val info = args.info
|
||||
val page = args.page
|
||||
val archive = DownloadManager.getDownloadInfo(info.gid)?.archiveFile
|
||||
if (archive != null) {
|
||||
ArchivePageLoader(archive, info.gid, page)
|
||||
} else {
|
||||
EhPageLoader(info, page)
|
||||
}
|
||||
}
|
||||
is ReaderScreenArgs.Archive -> {
|
||||
ArchivePageLoader(args.uri.toOkioPath()) { invalidator ->
|
||||
awaitInputText(
|
||||
title = getString(R.string.archive_need_passwd),
|
||||
hint = getString(R.string.archive_passwd),
|
||||
onUserDismiss = { popBackStack() },
|
||||
) { text ->
|
||||
ensure(text.isNotBlank()) { getString(R.string.passwd_cannot_be_empty) }
|
||||
ensure(invalidator(text)) { getString(R.string.passwd_wrong) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
|
||||
import com.hippo.ehviewer.ui.main.plus
|
||||
import com.hippo.ehviewer.ui.navToReader
|
||||
import com.hippo.ehviewer.ui.showMoveDownloadLabelList
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.hippo.ehviewer.ui.tools.Await
|
||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
|
||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid
|
||||
import com.hippo.ehviewer.ui.tools.HapticFeedbackType
|
||||
@@ -599,7 +599,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
}
|
||||
}
|
||||
|
||||
Deferred({ delay(200) }) {
|
||||
Await({ delay(200) }) {
|
||||
if (list.isEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(realPadding).fillMaxSize(),
|
||||
|
||||
@@ -124,7 +124,7 @@ import com.hippo.ehviewer.ui.main.GalleryInfoListItem
|
||||
import com.hippo.ehviewer.ui.main.GalleryList
|
||||
import com.hippo.ehviewer.ui.main.ImageSearch
|
||||
import com.hippo.ehviewer.ui.main.SearchFilter
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.hippo.ehviewer.ui.tools.Await
|
||||
import com.hippo.ehviewer.ui.tools.HapticFeedbackType
|
||||
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
|
||||
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
||||
@@ -467,7 +467,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
||||
}
|
||||
}
|
||||
}
|
||||
Deferred({ delay(200) }) {
|
||||
Await({ delay(200) }) {
|
||||
if (quickSearchList.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.quick_search_tip),
|
||||
|
||||
@@ -52,7 +52,7 @@ import com.hippo.ehviewer.ui.composing
|
||||
import com.hippo.ehviewer.ui.doGalleryInfoAction
|
||||
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
|
||||
import com.hippo.ehviewer.ui.main.plus
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.hippo.ehviewer.ui.tools.Await
|
||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
|
||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.ui.tools.thenIf
|
||||
@@ -164,7 +164,7 @@ fun HistoryScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Deferred(keyword, { delay(200) }) {
|
||||
Await(keyword, { delay(200) }) {
|
||||
if (historyData.itemCount == 0) {
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues).padding(horizontal = marginH).fillMaxSize(),
|
||||
|
||||
@@ -58,7 +58,7 @@ import com.hippo.ehviewer.client.EhFilter.trigger
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.ehviewer.dao.FilterMode
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.hippo.ehviewer.ui.tools.Await
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.thenIf
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
@@ -213,7 +213,7 @@ fun FilterScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
Deferred({ allFilterMap.await() }) { filters ->
|
||||
Await({ allFilterMap.await() }) { filters ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
contentPadding = paddingValues,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.hippo.ehviewer.ui.tools
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import arrow.core.None
|
||||
import arrow.core.Option
|
||||
import arrow.core.some
|
||||
|
||||
@Composable
|
||||
fun <T> Await(block: suspend () -> T, content: @Composable (T) -> Unit) {
|
||||
var completed by remember(block) { mutableStateOf<Option<T>>(None) }
|
||||
LaunchedEffect(block) {
|
||||
completed = block().some()
|
||||
}
|
||||
completed.onSome { content(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> Await(key: Any?, block: suspend () -> T, content: @Composable (T) -> Unit) {
|
||||
var completed by remember(key, block) { mutableStateOf<Option<T>>(None) }
|
||||
LaunchedEffect(key, block) {
|
||||
completed = block().some()
|
||||
}
|
||||
completed.onSome { content(it) }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.hippo.ehviewer.ui.tools
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
@Composable
|
||||
fun <T> Deferred(block: suspend () -> T, content: @Composable (T) -> Unit) {
|
||||
var completed by remember(block) { mutableStateOf<T?>(null) }
|
||||
LaunchedEffect(block) {
|
||||
completed = block()
|
||||
}
|
||||
completed?.let { content(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> Deferred(key: Any?, block: suspend () -> T, content: @Composable (T) -> Unit) {
|
||||
var completed by remember(key, block) { mutableStateOf<T?>(null) }
|
||||
LaunchedEffect(key, block) {
|
||||
completed = block()
|
||||
}
|
||||
completed?.let { content(it) }
|
||||
}
|
||||
@@ -156,13 +156,17 @@ class DialogState {
|
||||
hint: String? = null,
|
||||
isNumber: Boolean = false,
|
||||
@StringRes confirmText: Int = android.R.string.ok,
|
||||
onUserDismiss: (() -> Unit)? = null,
|
||||
invalidator: (suspend Raise<String>.(String) -> Unit)? = null,
|
||||
) = dialog { cont ->
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var state by remember(cont) { mutableStateOf(initial) }
|
||||
var error by remember(cont) { mutableStateOf<String?>(null) }
|
||||
AlertDialog(
|
||||
onDismissRequest = { cont.cancel() },
|
||||
onDismissRequest = {
|
||||
cont.cancel()
|
||||
onUserDismiss?.invoke()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
if (invalidator == null) {
|
||||
|
||||
@@ -12,7 +12,6 @@ private const val MAX_CACHE_SIZE = 512 * 1024 * 1024
|
||||
private const val MIN_CACHE_SIZE = 128 * 1024 * 1024
|
||||
|
||||
abstract class PageLoader {
|
||||
|
||||
private val cache by lazy {
|
||||
lruCache<Int, Image>(
|
||||
maxSize = if (isAtLeastO) {
|
||||
@@ -27,6 +26,7 @@ abstract class PageLoader {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val pages by lazy {
|
||||
check(size > 0)
|
||||
(0 until size).map { ReaderPage(it) }
|
||||
@@ -34,13 +34,16 @@ abstract class PageLoader {
|
||||
|
||||
private val preloads = com.hippo.ehviewer.Settings.preloadImage.coerceIn(0, 100)
|
||||
|
||||
abstract suspend fun awaitReady(): Boolean
|
||||
@CallSuper
|
||||
open suspend fun awaitReady(): Boolean {
|
||||
cache
|
||||
return true
|
||||
}
|
||||
|
||||
abstract val isReady: Boolean
|
||||
|
||||
@CallSuper
|
||||
open fun start() {
|
||||
cache
|
||||
}
|
||||
abstract fun start()
|
||||
|
||||
@CallSuper
|
||||
open fun stop() {
|
||||
|
||||
Reference in new issue
Block a user