Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e90555bed | ||
|
|
d36c189430 | ||
|
|
61e2ace5c8 | ||
|
|
d6a3f21754 | ||
|
|
4e54d7f66f | ||
|
|
f2dff038cd | ||
|
|
a7b30640e0 | ||
|
|
45048b1f46 | ||
|
|
498129314a | ||
|
|
3ee4b4a61e | ||
|
|
ee59c0f22e | ||
|
|
c56134cd92 | ||
|
|
b3d1b66a06 | ||
|
|
c9bb81bf63 | ||
|
|
d176810dae | ||
|
|
17bdca71b1 | ||
|
|
22fd3730df | ||
|
|
23d1271949 |
No files matched your search
@@ -65,7 +65,7 @@ android {
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 180045
|
||||
versionName = "1.8.11.0"
|
||||
versionName = "1.8.11.1"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
|
||||
@@ -24,7 +24,7 @@ set(BUILD_TESTING 0)
|
||||
|
||||
FetchContent_Declare(liblzma
|
||||
GIT_REPOSITORY https://github.com/FooIbar/xz.git
|
||||
GIT_TAG v5.4.4
|
||||
GIT_TAG v5.4.5
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(liblzma)
|
||||
|
||||
@@ -34,7 +34,6 @@ import com.hippo.ehviewer.client.parser.GalleryApiParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryListParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageApiParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
@@ -366,7 +365,7 @@ object EhEngine {
|
||||
pToken: String,
|
||||
showKey: String?,
|
||||
previousPToken: String?,
|
||||
): GalleryPageApiParser.Result {
|
||||
): GalleryPageParser.Result {
|
||||
val referer = if (index > 0 && previousPToken != null) EhUrl.getPageUrl(gid, index - 1, previousPToken) else null
|
||||
return ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
@@ -376,7 +375,7 @@ object EhEngine {
|
||||
put("imgkey", pToken)
|
||||
put("showkey", showKey)
|
||||
}
|
||||
}.executeAndParsingWith(GalleryPageApiParser::parse)
|
||||
}.executeAndParsingWith { GalleryPageParser.parse(filterNot { it == '\\' }) }
|
||||
}
|
||||
|
||||
suspend fun rateGallery(
|
||||
|
||||
@@ -33,7 +33,7 @@ object GalleryApiParser {
|
||||
titleJpn = item.titleJpn.unescapeXml()
|
||||
category = getCategory(item.category)
|
||||
thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!)
|
||||
uploader = item.uploader.unescapeXml()
|
||||
uploader = item.uploader?.unescapeXml()
|
||||
posted = ParserUtils.formatDate(item.posted * 1000)
|
||||
rating = item.rating
|
||||
simpleTags = item.tags
|
||||
@@ -54,7 +54,8 @@ object GalleryApiParser {
|
||||
val titleJpn: String,
|
||||
val category: String,
|
||||
val thumb: String,
|
||||
val uploader: String,
|
||||
// Null in some old galleries
|
||||
val uploader: String?,
|
||||
val posted: Long,
|
||||
@SerialName("filecount")
|
||||
val pages: Int,
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.parseAs
|
||||
import com.hippo.ehviewer.util.unescapeXml
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object GalleryPageApiParser {
|
||||
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
|
||||
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
|
||||
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
|
||||
|
||||
fun parse(body: String) = runCatching {
|
||||
val res = body.parseAs<JsonResult>()
|
||||
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
|
||||
groupValues[1].trim().unescapeXml()
|
||||
}
|
||||
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKeyAndOriginImageUrl)?.run {
|
||||
groupValues[1].trim().unescapeXml()
|
||||
}
|
||||
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.skipHathKeyAndOriginImageUrl)?.run {
|
||||
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
|
||||
}
|
||||
check(!imageUrl.isNullOrEmpty())
|
||||
Result(imageUrl, skipHathKey, originImageUrl)
|
||||
}.getOrElse { e ->
|
||||
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error) }
|
||||
throw ParseException("Parse image url and skip hath key error", e)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class JsonResult(
|
||||
@SerialName("i3")
|
||||
val imageUrl: String,
|
||||
@SerialName("i6")
|
||||
val skipHathKeyAndOriginImageUrl: String,
|
||||
)
|
||||
|
||||
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
|
||||
}
|
||||
@@ -17,46 +17,32 @@ package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.util.unescapeXml
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object GalleryPageParser {
|
||||
private val PATTERN_IMAGE_URL = Pattern.compile("<img[^>]*src=\"([^\"]+)\" style")
|
||||
private val PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^)]+)'\\)")
|
||||
private val PATTERN_ORIGIN_IMAGE_URL =
|
||||
Pattern.compile("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
|
||||
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
|
||||
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([\\d-]+)'\\)")
|
||||
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+/fullimg/[^\"]+)\">")
|
||||
|
||||
// TODO Not sure about the size of show keys
|
||||
private val PATTERN_SHOW_KEY = Pattern.compile("var showkey=\"([0-9a-z]+)\";")
|
||||
private val PATTERN_SHOW_KEY = Regex("var showkey=\"([0-9a-z]+)\";")
|
||||
|
||||
fun parse(body: String): Result {
|
||||
var m = PATTERN_IMAGE_URL.matcher(body)
|
||||
val imageUrl = if (m.find()) {
|
||||
m.group(1)!!.trim().unescapeXml()
|
||||
} else {
|
||||
null
|
||||
val imageUrl = PATTERN_IMAGE_URL.find(body)?.run {
|
||||
groupValues[1].unescapeXml()
|
||||
}
|
||||
m = PATTERN_SKIP_HATH_KEY.matcher(body)
|
||||
val skipHathKey = if (m.find()) {
|
||||
m.group(1)!!.trim().unescapeXml()
|
||||
} else {
|
||||
null
|
||||
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(body)?.run {
|
||||
groupValues[1]
|
||||
}
|
||||
m = PATTERN_ORIGIN_IMAGE_URL.matcher(body)
|
||||
val originImageUrl = if (m.find()) {
|
||||
m.group(1)!!.unescapeXml() + "fullimg.php" + m.group(2)!!.unescapeXml()
|
||||
} else {
|
||||
null
|
||||
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(body)?.run {
|
||||
groupValues[1].unescapeXml()
|
||||
}
|
||||
m = PATTERN_SHOW_KEY.matcher(body)
|
||||
val showKey = if (m.find()) {
|
||||
m.group(1)
|
||||
} else {
|
||||
null
|
||||
val showKey = PATTERN_SHOW_KEY.find(body)?.run {
|
||||
groupValues[1]
|
||||
}
|
||||
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {
|
||||
return if (!imageUrl.isNullOrEmpty()) {
|
||||
Result(imageUrl, skipHathKey, originImageUrl, showKey)
|
||||
} else {
|
||||
throw ParseException("Parse image url and show error")
|
||||
throw ParseException("Parse image url error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +50,6 @@ object GalleryPageParser {
|
||||
val imageUrl: String,
|
||||
val skipHathKey: String?,
|
||||
val originImageUrl: String?,
|
||||
val showKey: String,
|
||||
val showKey: String?,
|
||||
)
|
||||
}
|
||||
@@ -43,14 +43,16 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import moe.tarsin.coroutines.NamedMutex
|
||||
import moe.tarsin.coroutines.withLock
|
||||
|
||||
class ArchivePageLoader(context: Context, private val uri: Uri, passwdFlow: Flow<String>) : PageLoader2(), CoroutineScope {
|
||||
typealias PasswdInvalidator = (String) -> Boolean
|
||||
typealias PasswdProvider = suspend (PasswdInvalidator) -> String
|
||||
|
||||
class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider: PasswdProvider) : PageLoader2(), CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
private lateinit var pfd: ParcelFileDescriptor
|
||||
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
||||
@@ -65,12 +67,8 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdFlow: Flow
|
||||
it ?: return@forEach
|
||||
if (providePassword(it)) return@launch
|
||||
}
|
||||
passwdFlow.collect {
|
||||
if (providePassword(it)) {
|
||||
archivePasswds = archivePasswds?.toMutableSet()?.apply { add(it) } ?: setOf(it)
|
||||
currentCoroutineContext().cancel()
|
||||
}
|
||||
}
|
||||
val toAdd = passwdProvider { providePassword(it) }
|
||||
archivePasswds = archivePasswds?.toMutableSet()?.apply { add(toAdd) } ?: setOf(toAdd)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
|
||||
import com.hippo.ehviewer.ui.scene.ProgressScene
|
||||
import com.hippo.ehviewer.ui.scene.navAnimated
|
||||
import com.hippo.ehviewer.ui.scene.navWithUrl
|
||||
import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.buildWindowInsets
|
||||
@@ -242,71 +243,73 @@ class MainActivity : EhActivity() {
|
||||
BackHandler(drawerState.isOpen) {
|
||||
closeDrawer()
|
||||
}
|
||||
ModalNavigationDrawer(
|
||||
drawerContent = {
|
||||
ModalDrawerSheet(
|
||||
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 56).dp),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(scrollState).navigationBarsPadding(),
|
||||
LocalTouchSlopProvider(3f) {
|
||||
ModalNavigationDrawer(
|
||||
drawerContent = {
|
||||
ModalDrawerSheet(
|
||||
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 56).dp),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.sadpanda_low_poly),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
navItems.forEach { (id, stringId, icon) ->
|
||||
NavigationDrawerItem(
|
||||
label = {
|
||||
Text(text = stringResource(id = stringId))
|
||||
},
|
||||
selected = isSelected(id),
|
||||
onClick = {
|
||||
closeDrawer()
|
||||
onNavDestinationSelected2(id, navController)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
icon = {
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
},
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(scrollState).navigationBarsPadding(),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.sadpanda_low_poly),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
navItems.forEach { (id, stringId, icon) ->
|
||||
NavigationDrawerItem(
|
||||
label = {
|
||||
Text(text = stringResource(id = stringId))
|
||||
},
|
||||
selected = isSelected(id),
|
||||
onClick = {
|
||||
closeDrawer()
|
||||
onNavDestinationSelected2(id, navController)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
icon = {
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = !drawerLocked || drawerState.isOpen,
|
||||
) {
|
||||
val insets = buildWindowInsets {
|
||||
set(
|
||||
WindowInsetsCompat.Type.statusBars(),
|
||||
WindowInsets.statusBars,
|
||||
)
|
||||
set(
|
||||
WindowInsetsCompat.Type.navigationBars(),
|
||||
WindowInsets.navigationBars,
|
||||
)
|
||||
set(
|
||||
WindowInsetsCompat.Type.ime(),
|
||||
WindowInsets.ime,
|
||||
)
|
||||
}
|
||||
},
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = !drawerLocked || drawerState.isOpen,
|
||||
) {
|
||||
val insets = buildWindowInsets {
|
||||
set(
|
||||
WindowInsetsCompat.Type.statusBars(),
|
||||
WindowInsets.statusBars,
|
||||
)
|
||||
set(
|
||||
WindowInsetsCompat.Type.navigationBars(),
|
||||
WindowInsets.navigationBars,
|
||||
)
|
||||
set(
|
||||
WindowInsetsCompat.Type.ime(),
|
||||
WindowInsets.ime,
|
||||
)
|
||||
}
|
||||
AndroidViewBinding(factory = { inflater, parent, attachToParent ->
|
||||
ActivityMainBinding.inflate(inflater, parent, attachToParent).apply {
|
||||
val navHostFragment = fragmentContainer.getFragment<NavHostFragment>()
|
||||
navController = navHostFragment.navController.apply {
|
||||
graph = navInflater.inflate(R.navigation.nav_graph).apply {
|
||||
check(launchPage in 0..3)
|
||||
setStartDestination(navItems[launchPage].first)
|
||||
AndroidViewBinding(factory = { inflater, parent, attachToParent ->
|
||||
ActivityMainBinding.inflate(inflater, parent, attachToParent).apply {
|
||||
val navHostFragment = fragmentContainer.getFragment<NavHostFragment>()
|
||||
navController = navHostFragment.navController.apply {
|
||||
graph = navInflater.inflate(R.navigation.nav_graph).apply {
|
||||
check(launchPage in 0..3)
|
||||
setStartDestination(navItems[launchPage].first)
|
||||
}
|
||||
}
|
||||
isInitializedFlow.value = true
|
||||
}
|
||||
isInitializedFlow.value = true
|
||||
}) {
|
||||
ViewCompat.dispatchApplyWindowInsets(root, insets)
|
||||
}
|
||||
}) {
|
||||
ViewCompat.dispatchApplyWindowInsets(root, insets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,31 @@ package com.hippo.ehviewer.ui
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
|
||||
// To make sure compose theme and legacy view theme are in sync, we only use Mdc3Theme
|
||||
fun ComposeView.setMD3Content(content: @Composable () -> Unit) = setContent { Mdc3Theme(content = content) }
|
||||
fun ComponentActivity.setMD3Content(content: @Composable () -> Unit) = setContent { Mdc3Theme(content = content) }
|
||||
|
||||
fun ComposeView.setMD3Content(content: @Composable () -> Unit) = setContent {
|
||||
Mdc3Theme {
|
||||
val dialogState = remember { DialogState() }
|
||||
dialogState.Intercept()
|
||||
CompositionLocalProvider(LocalDialogState provides dialogState) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ComponentActivity.setMD3Content(content: @Composable () -> Unit) = setContent {
|
||||
Mdc3Theme {
|
||||
val dialogState = remember { DialogState() }
|
||||
dialogState.Intercept()
|
||||
CompositionLocalProvider(LocalDialogState provides dialogState) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class DrawerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : FrameLayout(context, attrs) {
|
||||
private var mMaxWidth = 0
|
||||
|
||||
init {
|
||||
val a = context.obtainStyledAttributes(attrs, SIZE_ATTRS)
|
||||
mMaxWidth = a.getDimensionPixelOffset(0, 0)
|
||||
a.recycle()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||
var widthSpec = widthSpec
|
||||
when (MeasureSpec.getMode(widthSpec)) {
|
||||
MeasureSpec.EXACTLY -> {}
|
||||
MeasureSpec.AT_MOST -> widthSpec = MeasureSpec.makeMeasureSpec(
|
||||
MeasureSpec.getSize(widthSpec).coerceAtMost(mMaxWidth),
|
||||
MeasureSpec.EXACTLY,
|
||||
)
|
||||
|
||||
MeasureSpec.UNSPECIFIED ->
|
||||
widthSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY)
|
||||
}
|
||||
// Let super sort out the height
|
||||
super.onMeasure(widthSpec, heightSpec)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SIZE_ATTRS = intArrayOf(android.R.attr.maxWidth)
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.ui.FINISH_ROUTE_NAME
|
||||
import com.hippo.ehviewer.ui.LocalNavController
|
||||
import com.hippo.ehviewer.ui.SELECT_SITE_ROUTE_NAME
|
||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
@@ -87,8 +87,7 @@ fun CookieSignInScene(windowSizeClass: WindowSizeClass) {
|
||||
|
||||
var signInJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
val dialogState = rememberDialogState()
|
||||
dialogState.Intercept()
|
||||
val dialogState = LocalDialogState.current
|
||||
|
||||
val noCookies = stringResource(R.string.from_clipboard_error)
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ import com.hippo.ehviewer.ui.LocalNavController
|
||||
import com.hippo.ehviewer.ui.SELECT_SITE_ROUTE_NAME
|
||||
import com.hippo.ehviewer.ui.WEBVIEW_SIGN_IN_ROUTE_NAME
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
@@ -88,9 +88,7 @@ fun SignInScreen(windowSizeClass: WindowSizeClass) {
|
||||
var passwordHidden by rememberSaveable { mutableStateOf(true) }
|
||||
val context = LocalContext.current
|
||||
var signInJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
val dialogState = rememberDialogState()
|
||||
dialogState.Intercept()
|
||||
val dialogState = LocalDialogState.current
|
||||
|
||||
BackHandler {
|
||||
(context as Activity).moveTaskToBack(true)
|
||||
|
||||
@@ -67,11 +67,8 @@ fun GalleryInfoListItem(
|
||||
modifier = Modifier.aspectRatio(DEFAULT_ASPECT).fillMaxSize(),
|
||||
)
|
||||
}
|
||||
val showFav by produceState(false, info.gid) {
|
||||
value = info.favoriteSlot != NOT_FAVORITED && !isInFavScene
|
||||
FavouriteStatusRouter.stateFlow(info.gid).collect {
|
||||
value = it != NOT_FAVORITED && !isInFavScene
|
||||
}
|
||||
val showFav by FavouriteStatusRouter.collectAsState(info) {
|
||||
it != NOT_FAVORITED && !isInFavScene
|
||||
}
|
||||
ConstraintLayout(modifier = Modifier.padding(8.dp, 4.dp).fillMaxSize()) {
|
||||
val (titleRef, uploaderRef, ratingRef, categoryRef, postedRef, favRef, iconsRef) = createRefs()
|
||||
|
||||
@@ -21,10 +21,8 @@ import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.app.DownloadManager
|
||||
import android.content.DialogInterface
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.LayoutInflater
|
||||
@@ -32,7 +30,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
@@ -82,7 +79,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -96,7 +92,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidViewBinding
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
@@ -170,7 +165,10 @@ import com.hippo.ehviewer.util.IntList
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.getParcelableCompat
|
||||
import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.requestPermission
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -197,20 +195,6 @@ class GalleryDetailScene : BaseScene() {
|
||||
private var getDetailError by mutableStateOf("")
|
||||
|
||||
private var mTorrentList: TorrentResult? = null
|
||||
private var requestStoragePermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
) { result: Boolean ->
|
||||
if (result && composeBindingGD != null) {
|
||||
val helper = TorrentListDialogHelper()
|
||||
val binding = DialogTorrentListBinding.inflate(layoutInflater)
|
||||
val dialog: Dialog = BaseDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.torrents)
|
||||
.setView(binding.root)
|
||||
.setOnDismissListener(helper)
|
||||
.show()
|
||||
helper.setDialog(dialog, binding, composeBindingGD!!.torrentUrl)
|
||||
}
|
||||
}
|
||||
private var mArchiveFormParamOr: String? = null
|
||||
private var mArchiveList: List<ArchiveParser.Archive>? = null
|
||||
private var mCurrentFunds: HomeParser.Funds? = null
|
||||
@@ -730,9 +714,8 @@ class GalleryDetailScene : BaseScene() {
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
||||
) {
|
||||
val favored by produceState(initialValue = false) {
|
||||
value = galleryDetail.favoriteSlot != NOT_FAVORITED
|
||||
FavouriteStatusRouter.stateFlow(galleryDetail.gid).collect { value = it != NOT_FAVORITED }
|
||||
val favored by FavouriteStatusRouter.collectAsState(galleryDetail) {
|
||||
it != NOT_FAVORITED
|
||||
}
|
||||
val favButtonText = if (favored) {
|
||||
galleryDetail.favoriteName ?: stringResource(id = R.string.local_favorites)
|
||||
@@ -1113,21 +1096,20 @@ class GalleryDetailScene : BaseScene() {
|
||||
|
||||
private fun showTorrentDialog() {
|
||||
val galleryDetail = composeBindingGD ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(
|
||||
requireActivity(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
val helper = TorrentListDialogHelper()
|
||||
val binding = DialogTorrentListBinding.inflate(layoutInflater)
|
||||
val dialog: Dialog = BaseDialogBuilder(requireContext())
|
||||
.setTitle(R.string.torrents)
|
||||
.setView(binding.root)
|
||||
.setOnDismissListener(helper)
|
||||
.show()
|
||||
helper.setDialog(dialog, binding, galleryDetail.torrentUrl)
|
||||
viewLifecycleOwner.lifecycleScope.launchUI {
|
||||
val granted = isAtLeastQ || requireContext().requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
if (granted) {
|
||||
val helper = TorrentListDialogHelper()
|
||||
val binding = DialogTorrentListBinding.inflate(layoutInflater)
|
||||
val dialog: Dialog = BaseDialogBuilder(requireContext())
|
||||
.setTitle(R.string.torrents)
|
||||
.setView(binding.root)
|
||||
.setOnDismissListener(helper)
|
||||
.show()
|
||||
helper.setDialog(dialog, binding, galleryDetail.torrentUrl)
|
||||
} else {
|
||||
showTip(getString(R.string.permission_denied), LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ import com.hippo.ehviewer.ui.main.EhPreviewItem
|
||||
import com.hippo.ehviewer.ui.navToReader
|
||||
import com.hippo.ehviewer.ui.setMD3Content
|
||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid
|
||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.util.findActivity
|
||||
import com.hippo.ehviewer.util.getParcelableCompat
|
||||
@@ -78,8 +77,6 @@ class GalleryPreviewScreen : Fragment() {
|
||||
val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val columnCount = calculateSuitableSpanCount()
|
||||
val state = rememberLazyGridState()
|
||||
val dialogState = rememberDialogState()
|
||||
dialogState.Intercept()
|
||||
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val pages = galleryDetail.pages
|
||||
val pgSize = galleryDetail.previewList.size
|
||||
|
||||
@@ -66,7 +66,8 @@ import com.hippo.ehviewer.ui.setMD3Content
|
||||
import com.hippo.ehviewer.ui.tools.CrystalCard
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
|
||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.pxToDp
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -77,8 +78,7 @@ class HistoryScene : BaseScene() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = ComposeWithViewLifecycle().apply {
|
||||
setMD3Content {
|
||||
val dialogState = rememberDialogState()
|
||||
dialogState.Intercept()
|
||||
val dialogState = LocalDialogState.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val historyData = remember { Pager(PagingConfig(20, jumpThreshold = 40)) { EhDB.historyLazyList }.flow.cachedIn(lifecycleScope) }.collectAsLazyPagingItems()
|
||||
@@ -133,33 +133,35 @@ class HistoryScene : BaseScene() {
|
||||
true
|
||||
},
|
||||
)
|
||||
SwipeToDismiss(
|
||||
state = dismissState,
|
||||
background = {},
|
||||
dismissContent = {
|
||||
// TODO: item delete & add animation
|
||||
// Bug tracker: https://issuetracker.google.com/issues/150812265
|
||||
GalleryInfoListItem(
|
||||
onClick = {
|
||||
navAnimated(
|
||||
R.id.galleryDetailScene,
|
||||
bundleOf(
|
||||
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
|
||||
GalleryDetailScene.KEY_GALLERY_INFO to info,
|
||||
),
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
coroutineScope.launchIO {
|
||||
dialogState.doGalleryInfoAction(info, context)
|
||||
}
|
||||
},
|
||||
info = info,
|
||||
modifier = Modifier.height(cardHeight),
|
||||
)
|
||||
},
|
||||
directions = setOf(DismissDirection.EndToStart),
|
||||
)
|
||||
LocalTouchSlopProvider(3f) {
|
||||
SwipeToDismiss(
|
||||
state = dismissState,
|
||||
background = {},
|
||||
dismissContent = {
|
||||
// TODO: item delete & add animation
|
||||
// Bug tracker: https://issuetracker.google.com/issues/150812265
|
||||
GalleryInfoListItem(
|
||||
onClick = {
|
||||
navAnimated(
|
||||
R.id.galleryDetailScene,
|
||||
bundleOf(
|
||||
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
|
||||
GalleryDetailScene.KEY_GALLERY_INFO to info,
|
||||
),
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
coroutineScope.launchIO {
|
||||
dialogState.doGalleryInfoAction(info, context)
|
||||
}
|
||||
},
|
||||
info = info,
|
||||
modifier = Modifier.height(cardHeight),
|
||||
)
|
||||
},
|
||||
directions = setOf(DismissDirection.EndToStart),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CrystalCard(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -34,8 +34,8 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.ui.LICENSE_SCREEN
|
||||
import com.hippo.ehviewer.ui.LocalNavController
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||
import com.hippo.ehviewer.ui.tools.toAnnotatedString
|
||||
import com.hippo.ehviewer.updater.AppUpdater
|
||||
import com.hippo.ehviewer.updater.Release
|
||||
@@ -66,8 +66,7 @@ fun AboutScreen() {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val dialogState = rememberDialogState()
|
||||
dialogState.Intercept()
|
||||
val dialogState = LocalDialogState.current
|
||||
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
||||
@@ -38,8 +38,8 @@ import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.ui.LocalNavController
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import java.io.File
|
||||
@@ -58,8 +58,7 @@ fun AdvancedScreen() {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
||||
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
|
||||
val dialogState = rememberDialogState()
|
||||
dialogState.Intercept()
|
||||
val dialogState = LocalDialogState.current
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
||||
@@ -49,9 +49,9 @@ import com.hippo.ehviewer.ui.MYTAGS_SCREEN
|
||||
import com.hippo.ehviewer.ui.SIGN_IN_ROUTE_NAME
|
||||
import com.hippo.ehviewer.ui.UCONFIG_SCREEN
|
||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.TimePickerDialog
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||
import com.hippo.ehviewer.util.copyTextToClipboard
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
@@ -70,8 +70,7 @@ fun EhScreen() {
|
||||
val context = LocalContext.current
|
||||
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
|
||||
val signin = EhCookieStore.hasSignedIn()
|
||||
val dialogState = rememberDialogState()
|
||||
dialogState.Intercept()
|
||||
val dialogState = LocalDialogState.current
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
||||
@@ -49,7 +49,7 @@ import com.hippo.ehviewer.databinding.DialogAddFilterBinding
|
||||
import com.hippo.ehviewer.ui.LocalNavController
|
||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@@ -63,8 +63,7 @@ fun FilterScreen() {
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val allFilterMap = remember { scope.async { EhFilter.filters.await().groupByToObserved { it.mode } } }
|
||||
val dialogState = rememberDialogState()
|
||||
dialogState.Intercept()
|
||||
val dialogState = LocalDialogState.current
|
||||
class AddFilterDialogHelper(private val dialog: AlertDialog) : View.OnClickListener {
|
||||
private val mArray = dialog.context.resources.getStringArray(R.array.filter_entries)
|
||||
private val binding = DialogAddFilterBinding.bind(dialog.findViewById(R.id.base)!!)
|
||||
|
||||
@@ -24,8 +24,8 @@ import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.ui.LocalNavController
|
||||
import com.hippo.ehviewer.ui.isAuthenticationSupported
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -37,8 +37,7 @@ fun PrivacyScreen() {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
||||
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
|
||||
val dialogState = rememberDialogState()
|
||||
dialogState.Intercept()
|
||||
val dialogState = LocalDialogState.current
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
||||
@@ -31,9 +31,11 @@ import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -49,6 +51,7 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
interface DialogScope<R> {
|
||||
@@ -106,17 +109,24 @@ class DialogState {
|
||||
title: String? = null,
|
||||
hint: String? = null,
|
||||
isNumber: Boolean = false,
|
||||
invalidator: ((String) -> String?)? = null,
|
||||
invalidator: (suspend (String) -> String?)? = null,
|
||||
): String {
|
||||
return dialog { cont ->
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var state by remember(cont) { mutableStateOf(initial) }
|
||||
var error by remember(cont) { mutableStateOf<String?>(null) }
|
||||
AlertDialog(
|
||||
onDismissRequest = { cont.cancel() },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
error = invalidator?.invoke(state)
|
||||
error ?: cont.resume(state)
|
||||
if (invalidator == null) {
|
||||
cont.resume(state)
|
||||
} else {
|
||||
coroutineScope.launch {
|
||||
error = invalidator.invoke(state)
|
||||
error ?: cont.resume(state)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
@@ -309,7 +319,4 @@ class DialogState {
|
||||
|
||||
private val IconWithTextCorner = RoundedCornerShape(8.dp)
|
||||
|
||||
@Composable
|
||||
fun rememberDialogState(): DialogState {
|
||||
return remember { DialogState() }
|
||||
}
|
||||
val LocalDialogState = compositionLocalOf<DialogState> { error("CompositionLocal LocalDialogState not present!") }
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.hippo.ehviewer.ui.tools
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.platform.ViewConfiguration
|
||||
|
||||
// TODO: https://issuetracker.google.com/issues/269627294
|
||||
@Composable
|
||||
fun LocalTouchSlopProvider(factor: Float, content: @Composable () -> Unit) {
|
||||
val current = LocalViewConfiguration.current
|
||||
CompositionLocalProvider(
|
||||
value = LocalViewConfiguration provides object : ViewConfiguration by current {
|
||||
override val touchSlop: Float
|
||||
get() = current.touchSlop * factor
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
@@ -15,11 +15,17 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisallowComposableCalls
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -40,5 +46,11 @@ object FavouriteStatusRouter {
|
||||
|
||||
val globalFlow = _globalFlow.asSharedFlow()
|
||||
|
||||
fun stateFlow(targetGid: Long) = globalFlow.transform { (gid, slot) -> if (targetGid == gid) emit(slot) }
|
||||
@Composable
|
||||
inline fun <R> collectAsState(initial: GalleryInfo, crossinline transform: @DisallowComposableCalls (Int) -> R) = remember(initial) {
|
||||
globalFlow.transform { (gid, slot) -> if (initial.gid == gid) emit(slot) }
|
||||
.map { transform(it) }
|
||||
// We cannot use .map(transform) otherwise we will get a strange NPE
|
||||
// TODO: Report to kotlin compiler issue-tracker
|
||||
}.collectAsState(transform(initial.favoriteSlot))
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -13,7 +14,21 @@ import kotlin.contracts.contract
|
||||
|
||||
inline fun buildWindowInsets(builderAction: WindowInsetsCompat.Builder.() -> Unit): WindowInsetsCompat {
|
||||
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
|
||||
val builder = WindowInsetsCompat.Builder()
|
||||
val builder = if (isAtLeastQ) {
|
||||
WindowInsetsCompat.Builder()
|
||||
} else {
|
||||
// TODO: Report to google issue tracker
|
||||
// On API 20-28, WindowInsetsCompat.Builder() will copy the WindowInsets.CONSUMED static field,
|
||||
// which isConsumed() returns true, resulting in the insets not being dispatched
|
||||
runCatching {
|
||||
val constructor = android.view.WindowInsets::class.java.getConstructor(Rect::class.java)
|
||||
val platformInsets = constructor.newInstance(Rect())
|
||||
WindowInsetsCompat.Builder(WindowInsetsCompat.toWindowInsetsCompat(platformInsets))
|
||||
}.getOrElse {
|
||||
it.printStackTrace()
|
||||
WindowInsetsCompat.Builder()
|
||||
}
|
||||
}
|
||||
builder.builderAction()
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.content.ClipData
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
@@ -43,17 +42,14 @@ import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.WindowCompat
|
||||
@@ -73,8 +69,8 @@ import com.hippo.ehviewer.gallery.EhPageLoader
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.ui.EhActivity
|
||||
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
|
||||
import com.hippo.ehviewer.ui.setMD3Content
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
@@ -83,7 +79,9 @@ import com.hippo.ehviewer.util.getParcelableExtraCompat
|
||||
import com.hippo.ehviewer.util.getValue
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
import com.hippo.ehviewer.util.isAtLeastP
|
||||
import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.lazyMut
|
||||
import com.hippo.ehviewer.util.requestPermission
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.ehviewer.util.setValue
|
||||
import com.hippo.unifile.UniFile
|
||||
@@ -96,6 +94,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||
@@ -105,20 +104,13 @@ import eu.kanade.tachiyomi.util.view.setTooltip
|
||||
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.math.abs
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import splitties.init.appCtx
|
||||
import splitties.systemservices.clipboardManager
|
||||
|
||||
@@ -140,6 +132,7 @@ class ReaderActivity : EhActivity() {
|
||||
private var mPage: Int = 0
|
||||
private var mCacheFileName: String? = null
|
||||
private val vm: GalleryModel by viewModels()
|
||||
private val dialogState = DialogState()
|
||||
|
||||
/**
|
||||
* Whether the menu is currently visible.
|
||||
@@ -177,20 +170,6 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
var mGalleryProvider by lazyMut { vm::galleryProvider }
|
||||
private var mCurrentIndex: Int = 0
|
||||
private var mSavingPage = -1
|
||||
private lateinit var builder: EditTextDialogBuilder
|
||||
private var dialogShown = false
|
||||
private lateinit var dialog: AlertDialog
|
||||
private var requestStoragePermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
) { result ->
|
||||
if (result!! && mSavingPage != -1) {
|
||||
saveImage(mSavingPage)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
mSavingPage = -1
|
||||
}
|
||||
|
||||
private val galleryDetailUrl: String?
|
||||
get() {
|
||||
@@ -225,43 +204,27 @@ class ReaderActivity : EhActivity() {
|
||||
Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
val continuation: AtomicReference<Continuation<String>?> = AtomicReference(null)
|
||||
mGalleryProvider = ArchivePageLoader(
|
||||
appCtx,
|
||||
mUri!!,
|
||||
flow {
|
||||
if (!dialogShown) {
|
||||
withUIContext {
|
||||
dialogShown = true
|
||||
dialog.run {
|
||||
show()
|
||||
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val passwd = builder.text
|
||||
if (passwd.isEmpty()) {
|
||||
builder.setError(getString(R.string.passwd_cannot_be_empty))
|
||||
} else {
|
||||
continuation.getAndSet(null)?.resume(passwd)
|
||||
}
|
||||
}
|
||||
setOnCancelListener {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
) { invalidator ->
|
||||
runCatching {
|
||||
dialogState.awaitInputText(
|
||||
title = getString(R.string.archive_need_passwd),
|
||||
hint = getString(R.string.archive_passwd),
|
||||
) {
|
||||
if (it.isBlank()) {
|
||||
getString(R.string.passwd_cannot_be_empty)
|
||||
} else if (invalidator(it)) {
|
||||
null
|
||||
} else {
|
||||
getString(R.string.passwd_wrong)
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
val r = suspendCancellableCoroutine {
|
||||
continuation.set(it)
|
||||
it.invokeOnCancellation { dialog.dismiss() }
|
||||
}
|
||||
emit(r)
|
||||
withUIContext {
|
||||
builder.setError(getString(R.string.passwd_wrong))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}.onFailure {
|
||||
finish()
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "Archives are not supported before Android P", Toast.LENGTH_LONG).show()
|
||||
@@ -337,13 +300,10 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
buildProvider()
|
||||
binding = ReaderActivityBinding.inflate(layoutInflater)
|
||||
binding.dialogStub.setMD3Content {
|
||||
dialogState.Intercept()
|
||||
}
|
||||
setContentView(binding.root)
|
||||
builder = EditTextDialogBuilder(this, null, getString(R.string.archive_passwd))
|
||||
builder.setTitle(getString(R.string.archive_need_passwd))
|
||||
builder.setPositiveButton(getString(android.R.string.ok), null)
|
||||
dialog = builder.create()
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
|
||||
mGalleryProvider.let {
|
||||
if (it == null) {
|
||||
finish()
|
||||
@@ -361,8 +321,6 @@ class ReaderActivity : EhActivity() {
|
||||
|
||||
fun setGallery() {
|
||||
if (mGalleryProvider?.isReady != true) return
|
||||
// TODO: Not well place to call it
|
||||
dialog.dismiss()
|
||||
|
||||
// Get start page
|
||||
if (mCurrentIndex == 0) {
|
||||
@@ -486,78 +444,73 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
|
||||
fun saveImage(page: Int) {
|
||||
if (null == mGalleryProvider) {
|
||||
return
|
||||
}
|
||||
mGalleryProvider ?: return
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
mSavingPage = page
|
||||
requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
return
|
||||
}
|
||||
lifecycleScope.launchUI {
|
||||
val granted = isAtLeastQ || requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
if (granted) {
|
||||
val filename = mGalleryProvider!!.getImageFilenameWithExtension(page)
|
||||
var mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
|
||||
MimeTypeMap.getFileExtensionFromUrl(filename),
|
||||
)
|
||||
if (mimeType.isNullOrEmpty()) {
|
||||
mimeType = "image/jpeg"
|
||||
}
|
||||
|
||||
val filename = mGalleryProvider!!.getImageFilenameWithExtension(page)
|
||||
var mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
|
||||
MimeTypeMap.getFileExtensionFromUrl(filename),
|
||||
)
|
||||
if (mimeType.isNullOrEmpty()) {
|
||||
mimeType = "image/jpeg"
|
||||
}
|
||||
val realPath: String
|
||||
val resolver = contentResolver
|
||||
val values = ContentValues()
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
|
||||
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||
if (isAtLeastQ) {
|
||||
values.put(
|
||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||
Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME,
|
||||
)
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME
|
||||
} else {
|
||||
val path = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||
AppConfig.APP_DIRNAME,
|
||||
)
|
||||
realPath = path.toString()
|
||||
if (!FileUtils.ensureDirectory(path)) {
|
||||
Toast.makeText(this@ReaderActivity, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
|
||||
return@launchUI
|
||||
}
|
||||
values.put(MediaStore.MediaColumns.DATA, path.toString() + File.separator + filename)
|
||||
}
|
||||
val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
if (imageUri == null) {
|
||||
Toast.makeText(this@ReaderActivity, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
|
||||
return@launchUI
|
||||
}
|
||||
if (!mGalleryProvider!!.save(page, UniFile.fromMediaUri(this@ReaderActivity, imageUri))) {
|
||||
try {
|
||||
resolver.delete(imageUri, null, null)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val realPath: String
|
||||
val resolver = contentResolver
|
||||
val values = ContentValues()
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
|
||||
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
values.put(
|
||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||
Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME,
|
||||
)
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME
|
||||
} else {
|
||||
val path = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||
AppConfig.APP_DIRNAME,
|
||||
)
|
||||
realPath = path.toString()
|
||||
if (!FileUtils.ensureDirectory(path)) {
|
||||
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
Toast.makeText(this@ReaderActivity, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
|
||||
return@launchUI
|
||||
} else if (isAtLeastQ) {
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(imageUri, contentValues, null, null)
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
this@ReaderActivity,
|
||||
getString(R.string.image_saved, realPath + File.separator + filename),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
} else {
|
||||
Toast.makeText(this@ReaderActivity, R.string.permission_denied, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
values.put(MediaStore.MediaColumns.DATA, path.toString() + File.separator + filename)
|
||||
}
|
||||
val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
if (imageUri == null) {
|
||||
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
if (!mGalleryProvider!!.save(page, UniFile.fromMediaUri(this, imageUri))) {
|
||||
try {
|
||||
resolver.delete(imageUri, null, null)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(imageUri, contentValues, null, null)
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.image_saved, realPath + File.separator + filename),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
|
||||
fun saveImageTo(page: Int) {
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center">
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/dialog_stub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/reader_container"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -533,6 +533,7 @@
|
||||
<string name="update_frequency_monthly">每月</string>
|
||||
<string name="new_version_available">有新版本!</string>
|
||||
<string name="already_latest_version">已是最新版本</string>
|
||||
<string name="permission_denied">权限被拒</string>
|
||||
<!-- Tachiyomi Reader Start-->
|
||||
<string name="action_settings">设置</string>
|
||||
<string name="action_retry">重试</string>
|
||||
|
||||
@@ -597,6 +597,7 @@
|
||||
<string name="update_frequency_monthly">Monthly</string>
|
||||
<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>
|
||||
<!-- Tachiyomi Reader Start-->
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="pref_rotation_type">Default rotation type</string>
|
||||
|
||||
@@ -24,13 +24,13 @@ arrow = "1.2.1"
|
||||
chucker = "4.0.0"
|
||||
coil = "2.5.0"
|
||||
compose-bom = "2023.11.00-alpha01"
|
||||
compose-compiler = "1.5.4-dev-k1.9.20-RC2-93f1625925e"
|
||||
compose-compiler = "1.5.4-dev-k1.9.20-50f08dfa4b4"
|
||||
google-accompanist = "0.32.0"
|
||||
google-android-material = "1.11.0-beta01"
|
||||
kotlin = "1.9.20-RC2"
|
||||
kotlin = "1.9.20"
|
||||
kotlinx-coroutines = "1.7.3"
|
||||
kotlinx-serialization = "1.6.0"
|
||||
ksp = "1.9.20-RC2-1.0.13"
|
||||
ksp = "1.9.20-1.0.13"
|
||||
ktlint = "1.0.1"
|
||||
ktor = "2.3.5"
|
||||
leakcanary = "2.12"
|
||||
@@ -94,7 +94,7 @@ compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" }
|
||||
cronet-api = "org.chromium.net:cronet-api:113.5672.61"
|
||||
cronet-gms = "com.google.android.gms:play-services-cronet:18.0.1"
|
||||
|
||||
desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
|
||||
desugar = "com.android.tools:desugar_jdk_libs_nio:2.0.3"
|
||||
|
||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
||||
|
||||
Reference in new issue
Block a user