Compare commits

..
18 Commits
Author SHA1 Message Date
FooIbar 7e90555bed Release 1.8.11.1
Releases / Build (push) Has been cancelled
2023-11-01 23:32:31 +08:00
FooIbar d36c189430 Update xz to v5.4.5 2023-11-01 23:29:48 +08:00
FooIbar 61e2ace5c8 Fix insets on API < 29 2023-11-01 23:17:46 +08:00
Lokesh Gidra d6a3f21754 Fix a weird NPE 2023-11-01 22:12:07 +08:00
Lokesh Gidra 4e54d7f66f Refine FavouriteStatusRouter observable stateFlow 2023-11-01 22:12:07 +08:00
Lokesh Gidra f2dff038cd Global, CompositionLocal DialogState
Signed-off-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-01 19:37:22 +08:00
Lokesh Gidra a7b30640e0 Drop DrawerView
Signed-off-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-01 19:37:22 +08:00
FooIbar 45048b1f46 [Marshmallow] Fix crash when saving images 2023-11-01 16:26:06 +08:00
FooIbar 498129314a Refine storage permission request 2023-11-01 15:42:55 +08:00
Officer 3ee4b4a61e Cleanup ReaderActivity & Fix activity not finish when no archive passwd available 2023-11-01 15:41:23 +08:00
FooIbar ee59c0f22e Convert ReaderActivity dialog to use a DialogState 2023-11-01 12:30:36 +08:00
FooIbar c56134cd92 Fix API results parsing error on certain galleries 2023-11-01 12:17:48 +08:00
renovate[bot] b3d1b66a06 Update dependency com.google.devtools.ksp to v1.9.20-1.0.13 2023-11-01 03:49:48 +00:00
renovate[bot] c9bb81bf63 Update kotlin to v1.9.20 2023-11-01 02:41:28 +00:00
FooIbar d176810dae Update compose compiler for kotlin 1.9.20 2023-11-01 10:38:36 +08:00
FooIbar 17bdca71b1 Reduce drawer and history item gesture sensitivity 2023-11-01 09:50:25 +08:00
FooIbar 22fd3730df Sync with upstream changes and cleanup code 2023-11-01 02:37:34 +08:00
FooIbar 23d1271949 Prepare for next iteration 2023-10-31 13:30:29 +08:00
30 changed files with 338 additions and 455 deletions

No files matched your search

+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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>
+1
View File
@@ -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>
+4 -4
View File
@@ -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"