Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d9008a1a6 | ||
|
|
fb577dc026 | ||
|
|
675eb1b7e2 | ||
|
|
6a3d522dfa | ||
|
|
47aa5c64e7 | ||
|
|
5ecf5c41ca | ||
|
|
bc6d12a873 | ||
|
|
4d983830df | ||
|
|
2f1e0d030e | ||
|
|
b7c873a172 | ||
|
|
9d5eb55290 | ||
|
|
78ffb1c4fe | ||
|
|
031d622d5d | ||
|
|
34da120a43 | ||
|
|
5285e1ccf2 | ||
|
|
e28efc1f50 | ||
|
|
1c043a81a6 | ||
|
|
9a769cc7b6 | ||
|
|
045c6abe40 | ||
|
|
4ad5cef412 | ||
|
|
c3ffab3c79 | ||
|
|
c82de0b779 | ||
|
|
4e36cbabc0 | ||
|
|
94f87b14b7 | ||
|
|
6bc80d1afa | ||
|
|
fe4d004083 | ||
|
|
28410befa3 | ||
|
|
515f7deb7e | ||
|
|
800e66261e | ||
|
|
65658c4a02 | ||
|
|
0488c7ebf4 | ||
|
|
c30c1b29ef | ||
|
|
876001ad99 | ||
|
|
60dc59adbc | ||
|
|
c994890f28 | ||
|
|
76223a2cd2 | ||
|
|
6274b39b68 | ||
|
|
cca3a3e350 | ||
|
|
86ef8f2b48 | ||
|
|
5d116d9477 | ||
|
|
6e73fcc8c9 | ||
|
|
3bd12a3047 | ||
|
|
c99d8e8275 | ||
|
|
4a12e71877 | ||
|
|
3043901b5a | ||
|
|
7cc28955d6 | ||
|
|
ef44bb4857 |
No files matched your search
@@ -17,11 +17,11 @@ jobs:
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup Java 17
|
||||
- name: Setup Java 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
|
||||
@@ -15,11 +15,11 @@ jobs:
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup Java 17
|
||||
- name: Setup Java 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
|
||||
|
||||
+26
-2
@@ -1,5 +1,29 @@
|
||||
# 变更日志
|
||||
|
||||
## [1.10.3] - 2024-01-15
|
||||
|
||||
### 新功能
|
||||
|
||||
* [Marshmallow] 支持裁剪边缘、打开压缩包和 GIF
|
||||
|
||||
### 改进
|
||||
|
||||
* 由于部分设备上的非预期行为 [#578](https://github.com/FooIbar/EhViewer/issues/578) [#596](https://github.com/FooIbar/EhViewer/issues/596),默认禁用预见式返回导航动画,可于 设置-高级 中开启
|
||||
* 杂项优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 刷新后页面无响应 [#579](https://github.com/FooIbar/EhViewer/issues/579)
|
||||
* 浏览中保存图片后点击操作被阻挡 [#581](https://github.com/FooIbar/EhViewer/issues/581)
|
||||
* 在画廊详情页面切换至后台后点击评论闪退 [#580](https://github.com/FooIbar/EhViewer/issues/580)
|
||||
* 已下载的画廊中未阅读缓存过的图片解码失败 [#592](https://github.com/FooIbar/EhViewer/issues/592)
|
||||
* 在收藏页多选并下载会使得下载状态出现问题 [#603](https://github.com/FooIbar/EhViewer/issues/603)
|
||||
* 未能在不重启情况下切换 Http 引擎 [#610](https://github.com/FooIbar/EhViewer/issues/610)
|
||||
* 在有进度的画廊阅读界面无法直接回到第一页 [#614](https://github.com/FooIbar/EhViewer/issues/614)
|
||||
* 请求新闻界面失效 [#605](https://github.com/FooIbar/EhViewer/issues/605)
|
||||
* 收藏夹为空解析失败 [#630](https://github.com/FooIbar/EhViewer/issues/630)
|
||||
|
||||
|
||||
## [1.10.2] - 2023-12-31
|
||||
|
||||
### 新功能
|
||||
@@ -73,7 +97,7 @@
|
||||
|
||||
### 改进
|
||||
|
||||
* \[阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
|
||||
* [阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
|
||||
* 优化原图下载重试策略
|
||||
* 优化小屏设备上的搜索类别布局
|
||||
* 优化过渡动画
|
||||
@@ -91,7 +115,7 @@
|
||||
* 修复手速过快页面切换过快使加载圈圈不自动消失 [#209](https://github.com/FooIbar/EhViewer/issues/209)
|
||||
* 修复打开并关闭右侧抽屉后,状态栏一片白色 [#101](https://github.com/FooIbar/EhViewer/issues/101)
|
||||
* 修复使用深色主题时,切换界面会闪过亮色界面 [#121](https://github.com/FooIbar/EhViewer/issues/121)
|
||||
* \[Marshmallow] 修复小内存设备上图片解码失败 [#229](https://github.com/FooIbar/EhViewer/issues/229) [#372](https://github.com/FooIbar/EhViewer/issues/372)
|
||||
* [Marshmallow] 修复小内存设备上图片解码失败 [#229](https://github.com/FooIbar/EhViewer/issues/229) [#372](https://github.com/FooIbar/EhViewer/issues/372)
|
||||
* 修复已舍弃画廊上传者解析
|
||||
|
||||
### 已知问题
|
||||
|
||||
@@ -73,7 +73,7 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
|
||||
| OSS | FOSS dependencies** |
|
||||
| GMS | Proprietary GMS component |
|
||||
|
||||
\* Archives and GIFs are not supported
|
||||
\* May not support some archives
|
||||
|
||||
** Fall back to OkHttp on devices that don't support [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine)
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ android {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 180049
|
||||
versionName = "1.10.2"
|
||||
versionCode = 180050
|
||||
versionName = "1.10.3"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
@@ -100,11 +100,6 @@ android {
|
||||
minSdk = 23
|
||||
applicationIdSuffix = ".m"
|
||||
versionNameSuffix = "-M"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
targets += "noop"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
@@ -246,6 +241,7 @@ dependencies {
|
||||
implementation(libs.diff)
|
||||
|
||||
implementation(libs.aboutlibraries.core)
|
||||
implementation(libs.aboutlibraries.compose.m3)
|
||||
|
||||
implementation(libs.insetter) // Dead Dependency
|
||||
|
||||
@@ -278,7 +274,7 @@ configurations.all {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
ksp {
|
||||
|
||||
@@ -78,5 +78,3 @@ include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED archive.c border.c gifutils.c natsort/strnatcmp.c)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log jnigraphics)
|
||||
|
||||
add_custom_target("noop")
|
||||
@@ -86,6 +86,8 @@ const char supportExt[10][6] = {
|
||||
};
|
||||
|
||||
static inline int filename_is_playable_file(const char *name) {
|
||||
if (!name)
|
||||
return false;
|
||||
const char *dotptr = strrchr(name, '.');
|
||||
if (!dotptr++)
|
||||
return false;
|
||||
@@ -105,7 +107,7 @@ static inline int compare_entries(const void *a, const void *b) {
|
||||
static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
const char *name = archive_entry_pathname(ctx->entry);
|
||||
const char *name = archive_entry_pathname_utf8(ctx->entry);
|
||||
if (filename_is_playable_file(name)) {
|
||||
entries[count].filename = strdup(name);
|
||||
entries[count].index = count;
|
||||
@@ -164,7 +166,7 @@ static void mempool_release_pages(void *addr, size_t size) {
|
||||
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
if (filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
if (filename_is_playable_file(archive_entry_pathname_utf8(ctx->entry)))
|
||||
count++;
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
@@ -212,7 +214,7 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
|
||||
|
||||
static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
if (!filename_is_playable_file(archive_entry_pathname_utf8(ctx->entry)))
|
||||
continue;
|
||||
if (ctx->next_index++ == index) {
|
||||
return ctx->next_index - 1;
|
||||
@@ -431,7 +433,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
archive_alloc_ctx(&ctx);
|
||||
void *tmpBuf = alloca(4096);
|
||||
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(entry)))
|
||||
if (!filename_is_playable_file(archive_entry_pathname_utf8(entry)))
|
||||
continue;
|
||||
if (!archive_entry_is_encrypted(entry))
|
||||
continue;
|
||||
@@ -456,7 +458,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz,
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret)
|
||||
return NULL;
|
||||
jstring str = (*env)->NewStringUTF(env, archive_entry_pathname(ctx->entry));
|
||||
jstring str = (*env)->NewStringUTF(env, archive_entry_pathname_utf8(ctx->entry));
|
||||
ctx->using = 0;
|
||||
return str;
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* 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 androidx.compose.material3
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* <a href="https://m3.material.io/components/snackbar/overview" class="external" target="_blank">Material Design snackbar</a>.
|
||||
*
|
||||
* Snackbars provide brief messages about app processes at the bottom of the screen.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* Snackbars inform users of a process that an app has performed or will perform. They appear
|
||||
* temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
|
||||
* and they don’t require user input to disappear.
|
||||
*
|
||||
* A Snackbar can contain a single action. "Dismiss" or "cancel" actions are optional.
|
||||
*
|
||||
* Snackbars with an action should not timeout or self-dismiss until the user performs another
|
||||
* action. Here, moving the keyboard focus indicator to navigate through interactive elements in a
|
||||
* page is not considered an action.
|
||||
*
|
||||
* This version of snackbar is designed to work with [SnackbarData] provided by the
|
||||
* [SnackbarHost], which is usually used inside of the [Scaffold].
|
||||
*
|
||||
* This components provides only the visuals of the Snackbar. If you need to show a Snackbar
|
||||
* with defaults on the screen, use [SnackbarHostState.showSnackbar]:
|
||||
*
|
||||
* @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
|
||||
*
|
||||
* If you want to customize appearance of the Snackbar, you can pass your own version as a child
|
||||
* of the [SnackbarHost] to the [Scaffold]:
|
||||
*
|
||||
* @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
|
||||
*
|
||||
* When a [SnackbarData.visuals] sets the Snackbar's duration as [SnackbarDuration.Indefinite], it's
|
||||
* recommended to display an additional close affordance action.
|
||||
* See [SnackbarVisuals.withDismissAction]:
|
||||
*
|
||||
* @sample androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar
|
||||
*
|
||||
* @param snackbarData data about the current snackbar showing via [SnackbarHostState]
|
||||
* @param modifier the [Modifier] to be applied to this snackbar
|
||||
* @param actionOnNewLine whether or not action should be put on a separate line. Recommended for
|
||||
* action with long action text.
|
||||
* @param shape defines the shape of this snackbar's container
|
||||
* @param containerColor the color used for the background of this snackbar. Use [Color.Transparent]
|
||||
* to have no color.
|
||||
* @param contentColor the preferred color for content inside this snackbar
|
||||
* @param actionColor the color of the snackbar's action
|
||||
* @param actionContentColor the preferred content color for the optional action inside this
|
||||
* snackbar. See [SnackbarVisuals.actionLabel].
|
||||
* @param dismissActionContentColor the preferred content color for the optional dismiss action
|
||||
* inside this snackbar. See [SnackbarVisuals.withDismissAction].
|
||||
*/
|
||||
@Composable
|
||||
fun Snackbar2(
|
||||
snackbarData: SnackbarData,
|
||||
modifier: Modifier = Modifier,
|
||||
actionOnNewLine: Boolean = false,
|
||||
shape: Shape = SnackbarDefaults.shape,
|
||||
containerColor: Color = SnackbarDefaults.color,
|
||||
contentColor: Color = SnackbarDefaults.contentColor,
|
||||
actionColor: Color = SnackbarDefaults.actionColor,
|
||||
actionContentColor: Color = SnackbarDefaults.actionContentColor,
|
||||
dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
|
||||
) {
|
||||
val actionLabel = snackbarData.visuals.actionLabel
|
||||
val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) {
|
||||
@Composable {
|
||||
TextButton(
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = actionColor),
|
||||
onClick = { snackbarData.performAction() },
|
||||
content = { Text(actionLabel) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val dismissActionComposable: (@Composable () -> Unit)? =
|
||||
if (snackbarData.visuals.withDismissAction) {
|
||||
@Composable {
|
||||
IconButton(
|
||||
onClick = { snackbarData.dismiss() },
|
||||
content = {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
contentDescription = null,
|
||||
// Workaround for regression of https://android-review.googlesource.com/c/platform/frameworks/support/+/2881006
|
||||
tint = dismissActionContentColor,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Snackbar(
|
||||
modifier = modifier.padding(12.dp),
|
||||
action = actionComposable,
|
||||
dismissAction = dismissActionComposable,
|
||||
actionOnNewLine = actionOnNewLine,
|
||||
shape = shape,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
actionContentColor = actionContentColor,
|
||||
dismissActionContentColor = dismissActionContentColor,
|
||||
content = { Text(snackbarData.visuals.message) },
|
||||
)
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.animation.core.SeekableTransitionState
|
||||
import androidx.compose.animation.core.rememberTransition
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
@@ -55,6 +56,8 @@ import androidx.navigation.NavHostController
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.createGraph
|
||||
import androidx.navigation.get
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -246,10 +249,15 @@ public fun NavHost(
|
||||
val zIndices = remember { mutableMapOf<String, Float>() }
|
||||
|
||||
if (backStackEntry != null) {
|
||||
val enablePredictiveBack by Settings.predictiveNavAnim.collectAsState()
|
||||
val transitionState = remember { SeekableTransitionState(backStackEntry) }
|
||||
val transition = rememberTransition(transitionState, label = "entry")
|
||||
LaunchedEffect(backStackEntry) {
|
||||
transitionState.animateTo(backStackEntry)
|
||||
val transition = if (enablePredictiveBack) {
|
||||
LaunchedEffect(backStackEntry) {
|
||||
transitionState.animateTo(backStackEntry)
|
||||
}
|
||||
rememberTransition(transitionState, label = "entry")
|
||||
} else {
|
||||
updateTransition(backStackEntry, label = "entry")
|
||||
}
|
||||
|
||||
var inPredictiveBack by remember { mutableStateOf(false) }
|
||||
|
||||
+3
-4
@@ -9,7 +9,6 @@ import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.GestureDetector
|
||||
@@ -231,10 +230,10 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
*
|
||||
* @param bitmap Image source.
|
||||
*/
|
||||
fun setImage(bitmap: BitmapDrawable) {
|
||||
fun setImage(bitmap: Bitmap, rect: Rect) {
|
||||
reset(true)
|
||||
srcRect = Rect(bitmap.bounds)
|
||||
onImageLoaded(bitmap.bitmap)
|
||||
srcRect = Rect(rect)
|
||||
onImageLoaded(bitmap)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,9 +29,10 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.asCoilImage
|
||||
import coil3.decode.ImageDecoderDecoder
|
||||
import coil3.fetch.NetworkFetcher
|
||||
import coil3.gif.AnimatedImageDecoder
|
||||
import coil3.network.NetworkFetcher
|
||||
import coil3.request.crossfade
|
||||
import coil3.serviceLoaderEnabled
|
||||
import coil3.util.DebugLogger
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
@@ -52,12 +53,12 @@ import com.hippo.ehviewer.util.Crash
|
||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.delegateLazy
|
||||
import com.hippo.ehviewer.util.isAtLeastP
|
||||
import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.isAtLeastS
|
||||
import com.hippo.ehviewer.util.isCronetAvailable
|
||||
import com.hippo.ehviewer.util.resettableLazy
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
@@ -101,9 +102,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
handler?.uncaughtException(t, e)
|
||||
}
|
||||
super.onCreate()
|
||||
if (isAtLeastP) {
|
||||
System.loadLibrary("ehviewer")
|
||||
}
|
||||
System.loadLibrary("ehviewer")
|
||||
System.loadLibrary("ehviewer_rust")
|
||||
ReadableTime.initialize(this)
|
||||
lifecycleScope.launchIO {
|
||||
@@ -177,18 +176,20 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
|
||||
galleryDetailCache.evictAll()
|
||||
// Keep one entry for GalleryCommentsScreen
|
||||
galleryDetailCache.trimToSize(1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: Context) = context.imageLoader {
|
||||
components {
|
||||
add(NetworkFetcher.Factory(unsafeLazy { ktorClient }))
|
||||
if (isAtLeastP) {
|
||||
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
|
||||
}
|
||||
serviceLoaderEnabled(false)
|
||||
add(NetworkFetcher.Factory(delegateLazy { ktorClient }))
|
||||
add(MergeInterceptor)
|
||||
add(DownloadThumbInterceptor)
|
||||
if (isAtLeastP) {
|
||||
add(AnimatedImageDecoder.Factory(false))
|
||||
}
|
||||
}
|
||||
diskCache(imageCache)
|
||||
crossfade(300)
|
||||
|
||||
@@ -109,6 +109,7 @@ object Settings : DataStorePreferences(null) {
|
||||
val listThumbSize = intPref("list_tile_size", 40)
|
||||
val languageFilter = intPref("language_filter", -1)
|
||||
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
|
||||
val predictiveNavAnim = boolPref("predictive_nav_anim_2", false)
|
||||
val blackDarkTheme = boolPref("black_dark_theme", false)
|
||||
|
||||
var downloadScheme by stringOrNullPref("image_scheme", null)
|
||||
@@ -148,15 +149,15 @@ object Settings : DataStorePreferences(null) {
|
||||
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
||||
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
||||
var downloadOriginImage by boolPref("download_origin_image", false)
|
||||
var enableCronet by boolPref("enable_cronet", true)
|
||||
var enableCronet by boolPref("enable_cronet", true).observed { updateWhenEnableCronetChanges() }
|
||||
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
|
||||
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
||||
var securityDelay by intPref("require_unlock_delay", 0)
|
||||
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
|
||||
var searchCategory by intPref("search_pref", EhUtils.ALL_CATEGORY)
|
||||
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
|
||||
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
|
||||
var requestNewsTime by intPref("request_news_time", 0).observed { updateWhenRequestNewsChanges() }
|
||||
var updateIntervalDays by intPref("update_interval_days", 7)
|
||||
var lastDawnDays by intPref("last_dawn_days", 0)
|
||||
var recentToplist by stringPref("recent_toplist", "11")
|
||||
var userAgent by stringPref("user_agent", CHROME_USER_AGENT)
|
||||
var recentDownloadLabel by stringOrNullPref("recent_download_label", null)
|
||||
@@ -164,7 +165,6 @@ object Settings : DataStorePreferences(null) {
|
||||
var displayName by stringOrNullPref("display_name", null)
|
||||
var avatar by stringOrNullPref("avatar", null)
|
||||
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
|
||||
var lastDawnTime by longPref("last_dawn_time", 0)
|
||||
var lastUpdateTime by longPref("last_update_time", 0)
|
||||
var favDialogTheta by floatPref("fav_select_dialog_delta", 0F)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.hippo.ehviewer
|
||||
import android.app.UiModeManager
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
@@ -43,6 +45,13 @@ fun updateWhenKeepMediaStatusChanges() {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWhenEnableCronetChanges() {
|
||||
collectScope.launchIO {
|
||||
ktorClient = ktorClient.apply { close() }
|
||||
noRedirectKtorClient = noRedirectKtorClient.apply { close() }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWhenThemeChanges() {
|
||||
collectScope.launch {
|
||||
delay(100) // Avoid recompose being cancelled
|
||||
|
||||
@@ -36,7 +36,7 @@ object EhCookieStore : CookiesStorage {
|
||||
val cookies = load(Url(EhUrl.HOST_E))
|
||||
cookies.fastForEach {
|
||||
if (it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH) {
|
||||
manager.setCookie(EhUrl.HOST_EX, it.toString())
|
||||
manager.setCookie(EhUrl.HOST_EX, it.copy(maxAge = Int.MAX_VALUE).toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ object EhCookieStore : CookiesStorage {
|
||||
val checkTips = EhUrl.DOMAIN_E in url.host
|
||||
return cookies?.let {
|
||||
parseClientCookiesHeader(cookies)
|
||||
.map { Cookie(it.key, it.value, maxAge = Int.MAX_VALUE) }
|
||||
.map { Cookie(it.key, it.value) }
|
||||
.filterTo(mutableListOf()) { it.name != KEY_UTMP_NAME }.apply {
|
||||
if (checkTips) {
|
||||
removeAll { it.name == KEY_CONTENT_WARNING }
|
||||
|
||||
@@ -50,6 +50,7 @@ import com.hippo.ehviewer.client.parser.TorrentResult
|
||||
import com.hippo.ehviewer.client.parser.VoteCommentResult
|
||||
import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||
import com.hippo.ehviewer.dailycheck.showEventNotification
|
||||
import com.hippo.ehviewer.dailycheck.today
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.StatusCodeException
|
||||
@@ -68,7 +69,6 @@ import java.nio.channels.FileChannel
|
||||
import kotlin.math.ceil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.addJsonArray
|
||||
@@ -131,6 +131,12 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
|
||||
if (e is ParseException || e is SerializationException) {
|
||||
body.onLeft { if ("<" !in it) throw EhException(it) }
|
||||
when (val message = e.cause?.message) {
|
||||
"No hits found!" -> throw EhException(R.string.gallery_list_empty_hit)
|
||||
"No watched tags!" -> throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
"Not logged in!" -> throw NotLoggedInException()
|
||||
is String -> if (message.startsWith("Your IP address")) throw EhException(message)
|
||||
}
|
||||
if (Settings.saveParseErrorBody) body.saveParseError(e)
|
||||
throw EhException(appCtx.getString(R.string.error_parse_error), e)
|
||||
}
|
||||
@@ -217,7 +223,7 @@ object EhEngine {
|
||||
suspend fun getGalleryDetail(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsText {
|
||||
val eventPane = EventPaneParser.parse(this)
|
||||
if (eventPane != null) {
|
||||
Settings.lastDawnTime = Clock.System.now().epochSeconds
|
||||
Settings.lastDawnDays = today
|
||||
showEventNotification(eventPane)
|
||||
}
|
||||
GalleryDetailParser.parse(this)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.NotLoggedInException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -22,7 +21,6 @@ object FavoritesParser {
|
||||
return runSuspendCatching {
|
||||
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
|
||||
}.getOrElse {
|
||||
if (it is RuntimeException && it.message == "Not logged in!") throw NotLoggedInException()
|
||||
throw ParseException("Parse favorites error", it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -14,20 +12,9 @@ import moe.tarsin.coroutines.runSuspendCatching
|
||||
object GalleryListParser {
|
||||
suspend fun parse(body: ByteBuffer) = runSuspendCatching {
|
||||
unmarshalParsingAs<GalleryListResult>(body, ::parseGalleryInfoList)
|
||||
}.onFailure {
|
||||
if (it is RuntimeException) {
|
||||
if (it.message?.startsWith("Your IP address") == true) {
|
||||
throw EhException(it.message)
|
||||
}
|
||||
if (it.message == "No watched tags!") {
|
||||
throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
}
|
||||
if (it.message == "No hits found!") {
|
||||
throw EhException(R.string.gallery_list_empty_hit)
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
throw ParseException("Can't parse gallery list", it)
|
||||
}.getOrThrow().apply {
|
||||
}.apply {
|
||||
galleryInfoList.onEach {
|
||||
if (it.favoriteSlot == NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
|
||||
it.favoriteSlot = LOCAL_FAVORITED
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.graphics.drawable.ColorDrawable
|
||||
import coil3.asCoilImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.Decoder
|
||||
import coil3.network.httpHeaders
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.httpHeaders
|
||||
import coil3.size.Size
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
||||
|
||||
@@ -28,7 +28,6 @@ import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.LocalTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
@@ -48,18 +47,12 @@ class DailyCheckWork(context: Context, workerParams: WorkerParameters) :
|
||||
}
|
||||
}
|
||||
|
||||
val schedHour
|
||||
get() = Settings.requestNewsTimerHour.takeUnless { it == -1 } ?: 0
|
||||
|
||||
val schedMinute
|
||||
get() = Settings.requestNewsTimerMinute.takeUnless { it == -1 } ?: 0
|
||||
|
||||
private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
|
||||
val now = Clock.System.now()
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
val whenToWork = LocalDateTime(
|
||||
now.toLocalDateTime(timeZone).date,
|
||||
LocalTime(schedHour, schedMinute),
|
||||
LocalTime.fromSecondOfDay(Settings.requestNewsTime),
|
||||
).toInstant(timeZone)
|
||||
val initialDelay = (whenToWork - now).run { if (isNegative()) plus(1.days) else this }
|
||||
val constraints = Constraints.Builder()
|
||||
@@ -77,7 +70,7 @@ fun updateDailyCheckWork(context: Context) {
|
||||
if (Settings.requestNews) {
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
|
||||
getDailyCheckWorkRequest(),
|
||||
)
|
||||
} else {
|
||||
@@ -85,12 +78,13 @@ fun updateDailyCheckWork(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
val today
|
||||
get() = (Clock.System.now().epochSeconds / 86400).toInt()
|
||||
|
||||
suspend fun checkDawn() = runCatching {
|
||||
val now = Clock.System.now()
|
||||
val last = Instant.fromEpochSeconds(Settings.lastDawnTime)
|
||||
if (EhCookieStore.hasSignedIn() && now > last + 1.days) {
|
||||
if (EhCookieStore.hasSignedIn() && Settings.lastDawnDays != today) {
|
||||
EhEngine.getNews(true)?.let {
|
||||
Settings.lastDawnTime = now.epochSeconds
|
||||
Settings.lastDawnDays = today
|
||||
showEventNotification(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import splitties.preferences.edit
|
||||
|
||||
object DownloadManager : OnSpiderListener {
|
||||
@@ -83,6 +85,8 @@ object DownloadManager : OnSpiderListener {
|
||||
private val mutableNotifyFlow = MutableSharedFlow<DownloadInfo>(extraBufferCapacity = 1)
|
||||
val notifyFlow = mutableNotifyFlow.asSharedFlow()
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
init {
|
||||
assertNotMainThread()
|
||||
map = runAssertingNotMainThread {
|
||||
@@ -134,35 +138,37 @@ object DownloadManager : OnSpiderListener {
|
||||
}
|
||||
|
||||
private suspend fun ensureDownload() {
|
||||
if (mCurrentTask != null) {
|
||||
// Only one download
|
||||
return
|
||||
}
|
||||
mutex.withLock {
|
||||
if (mCurrentTask != null) {
|
||||
// Only one download
|
||||
return
|
||||
}
|
||||
|
||||
// Get download from wait list
|
||||
if (!mWaitList.isEmpty()) {
|
||||
val info = mWaitList.removeFirst()
|
||||
val spider = SpiderQueen.obtainSpiderQueen(info, SpiderQueen.MODE_DOWNLOAD)
|
||||
mCurrentTask = info
|
||||
mCurrentSpider = spider
|
||||
spider.addOnSpiderListener(this)
|
||||
info.state = DownloadInfo.STATE_DOWNLOAD
|
||||
info.speed = -1
|
||||
info.remaining = -1
|
||||
info.total = -1
|
||||
info.finished = 0
|
||||
info.downloaded = 0
|
||||
info.legacy = -1
|
||||
// Update in DB
|
||||
EhDB.putDownloadInfo(info)
|
||||
// Start speed count
|
||||
mSpeedReminder.start()
|
||||
// Notify start downloading
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener!!.onStart(info)
|
||||
// Get download from wait list
|
||||
if (!mWaitList.isEmpty()) {
|
||||
val info = mWaitList.removeFirst()
|
||||
val spider = SpiderQueen.obtainSpiderQueen(info, SpiderQueen.MODE_DOWNLOAD)
|
||||
mCurrentTask = info
|
||||
mCurrentSpider = spider
|
||||
spider.addOnSpiderListener(this)
|
||||
info.state = DownloadInfo.STATE_DOWNLOAD
|
||||
info.speed = -1
|
||||
info.remaining = -1
|
||||
info.total = -1
|
||||
info.finished = 0
|
||||
info.downloaded = 0
|
||||
info.legacy = -1
|
||||
// Update in DB
|
||||
EhDB.putDownloadInfo(info)
|
||||
// Start speed count
|
||||
mSpeedReminder.start()
|
||||
// Notify start downloading
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener!!.onStart(info)
|
||||
}
|
||||
// Notify state update
|
||||
mutableNotifyFlow.emit(info)
|
||||
}
|
||||
// Notify state update
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +189,7 @@ object DownloadManager : OnSpiderListener {
|
||||
// Update in DB
|
||||
EhDB.putDownloadInfo(info)
|
||||
// Notify state update
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
mutableNotifyFlow.emit(info)
|
||||
// Make sure download is running
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -213,7 +219,7 @@ object DownloadManager : OnSpiderListener {
|
||||
EhDB.putDownloadInfo(info)
|
||||
|
||||
// Notify
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
mutableNotifyFlow.emit(info)
|
||||
// Make sure download is running
|
||||
ensureDownload()
|
||||
|
||||
@@ -234,7 +240,7 @@ object DownloadManager : OnSpiderListener {
|
||||
updateList.onEach {
|
||||
it.state = DownloadInfo.STATE_WAIT
|
||||
EhDB.putDownloadInfo(it)
|
||||
mutableNotifyFlow.tryEmit(it)
|
||||
mutableNotifyFlow.emit(it)
|
||||
}
|
||||
mWaitList.addAll(updateList)
|
||||
ensureDownload()
|
||||
@@ -270,7 +276,7 @@ object DownloadManager : OnSpiderListener {
|
||||
updateList.onEach {
|
||||
it.state = DownloadInfo.STATE_WAIT
|
||||
EhDB.putDownloadInfo(it)
|
||||
mutableNotifyFlow.tryEmit(it)
|
||||
mutableNotifyFlow.emit(it)
|
||||
}
|
||||
mWaitList.addAll(updateList)
|
||||
ensureDownload()
|
||||
@@ -329,14 +335,14 @@ object DownloadManager : OnSpiderListener {
|
||||
EhDB.putDownloadInfo(info)
|
||||
|
||||
// Notify
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
mutableNotifyFlow.emit(info)
|
||||
}
|
||||
|
||||
suspend fun stopDownload(gid: Long) {
|
||||
val info = stopDownloadInternal(gid)
|
||||
if (info != null) {
|
||||
// Update listener
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
mutableNotifyFlow.emit(info)
|
||||
// Ensure download
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -346,7 +352,7 @@ object DownloadManager : OnSpiderListener {
|
||||
val info = stopCurrentDownloadInternal()
|
||||
if (info != null) {
|
||||
// Update listener
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
mutableNotifyFlow.emit(info)
|
||||
// Ensure download
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -357,7 +363,7 @@ object DownloadManager : OnSpiderListener {
|
||||
|
||||
// Update listener
|
||||
gidList.mapNotNull { mAllInfoMap[it] }.forEach {
|
||||
mutableNotifyFlow.tryEmit(it)
|
||||
mutableNotifyFlow.emit(it)
|
||||
}
|
||||
|
||||
// Ensure download
|
||||
@@ -365,19 +371,22 @@ object DownloadManager : OnSpiderListener {
|
||||
}
|
||||
|
||||
suspend fun stopAllDownload() {
|
||||
// Stop all in wait list
|
||||
for (info in mWaitList) {
|
||||
info.state = DownloadInfo.STATE_NONE
|
||||
// Update in DB
|
||||
EhDB.putDownloadInfo(info)
|
||||
mutex.withLock {
|
||||
// Stop all in wait list
|
||||
for (info in mWaitList) {
|
||||
info.state = DownloadInfo.STATE_NONE
|
||||
// Update in DB
|
||||
EhDB.putDownloadInfo(info)
|
||||
|
||||
// Notify
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
// Notify
|
||||
mutableNotifyFlow.emit(info)
|
||||
}
|
||||
mWaitList.clear()
|
||||
|
||||
// Stop current
|
||||
val info = stopCurrentDownloadInternal()
|
||||
info?.let { mutableNotifyFlow.emit(it) }
|
||||
}
|
||||
mWaitList.clear()
|
||||
|
||||
// Stop current
|
||||
stopCurrentDownloadInternal()
|
||||
}
|
||||
|
||||
suspend fun deleteDownload(gid: Long, deleteFiles: Boolean = false) {
|
||||
@@ -398,7 +407,7 @@ object DownloadManager : OnSpiderListener {
|
||||
if (index >= 0) {
|
||||
list.remove(info)
|
||||
// Update listener
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
mutableNotifyFlow.emit(info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,7 +430,7 @@ object DownloadManager : OnSpiderListener {
|
||||
allInfoList.removeAll(list.toSet())
|
||||
|
||||
// Update listener
|
||||
list.forEach { mutableNotifyFlow.tryEmit(it) }
|
||||
list.forEach { mutableNotifyFlow.emit(it) }
|
||||
|
||||
// Ensure download
|
||||
ensureDownload()
|
||||
@@ -434,8 +443,7 @@ object DownloadManager : OnSpiderListener {
|
||||
val list = allInfoList.slice(range)
|
||||
val limit = allInfoList.size - 1
|
||||
list.zip(range).forEach { it.first.position = limit - it.second }
|
||||
val label = allInfoList[fromPosition].label
|
||||
getInfoListForLabel(label)!!.sortByPositionDescending()
|
||||
getInfoListForLabel(info.label)!!.sortByPositionDescending()
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -815,7 +823,9 @@ object DownloadManager : OnSpiderListener {
|
||||
Log.e(TAG, "Current task is null, but it should not be")
|
||||
} else {
|
||||
info.total = mPages
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
launchIO {
|
||||
mutableNotifyFlow.emit(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,7 +854,9 @@ object DownloadManager : OnSpiderListener {
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener!!.onGetPage(info)
|
||||
}
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
launchIO {
|
||||
mutableNotifyFlow.emit(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,7 +869,9 @@ object DownloadManager : OnSpiderListener {
|
||||
info.finished = mFinished
|
||||
info.downloaded = mDownloaded
|
||||
info.total = mTotal
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
launchIO {
|
||||
mutableNotifyFlow.emit(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,7 +910,7 @@ object DownloadManager : OnSpiderListener {
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener!!.onFinish(info)
|
||||
}
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
mutableNotifyFlow.emit(info)
|
||||
// Start next download
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -982,7 +996,9 @@ object DownloadManager : OnSpiderListener {
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener!!.onDownload(info)
|
||||
}
|
||||
mutableNotifyFlow.tryEmit(info)
|
||||
launchIO {
|
||||
mutableNotifyFlow.emit(info)
|
||||
}
|
||||
}
|
||||
mBytesRead = 0
|
||||
if (!mStop) {
|
||||
|
||||
@@ -13,17 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:RequiresApi(Build.VERSION_CODES.P)
|
||||
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hippo.ehviewer.Settings.archivePasswds
|
||||
import com.hippo.ehviewer.image.ByteBufferSource
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.jni.closeArchive
|
||||
import com.hippo.ehviewer.jni.extractToByteBuffer
|
||||
@@ -110,7 +107,7 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider:
|
||||
val buffer = extractToByteBuffer(index)
|
||||
buffer ?: return
|
||||
check(buffer.isDirect)
|
||||
val src = object : Image.ByteBufferSource {
|
||||
val src = object : ByteBufferSource {
|
||||
override val source: ByteBuffer = buffer
|
||||
|
||||
override fun close() {
|
||||
|
||||
@@ -19,107 +19,86 @@ package com.hippo.ehviewer.image
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ColorSpace
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.ImageDecoder.ImageInfo
|
||||
import android.graphics.ImageDecoder.Source
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil3.decode.BitmapFactoryDecoder
|
||||
import coil3.decode.DecodeUtils
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.request.Options
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import coil3.BitmapImage
|
||||
import coil3.DrawableImage
|
||||
import coil3.Image as CoilImage
|
||||
import coil3.asCoilImage
|
||||
import coil3.executeBlocking
|
||||
import coil3.imageLoader
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.allowHardware
|
||||
import coil3.request.colorSpace
|
||||
import coil3.size.Precision
|
||||
import coil3.size.Scale
|
||||
import coil3.size.Size
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.jni.isGif
|
||||
import com.hippo.ehviewer.jni.mmap
|
||||
import com.hippo.ehviewer.jni.munmap
|
||||
import com.hippo.ehviewer.jni.rewriteGifSource
|
||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
import com.hippo.ehviewer.util.isAtLeastP
|
||||
import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.isAtLeastU
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.openInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.min
|
||||
import okio.FileSystem
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import splitties.init.appCtx
|
||||
|
||||
private val CropAspect = 0.1..100.0
|
||||
private const val CROP_THRESHOLD = 0.75f
|
||||
|
||||
class Image private constructor(drawable: Drawable, private val src: AutoCloseable) {
|
||||
val size = drawable.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 }
|
||||
private val intrinsicRect = drawable.run { Rect(0, 0, intrinsicWidth, intrinsicHeight) }
|
||||
|
||||
var mObtainedDrawable: Drawable? = if (drawable is Animatable) {
|
||||
// Cannot crop animated image's border
|
||||
drawable
|
||||
class Image private constructor(image: CoilImage, private val src: AutoCloseable) {
|
||||
val size = image.size
|
||||
private val intrinsicRect = image.run { Rect(0, 0, width, height) }
|
||||
val rect: Rect = if (Settings.cropBorder.value && image is BitmapImage) {
|
||||
val array = detectBorder(image.bitmap)
|
||||
val r = Rect(array[0], array[1], array[2], array[3])
|
||||
val minWidth = image.width * CROP_THRESHOLD
|
||||
val minHeight = image.height * CROP_THRESHOLD
|
||||
if (r.width() > minWidth && r.height() > minHeight) r else intrinsicRect
|
||||
} else {
|
||||
if (Settings.cropBorder.value) {
|
||||
val bitmap = (drawable as BitmapDrawable).bitmap
|
||||
val array = detectBorder(bitmap)
|
||||
val rect = with(bitmap) {
|
||||
val r = Rect(array[0], array[1], array[2], array[3])
|
||||
val aspectAfterCrop = r.height().toFloat() / r.width()
|
||||
if (aspectAfterCrop in CropAspect) r else intrinsicRect
|
||||
}
|
||||
intrinsicRect
|
||||
}
|
||||
|
||||
val final = if (isAtLeastQ) {
|
||||
// Upload to Graphical Buffer to accelerate render
|
||||
bitmap.copy(Bitmap.Config.HARDWARE, false).apply {
|
||||
bitmap.recycle()
|
||||
}
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
var innerImage: CoilImage? = if (image is BitmapImage && isAtLeastQ) {
|
||||
val bitmap = image.bitmap
|
||||
|
||||
final.toDrawable(appCtx.resources).apply { bounds = rect }
|
||||
} else {
|
||||
drawable.apply { bounds = intrinsicRect }
|
||||
}
|
||||
// Upload to Graphical Buffer to accelerate render
|
||||
bitmap.copy(Bitmap.Config.HARDWARE, false).apply {
|
||||
bitmap.recycle()
|
||||
}.asCoilImage()
|
||||
} else {
|
||||
image
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun recycle() {
|
||||
(mObtainedDrawable as? Animatable)?.stop()
|
||||
(mObtainedDrawable as? BitmapDrawable)?.bitmap?.recycle()
|
||||
mObtainedDrawable?.callback = null
|
||||
if (mObtainedDrawable is Animatable) src.close()
|
||||
mObtainedDrawable = null
|
||||
when (val image = innerImage ?: return) {
|
||||
is DrawableImage -> {
|
||||
(image.drawable as Animatable).stop()
|
||||
image.drawable.callback = null
|
||||
src.close()
|
||||
}
|
||||
is BitmapImage -> image.bitmap.recycle()
|
||||
}
|
||||
innerImage = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
fun calculateSampleSize(info: ImageInfo, targetHeight: Int, targetWeight: Int): Int {
|
||||
return min(
|
||||
info.size.width / targetWeight,
|
||||
info.size.height / targetHeight,
|
||||
).coerceAtLeast(1)
|
||||
}
|
||||
|
||||
private val imageSearchMaxSize = appCtx.resources.getDimensionPixelOffset(R.dimen.image_search_max_size)
|
||||
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||
val imageSearchDecoderSampleListener by lazy {
|
||||
ImageDecoder.OnHeaderDecodedListener { decoder, info, _ ->
|
||||
decoder.setTargetSampleSize(
|
||||
calculateSampleSize(info, imageSearchMaxSize, imageSearchMaxSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3 / 2
|
||||
private val targetHeight = appCtx.resources.displayMetrics.heightPixels * 3 / 2
|
||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 2
|
||||
private val targetHeight = appCtx.resources.displayMetrics.heightPixels * 2
|
||||
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.O)
|
||||
val isWideColorGamut by lazy { appCtx.resources.configuration.isScreenWideColorGamut }
|
||||
@@ -127,63 +106,60 @@ class Image private constructor(drawable: Drawable, private val src: AutoCloseab
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
lateinit var colorSpace: ColorSpace
|
||||
|
||||
suspend fun decode(src: AutoCloseable): Image? {
|
||||
private suspend fun Either<ByteBufferSource, UniFileSource>.decodeCoil(): CoilImage {
|
||||
val req = appCtx.imageRequest {
|
||||
onLeft { data(it.source) }
|
||||
onRight { data(it.source.uri) }
|
||||
size(targetWidth, targetHeight)
|
||||
scale(Scale.FILL)
|
||||
precision(Precision.INEXACT)
|
||||
if (isAtLeastO) {
|
||||
colorSpace(colorSpace)
|
||||
}
|
||||
if (Settings.cropBorder.value) {
|
||||
allowHardware(false)
|
||||
}
|
||||
memoryCachePolicy(CachePolicy.DISABLED)
|
||||
}
|
||||
return when (val result = appCtx.imageLoader.execute(req)) {
|
||||
is SuccessResult -> result.image
|
||||
is ErrorResult -> throw result.throwable
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decode(src: ImageSource): Image? {
|
||||
return runCatching {
|
||||
when (src) {
|
||||
is UniFileSource -> {
|
||||
if (isAtLeastP) {
|
||||
if (!isAtLeastU) {
|
||||
src.source.openFileDescriptor("rw").use {
|
||||
val fd = it.fd
|
||||
if (isGif(fd)) {
|
||||
val buffer = mmap(fd)!!
|
||||
val source = object : ByteBufferSource {
|
||||
override val source = buffer
|
||||
override fun close() {
|
||||
munmap(buffer)
|
||||
src.close()
|
||||
}
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
src.source.openFileDescriptor("rw").use {
|
||||
val fd = it.fd
|
||||
if (isGif(fd)) {
|
||||
val buffer = mmap(fd)!!
|
||||
val source = object : ByteBufferSource {
|
||||
override val source = buffer
|
||||
override fun close() {
|
||||
munmap(buffer)
|
||||
src.close()
|
||||
}
|
||||
return decode(source)
|
||||
}
|
||||
return decode(source)
|
||||
}
|
||||
}
|
||||
val drawable = decodeDrawable(src.source.asImageSource())
|
||||
if (drawable !is Animatable) src.close()
|
||||
Image(drawable, src)
|
||||
} else {
|
||||
val options = Options(
|
||||
appCtx,
|
||||
size = Size(targetWidth, targetHeight),
|
||||
scale = Scale.FILL,
|
||||
allowInexactSize = true,
|
||||
)
|
||||
val drawable = ImageSource(
|
||||
src.source.openInputStream().source().buffer(),
|
||||
FileSystem.SYSTEM,
|
||||
).use {
|
||||
BitmapFactoryDecoder(it, options).decode().image.asDrawable(appCtx.resources)
|
||||
}
|
||||
src.close()
|
||||
Image(drawable, src)
|
||||
}
|
||||
val image = src.right().decodeCoil()
|
||||
if (image is BitmapImage) src.close()
|
||||
Image(image, src)
|
||||
}
|
||||
|
||||
is ByteBufferSource -> {
|
||||
if (isAtLeastP) {
|
||||
if (!isAtLeastU) {
|
||||
rewriteGifSource(src.source)
|
||||
}
|
||||
val source = ImageDecoder.createSource(src.source)
|
||||
val drawable = decodeDrawable(source)
|
||||
if (drawable !is Animatable) src.close()
|
||||
Image(drawable, src)
|
||||
} else {
|
||||
TODO("Unsupported")
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
rewriteGifSource(src.source)
|
||||
}
|
||||
val image = src.left().decodeCoil()
|
||||
if (image is BitmapImage) src.close()
|
||||
Image(image, src)
|
||||
}
|
||||
|
||||
else -> TODO("Unsupported")
|
||||
}
|
||||
}.onFailure {
|
||||
src.close()
|
||||
@@ -191,49 +167,30 @@ class Image private constructor(drawable: Drawable, private val src: AutoCloseab
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
private fun decodeDrawable(src: Source) = ImageDecoder.decodeDrawable(src) { decoder, info, _ ->
|
||||
if (!isAtLeastQ || Settings.cropBorder.value) {
|
||||
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||
fun Context.decodeBitmap(uri: Uri): Bitmap {
|
||||
val req = imageRequest {
|
||||
memoryCachePolicy(CachePolicy.DISABLED)
|
||||
data(uri)
|
||||
size(imageSearchMaxSize)
|
||||
scale(Scale.FILL)
|
||||
}
|
||||
decoder.setTargetColorSpace(colorSpace)
|
||||
decoder.setTargetSampleSize(calculateSampleSize(info, targetHeight, targetWidth))
|
||||
}
|
||||
|
||||
fun Context.decodeBitmap(uri: Uri): Bitmap? = if (isAtLeastP) {
|
||||
val src = ImageDecoder.createSource(contentResolver, uri)
|
||||
ImageDecoder.decodeBitmap(src, imageSearchDecoderSampleListener)
|
||||
} else {
|
||||
contentResolver.openFileDescriptor(uri, "r")!!.use {
|
||||
val options = BitmapFactory.Options()
|
||||
|
||||
// Disable these since we straight up compress the bitmap to JPEG
|
||||
options.inScaled = false
|
||||
options.inPremultiplied = false
|
||||
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFileDescriptor(it.fileDescriptor, null, options)
|
||||
options.inJustDecodeBounds = false
|
||||
|
||||
options.inSampleSize = DecodeUtils.calculateInSampleSize(
|
||||
options.outWidth,
|
||||
options.outHeight,
|
||||
imageSearchMaxSize,
|
||||
imageSearchMaxSize,
|
||||
Scale.FILL,
|
||||
)
|
||||
BitmapFactory.decodeFileDescriptor(it.fileDescriptor, null, options)
|
||||
val image = when (val result = appCtx.imageLoader.executeBlocking(req)) {
|
||||
is SuccessResult -> result.image
|
||||
is ErrorResult -> throw result.throwable
|
||||
}
|
||||
return (image as BitmapImage).bitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface UniFileSource : AutoCloseable {
|
||||
val source: UniFile
|
||||
}
|
||||
sealed interface ImageSource : AutoCloseable
|
||||
|
||||
interface ByteBufferSource : AutoCloseable {
|
||||
val source: ByteBuffer
|
||||
}
|
||||
interface UniFileSource : ImageSource {
|
||||
val source: UniFile
|
||||
}
|
||||
|
||||
interface ByteBufferSource : ImageSource {
|
||||
val source: ByteBuffer
|
||||
}
|
||||
|
||||
private external fun detectBorder(bitmap: Bitmap): IntArray
|
||||
@@ -1,9 +1,5 @@
|
||||
@file:RequiresApi(Build.VERSION_CODES.P)
|
||||
|
||||
package com.hippo.ehviewer.jni
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
@file:RequiresApi(Build.VERSION_CODES.P)
|
||||
|
||||
package com.hippo.ehviewer.jni
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
external fun isGif(fd: Int): Boolean
|
||||
|
||||
@@ -24,7 +24,7 @@ import com.hippo.ehviewer.client.getImageKey
|
||||
import com.hippo.ehviewer.coil.read
|
||||
import com.hippo.ehviewer.coil.suspendEdit
|
||||
import com.hippo.ehviewer.download.downloadLocation
|
||||
import com.hippo.ehviewer.image.Image.UniFileSource
|
||||
import com.hippo.ehviewer.image.UniFileSource
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.unifile.UniFile
|
||||
|
||||
@@ -212,7 +212,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
MODE_READ -> mReadReference++
|
||||
MODE_DOWNLOAD -> mDownloadReference++
|
||||
}
|
||||
check(mDownloadReference <= 1) { "mDownloadReference can't more than 0" }
|
||||
check(mDownloadReference <= 1) { "mDownloadReference can't more than 1" }
|
||||
}
|
||||
|
||||
private fun clearMode(@Mode mode: Int) {
|
||||
@@ -562,9 +562,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
}
|
||||
}.onFailure {
|
||||
if (it is CancellationException) {
|
||||
Log.d(WORKER_DEBUG_TAG, "Download image $index cancelled")
|
||||
if (it.message != FORCE_RETRY) {
|
||||
updatePageState(index, STATE_FAILED, "Cancelled")
|
||||
if (mReadReference > 0) {
|
||||
Log.d(WORKER_DEBUG_TAG, "Download image $index cancelled")
|
||||
if (it.message != FORCE_RETRY) {
|
||||
updatePageState(index, STATE_FAILED, "Cancelled")
|
||||
}
|
||||
}
|
||||
throw it
|
||||
}
|
||||
@@ -602,19 +604,15 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
}
|
||||
}
|
||||
val previousPToken: String?
|
||||
val pToken: String?
|
||||
val pToken: String
|
||||
|
||||
pTokenLock.withLock {
|
||||
// Non-local return will not release the lock
|
||||
// https://github.com/Kotlin/kotlinx.coroutines/issues/3985
|
||||
pToken = getPToken(index)
|
||||
if (pToken == null) {
|
||||
updatePageState(index, STATE_FAILED, PTOKEN_FAILED_MESSAGE)
|
||||
mSpiderInfo.pTokenMap[index] = TOKEN_FAILED
|
||||
}
|
||||
?: return updatePageState(index, STATE_FAILED, PTOKEN_FAILED_MESSAGE).also {
|
||||
mSpiderInfo.pTokenMap[index] = TOKEN_FAILED
|
||||
}
|
||||
previousPToken = getPToken(index - 1)
|
||||
}
|
||||
pToken ?: return
|
||||
|
||||
delayLock.withLock {
|
||||
delay(mDownloadDelay - lastRequestTime.elapsedNow())
|
||||
@@ -689,7 +687,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
}
|
||||
checkNotNull(targetImageUrl)
|
||||
|
||||
repeat(3) { times ->
|
||||
repeat(2) { times ->
|
||||
runCatching {
|
||||
Log.d(WORKER_DEBUG_TAG, "Start download image $index attempt #$times")
|
||||
val success = withTimeout(downloadTimeout) {
|
||||
|
||||
@@ -59,7 +59,6 @@ import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ShapeDefaults
|
||||
import androidx.compose.material3.SideDrawer
|
||||
import androidx.compose.material3.Snackbar2
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
@@ -341,7 +340,7 @@ class MainActivity : EhActivity() {
|
||||
LocalDrawerLockHandle provides lockDrawerHandle,
|
||||
LocalSnackbarHostState provides snackbarState,
|
||||
) {
|
||||
Scaffold(snackbarHost = { SnackbarHost(snackbarState) { Snackbar2(it) } }) {
|
||||
Scaffold(snackbarHost = { SnackbarHost(snackbarState) }) {
|
||||
LocalTouchSlopProvider(Settings.touchSlopFactor.toFloat()) {
|
||||
ModalNavigationDrawer(
|
||||
drawerContent = {
|
||||
|
||||
@@ -96,10 +96,10 @@ fun CookieSignInScene(navigator: DestinationsNavigator) {
|
||||
fun storeCookie(id: String, hash: String, igneous: String) {
|
||||
EhUtils.signOut()
|
||||
EhCookieStore.addCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_E)
|
||||
EhCookieStore.addCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_EX)
|
||||
EhCookieStore.addCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_E)
|
||||
EhCookieStore.addCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_EX)
|
||||
if (igneous.isNotBlank()) EhCookieStore.addCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX)
|
||||
if (igneous.isNotBlank() && igneous != "mystery") {
|
||||
EhCookieStore.addCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX)
|
||||
}
|
||||
}
|
||||
|
||||
fun login() {
|
||||
@@ -161,7 +161,7 @@ fun CookieSignInScene(navigator: DestinationsNavigator) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
|
||||
return
|
||||
}
|
||||
if (kvs.size < 3) {
|
||||
if (kvs.size < 2) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -59,11 +58,6 @@ fun WebViewSignInScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
SideEffect {
|
||||
EhUtils.signOut()
|
||||
CookieManager.getInstance().apply {
|
||||
flush()
|
||||
removeAllCookies(null)
|
||||
removeSessionCookies(null)
|
||||
}
|
||||
}
|
||||
WebView(
|
||||
state = state,
|
||||
|
||||
@@ -151,7 +151,7 @@ fun SearchFilter(
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
OutlinedTextField(
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
value = expectedValue.first.takeIf { it > 0 }?.toString().orEmpty(),
|
||||
onValueChange = {
|
||||
expectedValue =
|
||||
@@ -163,7 +163,7 @@ fun SearchFilter(
|
||||
)
|
||||
Text(text = stringResource(id = R.string.search_sp_to))
|
||||
OutlinedTextField(
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
value = expectedValue.second.takeIf { it > 0 }?.toString().orEmpty(),
|
||||
onValueChange = {
|
||||
expectedValue =
|
||||
|
||||
@@ -49,7 +49,6 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -63,12 +62,10 @@ import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.core.view.SoftwareKeyboardControllerCompat
|
||||
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -237,17 +234,9 @@ fun SearchBarScreen(
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (isAtLeastP) {
|
||||
// Workaround IME not hiding after performing search
|
||||
// https://github.com/FooIbar/EhViewer/pull/412
|
||||
val view = LocalView.current
|
||||
val keyboardManager = remember { SoftwareKeyboardControllerCompat(view) }
|
||||
SideEffect {
|
||||
if (!active) keyboardManager.hide()
|
||||
}
|
||||
} else {
|
||||
if (!isAtLeastP) {
|
||||
// Workaround unable to exit search field
|
||||
// https://android.googlesource.com/platform/frameworks/support/+/8f81d46e2c3f37564d0b7515ec9411314645e187
|
||||
// https://issuetracker.google.com/issues/318968220
|
||||
Box(Modifier.size(1.dp).focusable())
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -147,11 +145,6 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
||||
summary = if (enableCronet) "Cronet" else "OkHttp",
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
fun reset() {
|
||||
ktorClient = ktorClient.apply { close() }
|
||||
noRedirectKtorClient = noRedirectKtorClient.apply { close() }
|
||||
}
|
||||
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
title = R.string.settings_advanced_http_engine,
|
||||
showCancelButton = false,
|
||||
@@ -162,7 +155,6 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
||||
selected = !enableCronet,
|
||||
onClick = {
|
||||
enableCronet = false
|
||||
reset()
|
||||
},
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
|
||||
) {
|
||||
@@ -172,7 +164,6 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
||||
selected = enableCronet,
|
||||
onClick = {
|
||||
enableCronet = true
|
||||
reset()
|
||||
},
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
|
||||
enabled = isCronetAvailable,
|
||||
@@ -188,6 +179,10 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
||||
title = stringResource(id = R.string.preload_thumb_aggressively),
|
||||
value = Settings::preloadThumbAggressively,
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.predictive_nav_anim),
|
||||
value = Settings.predictiveNavAnim::value,
|
||||
)
|
||||
IntSliderPreference(
|
||||
maxValue = 5,
|
||||
minValue = 1,
|
||||
|
||||
@@ -39,9 +39,6 @@ import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.dailycheck.schedHour
|
||||
import com.hippo.ehviewer.dailycheck.schedMinute
|
||||
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
|
||||
import com.hippo.ehviewer.ui.destinations.FilterScreenDestination
|
||||
import com.hippo.ehviewer.ui.destinations.MyTagsScreenDestination
|
||||
import com.hippo.ehviewer.ui.destinations.SignInScreenDestination
|
||||
@@ -59,6 +56,7 @@ import io.ktor.http.Url
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalTime
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@@ -325,10 +323,9 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
||||
val pickerTitle = stringResource(id = R.string.settings_eh_request_news_timepicker)
|
||||
Preference(title = pickerTitle) {
|
||||
coroutineScope.launch {
|
||||
val (hour, minute) = dialogState.showTimePicker(pickerTitle, schedHour, schedMinute)
|
||||
Settings.requestNewsTimerHour = hour
|
||||
Settings.requestNewsTimerMinute = minute
|
||||
updateDailyCheckWork(context)
|
||||
val time = LocalTime.fromSecondOfDay(Settings.requestNewsTime)
|
||||
val (hour, minute) = dialogState.showTimePicker(pickerTitle, time.hour, time.minute)
|
||||
Settings.requestNewsTime = LocalTime(hour, minute).toSecondOfDay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.hippo.ehviewer.ui.settings
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -11,9 +13,13 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.hippo.ehviewer.R
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibraryDefaults
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@@ -21,6 +27,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
@Composable
|
||||
fun LicenseScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val context = LocalContext.current
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -33,7 +40,15 @@ fun LicenseScreen(navigator: DestinationsNavigator) {
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) {
|
||||
LibrariesContainer(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), contentPadding = it)
|
||||
) { paddingValues ->
|
||||
LibrariesContainer(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
contentPadding = paddingValues,
|
||||
colors = LibraryDefaults.libraryColors(badgeBackgroundColor = MaterialTheme.colorScheme.tertiary),
|
||||
padding = LibraryDefaults.libraryPadding(badgeContentPadding = PaddingValues(4.dp)),
|
||||
onLibraryClick = { library ->
|
||||
library.library.website?.let { context.openBrowser(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.hippo.ehviewer.ui.webview
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewState
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.util.setDefaultSettings
|
||||
|
||||
@Composable
|
||||
fun WebViewScreen(url: String, onNavigateUp: () -> Unit) {
|
||||
val state = rememberWebViewState(url = url)
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = state.pageTitle ?: url) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateUp) {
|
||||
Icon(imageVector = Icons.Default.Close, contentDescription = null)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
WebView(
|
||||
state = state,
|
||||
modifier = Modifier.padding(paddingValues).fillMaxSize(),
|
||||
onCreated = { it.setDefaultSettings() },
|
||||
)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
EhCookieStore.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,12 @@ import kotlin.reflect.KProperty
|
||||
|
||||
fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer)
|
||||
fun <T> resettableLazy(initializer: () -> T) = ResettableLazy(initializer)
|
||||
fun <T> delegateLazy(initializer: () -> T) = object : kotlin.Lazy<T> {
|
||||
override val value: T
|
||||
get() = initializer()
|
||||
|
||||
override fun isInitialized() = true
|
||||
}
|
||||
|
||||
interface Lazy<T> {
|
||||
var value: T
|
||||
|
||||
@@ -15,22 +15,19 @@
|
||||
*/
|
||||
package com.hippo.unifile
|
||||
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.open
|
||||
import android.os.ParcelFileDescriptor.parseMode
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.RequiresApi
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
class RawFile(parent: UniFile?, private var file: File) : UniFile(parent) {
|
||||
val parent = parent as? RawFile
|
||||
private val parent = parent as? RawFile
|
||||
|
||||
var cachePresent = false
|
||||
private var cachePresent = false
|
||||
private val allChildren by lazy {
|
||||
val current = this
|
||||
cachePresent = true
|
||||
@@ -41,9 +38,12 @@ class RawFile(parent: UniFile?, private var file: File) : UniFile(parent) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun popParentCacheIfPresent() = apply {
|
||||
val present = parent?.cachePresent ?: false
|
||||
if (present) parent?.allChildren?.add(this)
|
||||
private fun popCacheIfPresent(file: RawFile) = apply {
|
||||
if (cachePresent) {
|
||||
synchronized(allChildren) {
|
||||
allChildren.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createFile(displayName: String): UniFile? {
|
||||
@@ -88,7 +88,7 @@ class RawFile(parent: UniFile?, private var file: File) : UniFile(parent) {
|
||||
override fun ensureDir(): Boolean {
|
||||
if (file.isDirectory) return true
|
||||
if (file.mkdirs()) {
|
||||
popParentCacheIfPresent()
|
||||
parent?.popCacheIfPresent(this)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -98,7 +98,7 @@ class RawFile(parent: UniFile?, private var file: File) : UniFile(parent) {
|
||||
if (file.exists()) return file.isFile
|
||||
parent?.ensureDir()
|
||||
val success = file.createNewFile()
|
||||
if (success) popParentCacheIfPresent()
|
||||
if (success) parent?.popCacheIfPresent(this)
|
||||
return success
|
||||
}
|
||||
|
||||
@@ -108,9 +108,13 @@ class RawFile(parent: UniFile?, private var file: File) : UniFile(parent) {
|
||||
|
||||
override fun exists() = file.exists()
|
||||
|
||||
override fun listFiles() = allChildren
|
||||
override fun listFiles() = synchronized(allChildren) {
|
||||
allChildren.toList()
|
||||
}
|
||||
|
||||
override fun findFirst(filter: (String) -> Boolean) = allChildren.firstOrNull { filter(it.name) }
|
||||
override fun findFirst(filter: (String) -> Boolean) = synchronized(allChildren) {
|
||||
allChildren.firstOrNull { filter(it.name) }
|
||||
}
|
||||
|
||||
override fun renameTo(displayName: String): Boolean {
|
||||
val target = File(file.parentFile, displayName)
|
||||
@@ -122,9 +126,6 @@ class RawFile(parent: UniFile?, private var file: File) : UniFile(parent) {
|
||||
}
|
||||
|
||||
override fun openFileDescriptor(mode: String): ParcelFileDescriptor = open(file, parseMode(mode))
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
override fun asImageSource() = ImageDecoder.createSource(file)
|
||||
}
|
||||
|
||||
private fun getTypeForName(name: String): String {
|
||||
|
||||
@@ -36,7 +36,11 @@ class TreeDocumentFile(
|
||||
}
|
||||
|
||||
private fun popCacheIfPresent(file: TreeDocumentFile) = apply {
|
||||
if (cachePresent) allChildren.add(file)
|
||||
if (cachePresent) {
|
||||
synchronized(allChildren) {
|
||||
allChildren.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(parent: UniFile?, uri: Uri) : this(parent, uri, getFilenameForUri(uri))
|
||||
@@ -135,9 +139,13 @@ class TreeDocumentFile(
|
||||
|
||||
override fun exists() = DocumentsContractApi19.exists(uri)
|
||||
|
||||
override fun listFiles() = allChildren
|
||||
override fun listFiles() = synchronized(allChildren) {
|
||||
allChildren.toList()
|
||||
}
|
||||
|
||||
override fun findFirst(filter: (String) -> Boolean) = allChildren.firstOrNull { filter(it.filename) }
|
||||
override fun findFirst(filter: (String) -> Boolean) = synchronized(allChildren) {
|
||||
allChildren.firstOrNull { filter(it.filename) }
|
||||
}
|
||||
|
||||
override fun renameTo(displayName: String): Boolean {
|
||||
val result = DocumentsContractApi21.renameTo(appCtx, uri, displayName)
|
||||
|
||||
@@ -17,11 +17,8 @@ package com.hippo.unifile
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.provider.DocumentsContractCompat
|
||||
import java.io.File
|
||||
@@ -206,9 +203,6 @@ abstract class UniFile internal constructor(private val parent: UniFile?) {
|
||||
|
||||
open fun openFileDescriptor(mode: String) = appCtx.contentResolver.openFileDescriptor(uri, mode) ?: error("Can't open ParcelFileDescriptor")
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
open fun asImageSource() = ImageDecoder.createSource(appCtx.contentResolver, uri)
|
||||
|
||||
/**
|
||||
* Test there is a file with the display name in the directory.
|
||||
*
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package com.mikepenz.aboutlibraries.ui.compose
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.entity.Library
|
||||
import com.mikepenz.aboutlibraries.util.withJson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Displays all provided libraries in a simple list.
|
||||
*/
|
||||
@Composable
|
||||
fun LibrariesContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
librariesBlock: (Context) -> Libs = { context ->
|
||||
Libs.Builder().withJson(context, R.raw.aboutlibraries).build()
|
||||
},
|
||||
showAuthor: Boolean = true,
|
||||
showVersion: Boolean = true,
|
||||
showLicenseBadges: Boolean = true,
|
||||
padding: LibraryPadding = LibraryDefaults.libraryPadding(),
|
||||
itemContentPadding: PaddingValues = LibraryDefaults.ContentPadding,
|
||||
itemSpacing: Dp = LibraryDefaults.LibraryItemSpacing,
|
||||
header: (LazyListScope.() -> Unit)? = null,
|
||||
onLibraryClick: ((Library) -> Unit)? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val libraries = produceState<Libs?>(null) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
librariesBlock(context)
|
||||
}
|
||||
}
|
||||
|
||||
val libs = libraries.value?.libraries?.sortedBy { it.name.lowercase() }
|
||||
if (libs != null) {
|
||||
Libraries(
|
||||
libraries = libs,
|
||||
modifier = modifier,
|
||||
lazyListState = lazyListState,
|
||||
contentPadding = contentPadding,
|
||||
showAuthor = showAuthor,
|
||||
showVersion = showVersion,
|
||||
showLicenseBadges = showLicenseBadges,
|
||||
padding = padding,
|
||||
itemContentPadding = itemContentPadding,
|
||||
itemSpacing = itemSpacing,
|
||||
header = header,
|
||||
onLibraryClick = { library ->
|
||||
if (onLibraryClick != null) {
|
||||
onLibraryClick.invoke(library)
|
||||
} else {
|
||||
library.website?.let { context.openBrowser(it) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
package com.mikepenz.aboutlibraries.ui.compose
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SuggestionChip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import com.mikepenz.aboutlibraries.entity.Library
|
||||
import com.mikepenz.aboutlibraries.ui.compose.util.author
|
||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||
|
||||
/**
|
||||
* Displays all provided libraries in a simple list.
|
||||
*/
|
||||
@Composable
|
||||
fun Libraries(
|
||||
libraries: List<Library>,
|
||||
modifier: Modifier = Modifier,
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
showAuthor: Boolean = true,
|
||||
showVersion: Boolean = true,
|
||||
showLicenseBadges: Boolean = true,
|
||||
padding: LibraryPadding = LibraryDefaults.libraryPadding(),
|
||||
itemContentPadding: PaddingValues = LibraryDefaults.ContentPadding,
|
||||
itemSpacing: Dp = LibraryDefaults.LibraryItemSpacing,
|
||||
header: (LazyListScope.() -> Unit)? = null,
|
||||
onLibraryClick: ((Library) -> Unit)? = null,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||
state = lazyListState,
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
header?.invoke(this)
|
||||
libraryItems(
|
||||
libraries,
|
||||
showAuthor,
|
||||
showVersion,
|
||||
showLicenseBadges,
|
||||
padding,
|
||||
itemContentPadding,
|
||||
) {
|
||||
onLibraryClick?.invoke(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun LazyListScope.libraryItems(
|
||||
libraries: List<Library>,
|
||||
showAuthor: Boolean = true,
|
||||
showVersion: Boolean = true,
|
||||
showLicenseBadges: Boolean = true,
|
||||
padding: LibraryPadding,
|
||||
itemContentPadding: PaddingValues = LibraryDefaults.ContentPadding,
|
||||
crossinline onLibraryClick: ((Library) -> Unit),
|
||||
) {
|
||||
items(libraries) { library ->
|
||||
Library(
|
||||
library,
|
||||
showAuthor,
|
||||
showVersion,
|
||||
showLicenseBadges,
|
||||
padding,
|
||||
itemContentPadding,
|
||||
) {
|
||||
onLibraryClick.invoke(library)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun Library(
|
||||
library: Library,
|
||||
showAuthor: Boolean = true,
|
||||
showVersion: Boolean = true,
|
||||
showLicenseBadges: Boolean = true,
|
||||
padding: LibraryPadding = LibraryDefaults.libraryPadding(),
|
||||
contentPadding: PaddingValues = LibraryDefaults.ContentPadding,
|
||||
typography: Typography = MaterialTheme.typography,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth().clickable { onClick.invoke() }.padding(contentPadding)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = library.name,
|
||||
modifier = Modifier.padding(padding.namePadding).weight(1f),
|
||||
style = typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
val version = library.artifactVersion
|
||||
if (version != null && showVersion) {
|
||||
Text(
|
||||
version,
|
||||
modifier = Modifier.padding(padding.versionPadding),
|
||||
style = typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
val author = library.author
|
||||
if (showAuthor && author.isNotBlank()) {
|
||||
Text(
|
||||
text = author,
|
||||
style = typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
var openDialog by remember { mutableStateOf(false) }
|
||||
if (openDialog) {
|
||||
LicenseDialog(library = library) {
|
||||
openDialog = false
|
||||
}
|
||||
}
|
||||
if (showLicenseBadges && library.licenses.isNotEmpty()) {
|
||||
Row {
|
||||
library.licenses.forEach {
|
||||
SuggestionChip(
|
||||
onClick = { openDialog = true },
|
||||
label = { Text(text = it.name) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LicenseDialog(
|
||||
library: Library,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column(modifier = Modifier.verticalScroll(scrollState)) {
|
||||
HtmlText(html = library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HtmlText(
|
||||
html: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { context -> TextView(context) },
|
||||
update = { it.text = html.parseAsHtml(flags = HtmlCompat.FROM_HTML_MODE_COMPACT) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the default values used by [Library]
|
||||
*/
|
||||
object LibraryDefaults {
|
||||
private val LibraryItemPadding = 16.dp
|
||||
private val LibraryNamePaddingTop = 4.dp
|
||||
private val LibraryVersionPaddingStart = 8.dp
|
||||
private val LibraryBadgePaddingTop = 8.dp
|
||||
private val LibraryBadgePaddingEnd = 4.dp
|
||||
internal val LibraryItemSpacing = 0.dp
|
||||
|
||||
/**
|
||||
* The default content padding used by [Library]
|
||||
*/
|
||||
val ContentPadding = PaddingValues(LibraryItemPadding)
|
||||
|
||||
/**
|
||||
* Creates a [LibraryPadding] that represents the default paddings used in a [Library]
|
||||
*
|
||||
* @param namePadding the padding around the name shown as part of a [Library]
|
||||
* @param versionPadding the padding around the version shown as part of a [Library]
|
||||
* @param badgePadding the padding around a badge element shown as part of a [Library]
|
||||
* @param badgeContentPadding the padding around the content of a badge element shown as part of a [Library]
|
||||
*/
|
||||
@Composable
|
||||
fun libraryPadding(
|
||||
namePadding: PaddingValues = PaddingValues(top = LibraryNamePaddingTop),
|
||||
versionPadding: PaddingValues = PaddingValues(start = LibraryVersionPaddingStart),
|
||||
badgePadding: PaddingValues = PaddingValues(
|
||||
top = LibraryBadgePaddingTop,
|
||||
end = LibraryBadgePaddingEnd,
|
||||
),
|
||||
badgeContentPadding: PaddingValues = PaddingValues(4.dp),
|
||||
): LibraryPadding = DefaultLibraryPadding(
|
||||
namePadding = namePadding,
|
||||
versionPadding = versionPadding,
|
||||
badgePadding = badgePadding,
|
||||
badgeContentPadding = badgeContentPadding,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the padding values used in a library.
|
||||
*/
|
||||
@Stable
|
||||
interface LibraryPadding {
|
||||
/** Represents the padding around the name shown as part of a [Library] */
|
||||
val namePadding: PaddingValues
|
||||
|
||||
/** Represents the padding around the version shown as part of a [Library] */
|
||||
val versionPadding: PaddingValues
|
||||
|
||||
/** Represents the padding around a badge element shown as part of a [Library] */
|
||||
val badgePadding: PaddingValues
|
||||
|
||||
/** Represents the padding around the content of a badge element shown as part of a [Library] */
|
||||
val badgeContentPadding: PaddingValues
|
||||
}
|
||||
|
||||
/**
|
||||
* Default [LibraryPadding].
|
||||
*/
|
||||
@Immutable
|
||||
private class DefaultLibraryPadding(
|
||||
override val namePadding: PaddingValues,
|
||||
override val versionPadding: PaddingValues,
|
||||
override val badgePadding: PaddingValues,
|
||||
override val badgeContentPadding: PaddingValues,
|
||||
) : LibraryPadding
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.mikepenz.aboutlibraries.ui.compose.util
|
||||
|
||||
import com.mikepenz.aboutlibraries.entity.Library
|
||||
import com.mikepenz.aboutlibraries.entity.License
|
||||
|
||||
val Library.author: String
|
||||
get() = developers.takeIf { it.isNotEmpty() }?.map { it.name }?.joinToString(", ") ?: organization?.name ?: ""
|
||||
|
||||
val License.htmlReadyLicenseContent: String?
|
||||
get() = licenseContent?.replace("\n", "<br />")
|
||||
|
||||
val License.strippedLicenseContent: String?
|
||||
get() = licenseContent?.replace("<br />", "\n")?.replace("<br/>", "\n")
|
||||
@@ -47,14 +47,12 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
@@ -170,42 +168,38 @@ class ReaderActivity : EhActivity() {
|
||||
if (ACTION_EH == mAction) {
|
||||
mGalleryInfo?.let { mGalleryProvider = EhPageLoader(it) }
|
||||
} else if (Intent.ACTION_VIEW == mAction) {
|
||||
if (isAtLeastP) {
|
||||
if (mUri != null) {
|
||||
try {
|
||||
grantUriPermission(
|
||||
BuildConfig.APPLICATION_ID,
|
||||
mUri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (mUri != null) {
|
||||
try {
|
||||
grantUriPermission(
|
||||
BuildConfig.APPLICATION_ID,
|
||||
mUri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
mGalleryProvider = ArchivePageLoader(
|
||||
appCtx,
|
||||
mUri!!,
|
||||
) { 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)
|
||||
}
|
||||
mGalleryProvider = ArchivePageLoader(
|
||||
appCtx,
|
||||
mUri!!,
|
||||
) { 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)
|
||||
}
|
||||
}.onFailure {
|
||||
finish()
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
finish()
|
||||
}.getOrThrow()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "Archives are not supported before Android P", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +290,7 @@ class ReaderActivity : EhActivity() {
|
||||
dialogState.Intercept()
|
||||
|
||||
val showSeekbar by Settings.showReaderSeekbar.collectAsState()
|
||||
val sliderValueFlow = remember { MutableStateFlow(0) }
|
||||
val sliderValueFlow = remember { MutableStateFlow(-1) }
|
||||
LaunchedEffect(sliderValueFlow) {
|
||||
sliderValueFlow.drop(1).debounce(200).collect {
|
||||
moveToPageIndex(it)
|
||||
@@ -744,17 +738,11 @@ class ReaderActivity : EhActivity() {
|
||||
fun onPageLongTap(page: ReaderPage) {
|
||||
lifecycleScope.launch {
|
||||
dialogState.dialog {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun dismiss() = it.cancel()
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { dismiss() },
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = { it.cancel() },
|
||||
windowInsets = WindowInsets(0),
|
||||
) {
|
||||
ReaderPageSheet(page) {
|
||||
coroutineScope.launch { sheetState.hide() }
|
||||
}
|
||||
ReaderPageSheet(page) { it.cancel() }
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
@@ -844,6 +832,10 @@ class ReaderActivity : EhActivity() {
|
||||
.onEach { setGallery() }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
ReaderPreferences.cropBorders().changes()
|
||||
.onEach { restartGalleryProvider() }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
ReaderPreferences.readerTheme().changes()
|
||||
.onEach { theme ->
|
||||
binding.readerContainer.setBackgroundResource(
|
||||
|
||||
@@ -19,7 +19,7 @@ abstract class PageLoader {
|
||||
} else {
|
||||
(OSUtils.appMaxMemory / 3 * 2).toInt()
|
||||
},
|
||||
sizeOf = { _, v -> v.size },
|
||||
sizeOf = { _, v -> v.size.toInt() },
|
||||
onEntryRemoved = { _, _, o, _ -> o.recycle() },
|
||||
)
|
||||
val mPages by lazy {
|
||||
|
||||
+4
-10
@@ -37,6 +37,8 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
|
||||
init {
|
||||
addView(binding.root)
|
||||
|
||||
context as ReaderActivity
|
||||
|
||||
if (isAtLeastO) {
|
||||
binding.wideColorGamut.isEnabled = Image.isWideColorGamut
|
||||
if (!Image.isWideColorGamut) {
|
||||
@@ -44,28 +46,20 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
|
||||
}
|
||||
binding.wideColorGamut.bindToPreference(readerPreferences.wideColorGamut())
|
||||
|
||||
val activity = context as ReaderActivity
|
||||
|
||||
readerPreferences.cropBorders().asHotFlow {
|
||||
activity.restartGalleryProvider()
|
||||
}.launchIn(activity.lifecycleScope)
|
||||
|
||||
readerPreferences.wideColorGamut().asHotFlow {
|
||||
Image.colorSpace = ColorSpace.get(if (Image.isWideColorGamut && it) ColorSpace.Named.DISPLAY_P3 else ColorSpace.Named.SRGB)
|
||||
val colorMode = if (Image.isWideColorGamut && it) ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT else ActivityInfo.COLOR_MODE_DEFAULT
|
||||
with(activity) {
|
||||
with(context) {
|
||||
if (window.colorMode != colorMode) {
|
||||
window.colorMode = colorMode
|
||||
restartGalleryProvider()
|
||||
}
|
||||
}
|
||||
}.launchIn(activity.lifecycleScope)
|
||||
}.launchIn(context.lifecycleScope)
|
||||
} else {
|
||||
binding.wideColorGamut.isVisible = false
|
||||
}
|
||||
|
||||
context as ReaderActivity
|
||||
|
||||
readerPreferences.colorFilter().changes()
|
||||
.onEach { setColorFilter(it) }
|
||||
.launchIn(context.lifecycleScope)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.GestureDetector
|
||||
@@ -18,10 +19,13 @@ import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.core.view.isVisible
|
||||
import coil3.BitmapImage
|
||||
import coil3.DrawableImage
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.EASE_OUT_QUAD
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.SCALE_TYPE_CENTER_INSIDE
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
@@ -120,14 +124,16 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setImage(drawable: Drawable, config: Config) {
|
||||
fun setImage(drawable: Image, config: Config) {
|
||||
this.config = config
|
||||
if (drawable is Animatable) {
|
||||
val image = drawable.innerImage ?: return
|
||||
if (image is DrawableImage) {
|
||||
prepareAnimatedImageView()
|
||||
setAnimatedImage(drawable, config)
|
||||
} else {
|
||||
setAnimatedImage(image.drawable, config)
|
||||
}
|
||||
if (image is BitmapImage) {
|
||||
prepareNonAnimatedImageView()
|
||||
setNonAnimatedImage(drawable, config)
|
||||
setNonAnimatedImage(image.bitmap, drawable.rect, config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +239,8 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun setNonAnimatedImage(
|
||||
image: Drawable,
|
||||
image: Bitmap,
|
||||
bounds: Rect,
|
||||
config: Config,
|
||||
) = (pageView as? SubsamplingScaleImageView)?.apply {
|
||||
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
|
||||
@@ -254,7 +261,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
},
|
||||
)
|
||||
|
||||
setImage(image as BitmapDrawable)
|
||||
setImage(image, bounds)
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.view.isVisible
|
||||
import coil3.BitmapImage
|
||||
import com.hippo.ehviewer.databinding.ReaderErrorBinding
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
@@ -97,7 +97,7 @@ class PagerPageHolder(
|
||||
setDownloading()
|
||||
}
|
||||
Page.State.READY -> {
|
||||
page.image?.mObtainedDrawable?.let { setImage(it) }
|
||||
page.image?.let { setImage(it) }
|
||||
cancelProgressJob()
|
||||
}
|
||||
Page.State.ERROR -> {
|
||||
@@ -148,12 +148,12 @@ class PagerPageHolder(
|
||||
/**
|
||||
* Called when the page is ready.
|
||||
*/
|
||||
private fun setImage(drawable: Drawable) {
|
||||
private fun setImage(image: Image) {
|
||||
progressIndicator.setProgress(0)
|
||||
progressIndicator.hide()
|
||||
errorLayout?.root?.isVisible = false
|
||||
setImage(
|
||||
drawable,
|
||||
image,
|
||||
Config(
|
||||
zoomDuration = viewer.config.doubleTapAnimDuration,
|
||||
minimumScaleType = viewer.config.imageScaleType,
|
||||
@@ -162,7 +162,7 @@ class PagerPageHolder(
|
||||
landscapeZoom = viewer.config.landscapeZoom,
|
||||
),
|
||||
)
|
||||
if (drawable !is Animatable) {
|
||||
if (image.innerImage is BitmapImage) {
|
||||
pageBackground = background
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
@@ -10,6 +9,7 @@ import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.hippo.ehviewer.databinding.ReaderErrorBinding
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
@@ -145,7 +145,7 @@ class WebtoonPageHolder(
|
||||
setDownloading()
|
||||
}
|
||||
Page.State.READY -> {
|
||||
page?.image?.mObtainedDrawable?.let { setImage(it) }
|
||||
page?.image?.let { setImage(it) }
|
||||
cancelProgressJob()
|
||||
}
|
||||
Page.State.ERROR -> {
|
||||
@@ -194,7 +194,7 @@ class WebtoonPageHolder(
|
||||
/**
|
||||
* Called when the page is ready.
|
||||
*/
|
||||
private fun setImage(drawable: Drawable) {
|
||||
private fun setImage(drawable: Image) {
|
||||
progressIndicator.setProgress(0)
|
||||
progressIndicator.hide()
|
||||
removeErrorLayout()
|
||||
|
||||
@@ -4,6 +4,7 @@ import arrow.core.memoize
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class NamedMutex<T> {
|
||||
val innerMutexGetter = { _: T -> Mutex() }.memoize()
|
||||
@@ -18,12 +19,5 @@ suspend inline fun <T, K> NamedMutex<K>.withLock(key: K, owner: Any? = null, act
|
||||
innerMutexGetter(key)
|
||||
}
|
||||
|
||||
mutex.run {
|
||||
lock(owner)
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
unlock(owner)
|
||||
}
|
||||
}
|
||||
return mutex.withLock(owner, action)
|
||||
}
|
||||
@@ -508,6 +508,7 @@
|
||||
<string name="list_tile_thumb_size">详情模式下缩略图大小</string>
|
||||
<string name="account_name">账户</string>
|
||||
<string name="preload_thumb_aggressively">激进地预载缩略图</string>
|
||||
<string name="predictive_nav_anim">[实验性] 预见式返回导航动画</string>
|
||||
<string name="auto_updates">自动检查更新</string>
|
||||
<string name="update_frequency_never">从不</string>
|
||||
<string name="update_frequency_daily">每天</string>
|
||||
|
||||
@@ -568,9 +568,10 @@
|
||||
<string name="archive_need_passwd">Archive need password</string>
|
||||
<string name="passwd_wrong">Password Wrong</string>
|
||||
<string name="passwd_cannot_be_empty">Password can\'t be empty</string>
|
||||
<string name="list_tile_thumb_size">Thumb size in thumb mode</string>
|
||||
<string name="list_tile_thumb_size">Thumb size in detail mode</string>
|
||||
<string name="account_name">Account</string>
|
||||
<string name="preload_thumb_aggressively">Preload thumbs aggressively</string>
|
||||
<string name="predictive_nav_anim">[Experimental] Predictive back navigation animations</string>
|
||||
<string name="user_agent">User Agent</string>
|
||||
<string name="auto_updates">Automatically check for updates</string>
|
||||
<string name="update_frequency_never">Never</string>
|
||||
|
||||
@@ -18,7 +18,7 @@ use log::LevelFilter;
|
||||
use serde::Serialize;
|
||||
use std::ffi::c_void;
|
||||
use std::io::Cursor;
|
||||
use std::ptr::{slice_from_raw_parts, slice_from_raw_parts_mut};
|
||||
use std::ptr::slice_from_raw_parts_mut;
|
||||
use std::str::from_utf8_unchecked;
|
||||
use tl::{Bytes, Node, NodeHandle, Parser, VDom};
|
||||
|
||||
@@ -62,39 +62,36 @@ where
|
||||
handle.get(parser)
|
||||
}
|
||||
|
||||
fn parse_marshal_inplace<F, R>(env: &mut JNIEnv, str: JByteBuffer, limit: jint, mut f: F) -> i32
|
||||
fn deref_direct_bytebuffer<'a>(env: &JNIEnv, buffer: &JByteBuffer) -> &'a mut [u8] {
|
||||
let ptr = env.get_direct_buffer_address(buffer).unwrap();
|
||||
let cap = env.get_direct_buffer_capacity(buffer).unwrap();
|
||||
unsafe { &mut *slice_from_raw_parts_mut(ptr, cap) }
|
||||
}
|
||||
|
||||
fn throw_msg(env: &mut JNIEnv, msg: &str) -> i32 {
|
||||
env.throw_new("java/lang/RuntimeException", msg).ok();
|
||||
0
|
||||
}
|
||||
|
||||
fn parse_marshal_inplace<F, R>(env: &mut JNIEnv, str: JByteBuffer, limit: jint, f: F) -> i32
|
||||
where
|
||||
F: FnMut(&VDom, &Parser, &str) -> Result<R, &'static str>,
|
||||
F: Fn(&VDom, &str) -> Result<R, &'static str>,
|
||||
R: Serialize,
|
||||
{
|
||||
let ptr = env.get_direct_buffer_address(&str).unwrap();
|
||||
let html = unsafe {
|
||||
let buff = slice_from_raw_parts(ptr, limit as usize);
|
||||
from_utf8_unchecked(&*buff)
|
||||
};
|
||||
let mut f = || {
|
||||
let buffer = deref_direct_bytebuffer(env, &str);
|
||||
let html = unsafe { from_utf8_unchecked(&buffer[..limit as usize]) };
|
||||
let f = || {
|
||||
let dom = tl::parse(html, tl::ParserOptions::default()).map_err(|_| html)?;
|
||||
let parser = dom.parser();
|
||||
f(&dom, parser, html)
|
||||
f(&dom, html)
|
||||
};
|
||||
match f() {
|
||||
// Nothing to marshal
|
||||
Err(err) => {
|
||||
env.throw_new("java/lang/RuntimeException", err).ok();
|
||||
0
|
||||
}
|
||||
Err(err) => throw_msg(env, err),
|
||||
Ok(value) => {
|
||||
let mut cursor = unsafe {
|
||||
let slice = slice_from_raw_parts_mut(ptr, 0x80000);
|
||||
Cursor::new(&mut *slice)
|
||||
};
|
||||
let mut cursor = Cursor::new(buffer);
|
||||
match serde_cbor::to_writer(&mut cursor, &value) {
|
||||
Ok(_) => cursor.position() as i32,
|
||||
Err(err) => {
|
||||
env.throw_new("java/lang/RuntimeException", format!("{}", err))
|
||||
.ok();
|
||||
0
|
||||
}
|
||||
Err(err) => throw_msg(env, &format!("{}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ fn parse_fav(dom: &VDom, parser: &Parser, html: &str) -> Result<FavResult, &'sta
|
||||
#[allow(non_snake_case)]
|
||||
#[jni_fn("com.hippo.ehviewer.client.parser.FavoritesParserKt")]
|
||||
pub fn parseFav(mut env: JNIEnv, _class: JClass, input: JByteBuffer, limit: jint) -> jint {
|
||||
parse_marshal_inplace(&mut env, input, limit, |dom, parser, html| {
|
||||
parse_fav(dom, parser, html)
|
||||
parse_marshal_inplace(&mut env, input, limit, |dom, html| {
|
||||
parse_fav(dom, dom.parser(), html)
|
||||
})
|
||||
}
|
||||
@@ -33,7 +33,7 @@ fn parse_limit(dom: &VDom, parser: &Parser) -> Option<Limits> {
|
||||
#[allow(non_snake_case)]
|
||||
#[jni_fn("com.hippo.ehviewer.client.parser.HomeParserKt")]
|
||||
pub fn parseLimit(mut env: JNIEnv, _class: JClass, input: JByteBuffer, limit: jint) -> jint {
|
||||
parse_marshal_inplace(&mut env, input, limit, |dom, parser, _| {
|
||||
parse_limit(dom, parser).ok_or("Can't parse Limit")
|
||||
parse_marshal_inplace(&mut env, input, limit, |dom, _| {
|
||||
parse_limit(dom, dom.parser()).ok_or("Can't parse Limit")
|
||||
})
|
||||
}
|
||||
@@ -194,9 +194,6 @@ pub fn parse_info_list(
|
||||
parser: &Parser,
|
||||
str: &str,
|
||||
) -> Result<GalleryListResult, &'static str> {
|
||||
if !str.contains('<') {
|
||||
return Err("No content!");
|
||||
}
|
||||
if str.contains("<p>You do not have any watched tags") {
|
||||
return Err("No watched tags!");
|
||||
}
|
||||
@@ -241,7 +238,7 @@ pub fn parse_info_list(
|
||||
#[allow(non_snake_case)]
|
||||
#[jni_fn("com.hippo.ehviewer.client.parser.GalleryListParserKt")]
|
||||
pub fn parseGalleryInfoList(mut env: JNIEnv, _: JClass, buffer: JByteBuffer, limit: jint) -> jint {
|
||||
parse_marshal_inplace(&mut env, buffer, limit, |dom, parser, str| {
|
||||
parse_info_list(dom, parser, str)
|
||||
parse_marshal_inplace(&mut env, buffer, limit, |dom, str| {
|
||||
parse_info_list(dom, dom.parser(), str)
|
||||
})
|
||||
}
|
||||
@@ -44,7 +44,7 @@ fn parse_torrent_list(dom: &VDom, parser: &Parser) -> Result<Vec<Torrent>, &'sta
|
||||
#[allow(non_snake_case)]
|
||||
#[jni_fn("com.hippo.ehviewer.client.parser.TorrentParserKt")]
|
||||
pub fn parseTorrent(mut env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit: jint) -> jint {
|
||||
parse_marshal_inplace(&mut env, buffer, limit, |dom, parser, _html| {
|
||||
parse_torrent_list(dom, parser)
|
||||
parse_marshal_inplace(&mut env, buffer, limit, |dom, _| {
|
||||
parse_torrent_list(dom, dom.parser())
|
||||
})
|
||||
}
|
||||
@@ -72,7 +72,7 @@
|
||||
| OSS | FOSS 依赖项** |
|
||||
| GMS | 专有 GMS 组件 |
|
||||
|
||||
\* 不支持压缩包和 GIF
|
||||
\* 可能不支持某些压缩包
|
||||
|
||||
** 在不支持 [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine) 的设备上回退到 OkHttp
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
| OSS | FOSS 依賴項** |
|
||||
| GMS | 專有 GMS 元件 |
|
||||
|
||||
\* 不支援壓縮包和 GIF
|
||||
\* 可能不支援某些壓縮包
|
||||
|
||||
** 在不支援 [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine) 的裝置上回退到 OkHttp
|
||||
|
||||
|
||||
+13
-12
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
aboutlibraries = "10.9.2"
|
||||
agp = "8.2.0"
|
||||
android-tools-r8 = "8.3.16-dev"
|
||||
aboutlibraries = "10.10.0"
|
||||
agp = "8.2.1"
|
||||
android-tools-r8 = "8.3.31"
|
||||
androidx-activity = "1.8.2"
|
||||
androidx-appcompat = "1.6.1"
|
||||
androidx-biometric = "1.2.0-alpha05"
|
||||
@@ -9,20 +9,20 @@ androidx-browser = "1.7.0"
|
||||
androidx-constraintlayout-compose = "1.1.0-alpha13"
|
||||
androidx-core = "1.12.0"
|
||||
androidx-core-splashscreen = "1.1.0-alpha02"
|
||||
androidx-lifecycle = "2.7.0-rc02"
|
||||
androidx-lifecycle = "2.7.0"
|
||||
androidx-navigation = "2.7.6"
|
||||
androidx-paging = "3.2.1"
|
||||
androidx-recyclerview = "1.3.2"
|
||||
androidx-room = "2.6.1"
|
||||
androidx-work = "2.9.0"
|
||||
arrow = "1.2.1"
|
||||
coil = "3.0.0-alpha01"
|
||||
compose-bom = "2023.12.00-alpha04"
|
||||
compose-compiler = "1.5.6-dev-k2.0.0-Beta1-06a03be2b42"
|
||||
compose-destinations = "1.9.56"
|
||||
google-android-material = "1.12.0-alpha02"
|
||||
kotlin = "2.0.0-Beta1"
|
||||
kotlinx-coroutines = "1.7.3"
|
||||
coil = "3.0.0-alpha02"
|
||||
compose-bom = "2024.01.00-alpha01"
|
||||
compose-compiler = "1.5.8-dev-k2.0.0-Beta2-99ed868a0f8"
|
||||
compose-destinations = "1.9.60"
|
||||
google-android-material = "1.12.0-alpha03"
|
||||
kotlin = "2.0.0-Beta2"
|
||||
kotlinx-coroutines = "1.8.0-RC2"
|
||||
kotlinx-datetime = "0.5.0"
|
||||
kotlinx-serialization = "1.6.2"
|
||||
ksp = "2.0.0-Beta2-1.0.16"
|
||||
@@ -34,6 +34,7 @@ splitties = "3.0.0"
|
||||
|
||||
[libraries]
|
||||
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" }
|
||||
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
|
||||
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||
@@ -137,4 +138,4 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref =
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
rustAndroidPlugin = { id = "org.mozilla.rust-android-gradle.rust-android", version = "0.9.3" }
|
||||
spotless = { id = "com.diffplug.spotless", version = "6.23.3" }
|
||||
spotless = { id = "com.diffplug.spotless", version = "6.24.0" }
|
||||
+1
-1
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io")
|
||||
maven("https://androidx.dev/storage/compose-compiler/repository/")
|
||||
maven("https://androidx.dev/snapshots/builds/11260879/artifacts/repository")
|
||||
maven("https://androidx.dev/snapshots/builds/11311962/artifacts/repository")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user