Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3cb6d2782 | ||
|
|
45be957df5 | ||
|
|
8e45014e3f | ||
|
|
d84e948352 | ||
|
|
68bd835038 | ||
|
|
c297bebea1 | ||
|
|
6c0a823216 | ||
|
|
067a701bdc | ||
|
|
c3fb008c92 | ||
|
|
bc4321da2e | ||
|
|
8dec040803 | ||
|
|
29e4e77c44 | ||
|
|
11429ef7fe | ||
|
|
f5fcd96a1e | ||
|
|
19120038c5 | ||
|
|
9021f28ead | ||
|
|
62934f72e3 | ||
|
|
6c534e1049 | ||
|
|
f80cd90792 | ||
|
|
3999c0c114 | ||
|
|
59399453c4 | ||
|
|
694bb19e2a | ||
|
|
9928ecd4d9 | ||
|
|
2c5293af6e | ||
|
|
9569ca774d | ||
|
|
3a04460926 | ||
|
|
acb4fb6166 | ||
|
|
7d137bb016 | ||
|
|
8c2ed37b72 | ||
|
|
749b230250 | ||
|
|
23531cb6e0 | ||
|
|
0ac11dbe0b | ||
|
|
9b0e017542 | ||
|
|
f2f5fcacf9 | ||
|
|
52ded265f3 | ||
|
|
e77f905c4a | ||
|
|
2d14d34d53 | ||
|
|
78facda18a | ||
|
|
1fe1b43015 | ||
|
|
63164dc70a | ||
|
|
0d39092a7f | ||
|
|
c50db8a327 | ||
|
|
7e107cc5e2 | ||
|
|
260949e3a7 | ||
|
|
d9ec108721 | ||
|
|
1e97407936 | ||
|
|
73afe05a18 | ||
|
|
23475de932 | ||
|
|
6fb2172c2c | ||
|
|
9226b71119 | ||
|
|
58e38b58bf | ||
|
|
db0d110627 | ||
|
|
08812bf7ff | ||
|
|
daf7f8ffca | ||
|
|
74564fa07f | ||
|
|
fc51d3fc81 | ||
|
|
c5d5b8abd2 | ||
|
|
193badccb8 | ||
|
|
2653d0568a | ||
|
|
adf5a65371 | ||
|
|
5b59dabc66 | ||
|
|
f7259c5b40 | ||
|
|
6f9fceaef8 |
No files matched your search
@@ -1,5 +1,26 @@
|
||||
# 变更日志
|
||||
|
||||
## [1.11.3] - 2024-04-06
|
||||
|
||||
### 改进
|
||||
|
||||
* 优化预加载逻辑和图片加载顺序
|
||||
* 优化画廊详情和评论页面 UI
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 某些模式下搜索结果不显示画廊语言 [#923](https://github.com/FooIbar/EhViewer/issues/923)
|
||||
* 某些情况下在搜索结果中直接阅读画廊后崩溃 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
||||
* 更新至最新版本后仍提示有新版本 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
||||
* 从旧版本升级时包含不兼容数据导致崩溃/无法导出数据 [#940](https://github.com/FooIbar/EhViewer/issues/940) [#974](https://github.com/FooIbar/EhViewer/issues/974)
|
||||
* Android 10 以下的设备打开 设置-隐私 时崩溃 [#953](https://github.com/FooIbar/EhViewer/issues/953)
|
||||
* 下载标签无法拖动
|
||||
* 刷新 和 刷新(原图) 无法在已下载的图片上使用
|
||||
* 临时 IP 封禁错误显示为解析失败
|
||||
* 某些情况下下载时闪退 [#980](https://github.com/FooIbar/EhViewer/issues/980)
|
||||
* 某些设备上下载列表中的缩略图无法显示 [#977](https://github.com/FooIbar/EhViewer/issues/977)
|
||||
|
||||
|
||||
## [1.11.2] - 2024-03-17
|
||||
|
||||
### 重大变更
|
||||
|
||||
+16
-17
@@ -62,8 +62,8 @@ android {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 180053
|
||||
versionName = "1.11.2"
|
||||
versionCode = 180054
|
||||
versionName = "1.11.3"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
@@ -80,7 +80,7 @@ android {
|
||||
"nb-rNO",
|
||||
),
|
||||
)
|
||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName$versionNameSuffix\"")
|
||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||
ndk {
|
||||
@@ -139,19 +139,13 @@ android {
|
||||
|
||||
lint {
|
||||
disable += setOf("MissingTranslation", "MissingQuantity")
|
||||
fatal += setOf("NewApi", "InlineApi")
|
||||
fatal += setOf("NewApi", "InlinedApi")
|
||||
}
|
||||
|
||||
packaging {
|
||||
dex {
|
||||
useLegacyPackaging = false
|
||||
}
|
||||
resources {
|
||||
excludes += "/META-INF/**"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo.includeInApk = false
|
||||
@@ -183,6 +177,17 @@ android {
|
||||
namespace = "com.hippo.ehviewer"
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().withBuildType("release")) {
|
||||
it.packaging.resources.excludes.addAll(
|
||||
"/META-INF/**",
|
||||
"/kotlin/**",
|
||||
"**.txt",
|
||||
"**.bin",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@@ -263,13 +268,6 @@ dependencies {
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
// Workaround for https://issuetracker.google.com/329489167
|
||||
force("androidx.compose.foundation:foundation-android:1.7.0-alpha03")
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
@@ -277,6 +275,7 @@ kotlin {
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.generateKotlin", "true")
|
||||
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
|
||||
Vendored
+5
@@ -11,6 +11,11 @@
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
# Workaround for https://issuetracker.google.com/331556916
|
||||
-keepclassmembers class androidx.biometric.BiometricManager {
|
||||
public int canAuthenticate(int);
|
||||
}
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
From d1db03ebd9989d10a5a905b792b712c6487a0057 Mon Sep 17 00:00:00 2001
|
||||
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||
Date: Sat, 24 Feb 2024 12:03:15 +0000
|
||||
Subject: [PATCH] Don't build other binaries
|
||||
|
||||
---
|
||||
CMakeLists.txt | 8 ++++----
|
||||
1 file changed, 4 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index a346dd02..52b5d0d8 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -1567,7 +1567,7 @@ endif()
|
||||
# xzdec and lzmadec
|
||||
#############################################################################
|
||||
|
||||
-if(HAVE_DECODERS AND (NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900))
|
||||
+if(0)
|
||||
foreach(XZDEC xzdec lzmadec)
|
||||
add_executable("${XZDEC}"
|
||||
src/common/sysdefs.h
|
||||
@@ -1624,7 +1624,7 @@ endif()
|
||||
# lzmainfo
|
||||
#############################################################################
|
||||
|
||||
-if(HAVE_DECODERS AND (NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900))
|
||||
+if(0)
|
||||
add_executable(lzmainfo
|
||||
src/common/sysdefs.h
|
||||
src/common/tuklib_common.h
|
||||
@@ -1680,7 +1680,7 @@ endif()
|
||||
# xz
|
||||
#############################################################################
|
||||
|
||||
-if(NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900)
|
||||
+if(0)
|
||||
add_executable(xz
|
||||
src/common/mythread.h
|
||||
src/common/sysdefs.h
|
||||
@@ -1914,7 +1914,7 @@ endif()
|
||||
# Scripts
|
||||
#############################################################################
|
||||
|
||||
-if(UNIX)
|
||||
+if(0)
|
||||
# NOTE: This isn't as sophisticated as in the Autotools build which
|
||||
# uses posix-shell.m4 but hopefully this doesn't need to be either.
|
||||
# CMake likely won't be used on as many (old) obscure systems as the
|
||||
--
|
||||
2.34.1
|
||||
|
||||
@@ -20,8 +20,9 @@ option(BUILD_TESTING OFF)
|
||||
|
||||
FetchContent_Declare(
|
||||
liblzma
|
||||
GIT_REPOSITORY https://github.com/FooIbar/xz.git
|
||||
GIT_REPOSITORY https://git.tukaani.org/xz.git
|
||||
GIT_TAG v5.6.1
|
||||
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch || cd .
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(liblzma)
|
||||
|
||||
@@ -127,8 +127,6 @@ static long archive_map_entries_index(archive_ctx *ctx, bool sort) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
if (sort)
|
||||
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||
return count;
|
||||
@@ -171,8 +169,6 @@ static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
if (archive_entry_is_playable(ctx->entry))
|
||||
count++;
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -306,6 +302,11 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
} else {
|
||||
entryCount = archive_list_all_entries(ctx);
|
||||
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
|
||||
if (!entryCount) {
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
archive_release_ctx(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We must read through the file|vm then we can know whether it is encrypted
|
||||
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.SearchBarDefaults.InputFieldHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun SearchBarInputField(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val textColor = LocalTextStyle.current.color.takeOrElse {
|
||||
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
state = state,
|
||||
modifier = modifier.sizeIn(
|
||||
minWidth = SearchBarMinWidth,
|
||||
maxWidth = SearchBarMaxWidth,
|
||||
minHeight = InputFieldHeight,
|
||||
).focusRequester(focusRequester).onFocusChanged {
|
||||
if (it.isFocused) onExpandedChange(true)
|
||||
}.fillMaxWidth(),
|
||||
enabled = enabled,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
|
||||
cursorBrush = SolidColor(colors.cursorColor),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
onKeyboardAction = { onSearch() },
|
||||
interactionSource = interactionSource,
|
||||
decorator = @Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
innerTextField = innerTextField,
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon?.let { leading -> { Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() } } },
|
||||
trailingIcon = trailingIcon?.let { trailing -> { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } } },
|
||||
shape = SearchBarDefaults.inputFieldShape,
|
||||
colors = colors,
|
||||
contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
|
||||
container = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val shouldClearFocus = !expanded && focused
|
||||
LaunchedEffect(expanded) {
|
||||
if (shouldClearFocus) {
|
||||
// Not strictly needed according to the motion spec, but since the animation
|
||||
// already has a delay, this works around b/261632544.
|
||||
delay(AnimationDelayMillis.toLong())
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val SearchBarIconOffsetX = 4.dp
|
||||
val SearchBarMinWidth = 360.dp
|
||||
val SearchBarMaxWidth = 720.dp
|
||||
val SearchBarHorizontalPadding = 16.dp
|
||||
const val AnimationDelayMillis = 100
|
||||
@@ -1,591 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.MutatorMutex
|
||||
import androidx.compose.foundation.interaction.Interaction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.MutableWindowInsets
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.GenericShape
|
||||
import androidx.compose.foundation.text.BasicTextField2
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.SearchBarDefaults.InputFieldHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableFloatState
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.geometry.toRect
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.semantics.onClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.lerp
|
||||
import androidx.compose.ui.unit.offset
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.compose.ui.zIndex
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sign
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* <a href="https://m3.material.io/components/search/overview" class="external" target="_blank">Material Design search</a>.
|
||||
*
|
||||
* A search bar represents a floating search field that allows users to enter a keyword or phrase
|
||||
* and get relevant information. It can be used as a way to navigate through an app via search
|
||||
* queries.
|
||||
*
|
||||
* An active search bar expands into a search "view" and can be used to display dynamic suggestions.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* A [SearchBar] expands to occupy the entirety of its allowed size when active. For full-screen
|
||||
* behavior as specified by Material guidelines, parent layouts of the [SearchBar] must not pass
|
||||
* any [Constraints] that limit its size, and the host activity should set
|
||||
* `WindowCompat.setDecorFitsSystemWindows(window, false)`.
|
||||
*
|
||||
* If this expansion behavior is undesirable, for example on large tablet screens, [DockedSearchBar]
|
||||
* can be used instead.
|
||||
*
|
||||
* @param state the query text to be shown in the search bar's input field
|
||||
* @param onSearch the callback to be invoked when the input service triggers the [ImeAction.Search]
|
||||
* action.
|
||||
* @param active whether this search bar is active
|
||||
* @param onActiveChange the callback to be invoked when this search bar's active state is changed
|
||||
* @param modifier the [Modifier] to be applied to this search bar
|
||||
* @param enabled controls the enabled state of this search bar. When `false`, this component will
|
||||
* not respond to user input, and it will appear visually disabled and disabled to accessibility
|
||||
* services.
|
||||
* @param placeholder the placeholder to be displayed when the search bar's query is empty.
|
||||
* @param leadingIcon the leading icon to be displayed at the beginning of the search bar container
|
||||
* @param trailingIcon the trailing icon to be displayed at the end of the search bar container
|
||||
* @param shape the shape of this search bar when it is not [active]. When [active], the shape will
|
||||
* always be [SearchBarDefaults.fullScreenShape].
|
||||
* @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
|
||||
* in different states. See [SearchBarDefaults.colors].
|
||||
* @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
|
||||
* translucent primary color overlay is applied on top of the container. A higher tonal elevation
|
||||
* value will result in a darker color in light theme and lighter color in dark theme. See also:
|
||||
* [Surface].
|
||||
* @param shadowElevation the elevation for the shadow below the search bar
|
||||
* @param windowInsets the window insets that the search bar will respect
|
||||
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
|
||||
* emitting [Interaction]s for this search bar. You can use this to change the search bar's
|
||||
* appearance or preview the search bar in different states. Note that if `null` is provided,
|
||||
* interactions will still happen internally.
|
||||
* @param content the content of this search bar that will be displayed below the input field
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
active: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
||||
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
|
||||
shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val animationProgress = remember { Animatable(initialValue = if (active) 1f else 0f) }
|
||||
val finalBackProgress = remember { mutableFloatStateOf(Float.NaN) }
|
||||
val firstBackEvent = remember { mutableStateOf<BackEventCompat?>(null) }
|
||||
val currentBackEvent = remember { mutableStateOf<BackEventCompat?>(null) }
|
||||
|
||||
LaunchedEffect(active) {
|
||||
val animationInProgress = animationProgress.value > 0 && animationProgress.value < 1
|
||||
val animationSpec =
|
||||
if (animationInProgress) {
|
||||
AnimationPredictiveBackExitFloatSpec
|
||||
} else if (active) {
|
||||
AnimationEnterFloatSpec
|
||||
} else {
|
||||
AnimationExitFloatSpec
|
||||
}
|
||||
val targetValue = if (active) 1f else 0f
|
||||
if (animationProgress.value != targetValue) {
|
||||
animationProgress.animateTo(targetValue, animationSpec)
|
||||
}
|
||||
if (!active) {
|
||||
finalBackProgress.floatValue = Float.NaN
|
||||
firstBackEvent.value = null
|
||||
currentBackEvent.value = null
|
||||
}
|
||||
}
|
||||
|
||||
val mutatorMutex = remember { MutatorMutex() }
|
||||
PredictiveBackHandler(enabled = active) { progress ->
|
||||
mutatorMutex.mutate {
|
||||
try {
|
||||
finalBackProgress.floatValue = Float.NaN
|
||||
progress.collect { backEvent ->
|
||||
if (firstBackEvent.value == null) {
|
||||
firstBackEvent.value = backEvent
|
||||
}
|
||||
currentBackEvent.value = backEvent
|
||||
val interpolatedProgress = EaseOut.transform(backEvent.progress)
|
||||
animationProgress.snapTo(targetValue = 1 - interpolatedProgress)
|
||||
}
|
||||
finalBackProgress.floatValue = animationProgress.value
|
||||
onActiveChange(false)
|
||||
} catch (e: CancellationException) {
|
||||
animationProgress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = AnimationPredictiveBackExitFloatSpec,
|
||||
)
|
||||
finalBackProgress.floatValue = Float.NaN
|
||||
firstBackEvent.value = null
|
||||
currentBackEvent.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchBarInternal(
|
||||
animationProgress = animationProgress,
|
||||
finalBackProgress = finalBackProgress,
|
||||
firstBackEvent = firstBackEvent,
|
||||
currentBackEvent = currentBackEvent,
|
||||
state = state,
|
||||
onSearch = onSearch,
|
||||
active = active,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
title = title,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation,
|
||||
windowInsets = windowInsets,
|
||||
interactionSource = interactionSource,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalMaterial3Api
|
||||
@Composable
|
||||
internal fun SearchBarInternal(
|
||||
animationProgress: Animatable<Float, AnimationVector1D>,
|
||||
finalBackProgress: MutableFloatState,
|
||||
firstBackEvent: MutableState<BackEventCompat?>,
|
||||
currentBackEvent: MutableState<BackEventCompat?>,
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
active: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
||||
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
|
||||
shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val defaultInputFieldShape = SearchBarDefaults.inputFieldShape
|
||||
val defaultFullScreenShape = SearchBarDefaults.fullScreenShape
|
||||
val useFullScreenShape by remember {
|
||||
derivedStateOf(structuralEqualityPolicy()) { animationProgress.value == 1f }
|
||||
}
|
||||
val animatedShape = remember(useFullScreenShape, shape) {
|
||||
when {
|
||||
shape == defaultInputFieldShape ->
|
||||
// The shape can only be animated if it's the default spec value
|
||||
GenericShape { size, _ ->
|
||||
val radius = with(density) {
|
||||
(SearchBarCornerRadius * (1 - animationProgress.value)).toPx()
|
||||
}
|
||||
addRoundRect(RoundRect(size.toRect(), CornerRadius(radius)))
|
||||
}
|
||||
useFullScreenShape -> defaultFullScreenShape
|
||||
else -> shape
|
||||
}
|
||||
}
|
||||
|
||||
// The main animation complexity is allowing the component to smoothly expand while keeping the
|
||||
// input field at the same relative location on screen. `Modifier.windowInsetsPadding` does not
|
||||
// support animation and thus is not suitable. Instead, we convert the insets to a padding
|
||||
// applied to the Surface, which gradually becomes padding applied to the input field as the
|
||||
// animation proceeds.
|
||||
val unconsumedInsets = remember { MutableWindowInsets() }
|
||||
val topPadding = remember(density) {
|
||||
derivedStateOf {
|
||||
SearchBarVerticalPadding +
|
||||
unconsumedInsets.asPaddingValues(density).calculateTopPadding()
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
shape = animatedShape,
|
||||
color = colors.containerColor,
|
||||
contentColor = contentColorFor(colors.containerColor),
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation,
|
||||
modifier = modifier
|
||||
.padding(horizontal = lerp(SearchBarHorizontalPadding, 0.dp, animationProgress.value))
|
||||
.zIndex(1f)
|
||||
.onConsumedWindowInsetsChanged { consumedInsets ->
|
||||
unconsumedInsets.insets = windowInsets.exclude(consumedInsets)
|
||||
}
|
||||
.consumeWindowInsets(unconsumedInsets)
|
||||
.layout { measurable, constraints ->
|
||||
val animatedTopPadding =
|
||||
lerp(topPadding.value, 0.dp, animationProgress.value).roundToPx()
|
||||
|
||||
val defaultStartWidth = max(constraints.minWidth, SearchBarMinWidth.roundToPx())
|
||||
.coerceAtMost(min(constraints.maxWidth, SearchBarMaxWidth.roundToPx()))
|
||||
val defaultStartHeight = max(constraints.minHeight, InputFieldHeight.roundToPx())
|
||||
.coerceAtMost(constraints.maxHeight)
|
||||
val predictiveBackStartWidth =
|
||||
(constraints.maxWidth * SearchBarPredictiveBackMinScale).roundToInt()
|
||||
val predictiveBackStartHeight =
|
||||
(constraints.maxHeight * SearchBarPredictiveBackMinScale).roundToInt()
|
||||
val predictiveBackMultiplier = calculatePredictiveBackMultiplier(
|
||||
currentBackEvent.value,
|
||||
animationProgress.value,
|
||||
finalBackProgress.floatValue,
|
||||
)
|
||||
val startWidth =
|
||||
lerp(defaultStartWidth, predictiveBackStartWidth, predictiveBackMultiplier)
|
||||
val startHeight =
|
||||
lerp(defaultStartHeight, predictiveBackStartHeight, predictiveBackMultiplier)
|
||||
|
||||
val endWidth = constraints.maxWidth
|
||||
val endHeight = constraints.maxHeight
|
||||
|
||||
val width = lerp(startWidth, endWidth, animationProgress.value)
|
||||
val height =
|
||||
lerp(startHeight, endHeight, animationProgress.value) + animatedTopPadding
|
||||
|
||||
val minOffsetMargin = SearchBarPredictiveBackMinMargin.roundToPx()
|
||||
val predictiveBackOffsetX = calculatePredictiveBackOffsetX(
|
||||
constraints,
|
||||
minOffsetMargin,
|
||||
currentBackEvent.value,
|
||||
animationProgress.value,
|
||||
predictiveBackMultiplier,
|
||||
)
|
||||
val predictiveBackOffsetY = calculatePredictiveBackOffsetY(
|
||||
constraints,
|
||||
minOffsetMargin,
|
||||
currentBackEvent.value,
|
||||
firstBackEvent.value,
|
||||
height,
|
||||
SearchBarPredictiveBackMaxOffsetY.roundToPx(),
|
||||
predictiveBackMultiplier,
|
||||
)
|
||||
|
||||
val placeable = measurable.measure(
|
||||
Constraints
|
||||
.fixed(width, height)
|
||||
.offset(
|
||||
vertical = -animatedTopPadding,
|
||||
),
|
||||
)
|
||||
layout(width, height) {
|
||||
placeable.placeRelative(
|
||||
predictiveBackOffsetX,
|
||||
animatedTopPadding + predictiveBackOffsetY,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
val animatedInputFieldPadding = remember {
|
||||
AnimatedPaddingValues2(animationProgress.asState(), topPadding)
|
||||
}
|
||||
SearchBarInputField(
|
||||
state = state,
|
||||
onSearch = onSearch,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = Modifier.padding(paddingValues = animatedInputFieldPadding),
|
||||
enabled = enabled,
|
||||
label = title.takeUnless { active || animationProgress.value > 0 || state.text.isNotEmpty() },
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
colors = colors.inputFieldColors,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
||||
val showResults by remember {
|
||||
derivedStateOf(structuralEqualityPolicy()) { animationProgress.value > 0 }
|
||||
}
|
||||
if (showResults) {
|
||||
Column(Modifier.graphicsLayer { alpha = animationProgress.value }) {
|
||||
HorizontalDivider(color = colors.dividerColor)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
val shouldClearFocus = !active && isFocused
|
||||
LaunchedEffect(active) {
|
||||
if (shouldClearFocus) {
|
||||
// Not strictly needed according to the motion spec, but since the animation already has
|
||||
// a delay, this works around b/261632544.
|
||||
delay(AnimationDelayMillis.toLong())
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackMultiplier(
|
||||
currentBackEvent: BackEventCompat?,
|
||||
progress: Float,
|
||||
finalBackProgress: Float,
|
||||
) = when {
|
||||
currentBackEvent == null -> 0f // Not in predictive back at all.
|
||||
finalBackProgress.isNaN() -> 1f // User is currently swiping predictive back.
|
||||
finalBackProgress <= 0 -> 0f // Safety check for divide by zero.
|
||||
else -> progress / finalBackProgress // User has released predictive back swipe.
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackOffsetX(
|
||||
constraints: Constraints,
|
||||
minMargin: Int,
|
||||
currentBackEvent: BackEventCompat?,
|
||||
progress: Float,
|
||||
predictiveBackMultiplier: Float,
|
||||
): Int {
|
||||
if (currentBackEvent == null || predictiveBackMultiplier == 0f) {
|
||||
return 0
|
||||
}
|
||||
val directionMultiplier = if (currentBackEvent.swipeEdge == BackEventCompat.EDGE_LEFT) 1 else -1
|
||||
val maxOffsetX =
|
||||
(constraints.maxWidth * SearchBarPredictiveBackMaxOffsetXRatio) - minMargin
|
||||
val interpolatedOffsetX = maxOffsetX * (1 - progress)
|
||||
return (interpolatedOffsetX * predictiveBackMultiplier * directionMultiplier).roundToInt()
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackOffsetY(
|
||||
constraints: Constraints,
|
||||
minMargin: Int,
|
||||
currentBackEvent: BackEventCompat?,
|
||||
firstBackEvent: BackEventCompat?,
|
||||
height: Int,
|
||||
maxOffsetY: Int,
|
||||
predictiveBackMultiplier: Float,
|
||||
): Int {
|
||||
if (firstBackEvent == null || currentBackEvent == null || predictiveBackMultiplier == 0f) {
|
||||
return 0
|
||||
}
|
||||
val availableVerticalSpace = max(0, (constraints.maxHeight - height) / 2 - minMargin)
|
||||
val adjustedMaxOffsetY = min(availableVerticalSpace, maxOffsetY)
|
||||
val yDelta = currentBackEvent.touchY - firstBackEvent.touchY
|
||||
val yProgress = abs(yDelta) / constraints.maxHeight
|
||||
val directionMultiplier = sign(yDelta)
|
||||
val interpolatedOffsetY = lerp(0, adjustedMaxOffsetY, yProgress)
|
||||
return (interpolatedOffsetY * predictiveBackMultiplier * directionMultiplier).roundToInt()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchBarInputField(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val textColor = LocalTextStyle.current.color.takeOrElse {
|
||||
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
|
||||
}
|
||||
|
||||
BasicTextField2(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
.height(InputFieldHeight)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { if (it.isFocused) onActiveChange(true) }
|
||||
.semantics {
|
||||
onClick {
|
||||
focusRequester.requestFocus()
|
||||
true
|
||||
}
|
||||
},
|
||||
enabled = enabled,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
|
||||
cursorBrush = SolidColor(colors.cursorColor),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { onSearch() }),
|
||||
interactionSource = interactionSource,
|
||||
decorator = @Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
innerTextField = innerTextField,
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon?.let { leading ->
|
||||
{
|
||||
Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() }
|
||||
}
|
||||
},
|
||||
trailingIcon = trailingIcon?.let { trailing ->
|
||||
{
|
||||
Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() }
|
||||
}
|
||||
},
|
||||
shape = SearchBarDefaults.inputFieldShape,
|
||||
colors = colors,
|
||||
contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
|
||||
container = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
private class AnimatedPaddingValues2(
|
||||
val animationProgress: State<Float>,
|
||||
val topPadding: State<Dp>,
|
||||
) : PaddingValues {
|
||||
override fun calculateTopPadding(): Dp = topPadding.value * animationProgress.value
|
||||
override fun calculateBottomPadding(): Dp = SearchBarVerticalPadding * animationProgress.value
|
||||
|
||||
override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = 0.dp
|
||||
override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = 0.dp
|
||||
}
|
||||
|
||||
// Measurement specs
|
||||
private val SearchBarCornerRadius: Dp = InputFieldHeight / 2
|
||||
internal val SearchBarMinWidth: Dp = 360.dp
|
||||
private val SearchBarMaxWidth: Dp = 720.dp
|
||||
internal val SearchBarVerticalPadding: Dp = 8.dp
|
||||
internal val SearchBarHorizontalPadding: Dp = 16.dp
|
||||
|
||||
// Search bar has 16dp padding between icons and start/end, while by default text field has 12dp.
|
||||
private val SearchBarIconOffsetX: Dp = 4.dp
|
||||
private const val SearchBarPredictiveBackMinScale: Float = 9f / 10f
|
||||
private val SearchBarPredictiveBackMinMargin: Dp = 8.dp
|
||||
private const val SearchBarPredictiveBackMaxOffsetXRatio: Float = 1f / 20f
|
||||
private val SearchBarPredictiveBackMaxOffsetY: Dp = 24.dp
|
||||
|
||||
// Animation specs
|
||||
private const val AnimationEnterDurationMillis: Int = 600
|
||||
private const val AnimationExitDurationMillis: Int = 350
|
||||
private const val AnimationDelayMillis: Int = 100
|
||||
private val AnimationEnterEasing = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1.0f)
|
||||
private val AnimationExitEasing = CubicBezierEasing(0.0f, 1.0f, 0.0f, 1.0f)
|
||||
private val AnimationEnterFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationEnterDurationMillis,
|
||||
delayMillis = AnimationDelayMillis,
|
||||
easing = AnimationEnterEasing,
|
||||
)
|
||||
private val AnimationExitFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationExitDurationMillis,
|
||||
delayMillis = AnimationDelayMillis,
|
||||
easing = AnimationExitEasing,
|
||||
)
|
||||
private val AnimationPredictiveBackExitFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationExitDurationMillis,
|
||||
easing = AnimationExitEasing,
|
||||
)
|
||||
+3
-140
@@ -236,7 +236,7 @@ class DrawerState2(
|
||||
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDrawerState(
|
||||
fun rememberDrawerState2(
|
||||
initialValue: DrawerValue,
|
||||
confirmStateChange: (DrawerValue) -> Boolean = { true },
|
||||
): DrawerState2 {
|
||||
@@ -246,147 +246,10 @@ fun rememberDrawerState(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalNavigationDrawer(
|
||||
fun ModalSideDrawer(
|
||||
drawerContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
drawerState: DrawerState2 = rememberDrawerState(DrawerValue.Closed),
|
||||
gesturesEnabled: Boolean = true,
|
||||
scrimColor: Color = DrawerDefaults.scrimColor,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
var minValue by remember { mutableFloatStateOf(-with(density) { ContainerWidth.toPx() }) }
|
||||
val maxValue = 0f
|
||||
|
||||
SideEffect {
|
||||
drawerState.density = density
|
||||
drawerState.anchoredDraggableState.updateAnchors(
|
||||
DraggableAnchors {
|
||||
DrawerValue.Closed at minValue
|
||||
DrawerValue.Open at maxValue
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
val velocityTracker = remember { VelocityTracker() }
|
||||
val dragModifier = if (gesturesEnabled) {
|
||||
Modifier.pointerInput(isRtl) {
|
||||
val multiplier = if (isRtl) -1 else 1
|
||||
awaitEachGesture {
|
||||
fun canConsume(slop: Float) = (slop > 0 && drawerState.isClosed) || (slop < 0 && drawerState.isOpen)
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
var ignore = false
|
||||
val drag = awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
|
||||
val overSlop = over * multiplier
|
||||
val canConsume = canConsume(overSlop)
|
||||
if (canConsume && !ignore) {
|
||||
velocityTracker.addPointerInputChange(change)
|
||||
change.consume()
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(overSlop)
|
||||
} else {
|
||||
ignore = true
|
||||
}
|
||||
}
|
||||
if (drag != null) {
|
||||
horizontalDrag(drag.id) {
|
||||
velocityTracker.addPointerInputChange(it)
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(it.positionChange().x * multiplier)
|
||||
}
|
||||
val velocity = velocityTracker.calculateVelocity()
|
||||
velocityTracker.resetTracking()
|
||||
scope.launch {
|
||||
drawerState.anchoredDraggableState.settle(velocity.x * multiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(modifier.fillMaxSize().then(dragModifier)) {
|
||||
val radius by remember {
|
||||
snapshotFlow {
|
||||
val step = calculateFraction(minValue, maxValue, drawerState.currentOffset)
|
||||
lerp(0, 10, step).dp
|
||||
}
|
||||
}.collectAsState(0.dp)
|
||||
val blurModifier = Modifier.graphicsLayer {
|
||||
if (radius != 0.dp) {
|
||||
renderEffect = BlurEffect(radius.toPx(), radius.toPx(), TileMode.Clamp)
|
||||
shape = RectangleShape
|
||||
clip = true
|
||||
}
|
||||
}
|
||||
Box(modifier = blurModifier) {
|
||||
content()
|
||||
}
|
||||
Scrim(
|
||||
open = drawerState.isOpen,
|
||||
onClose = {
|
||||
if (gesturesEnabled && drawerState.confirmStateChange(DrawerValue.Closed)) {
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
},
|
||||
fraction = {
|
||||
calculateFraction(minValue, maxValue, drawerState.requireOffset())
|
||||
},
|
||||
color = scrimColor,
|
||||
)
|
||||
val predictiveState by animateFloatMergeOneWayPredictiveBackAsState(drawerState.isOpen) {
|
||||
drawerState.close()
|
||||
}
|
||||
val absoluteElevation = LocalAbsoluteTonalElevation.current + DrawerDefaults.ModalDrawerElevation
|
||||
val predictiveModifier = if (drawerState.isOpen) {
|
||||
if (predictiveState > 0) {
|
||||
Modifier.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
val multiplierX = lerp(1f, 1.05f, predictiveState)
|
||||
val multiplierY = lerp(1f, 0.95f, predictiveState)
|
||||
val scaledWidth = (multiplierX * placeable.width).roundToInt()
|
||||
val scaledHeight = (multiplierY * placeable.height).roundToInt()
|
||||
val reMeasured = measurable.measure(Constraints.fixed(scaledWidth, scaledHeight))
|
||||
layout(scaledWidth, scaledHeight) {
|
||||
reMeasured.placeRelative(scaledWidth - placeable.width, 0)
|
||||
}
|
||||
}.background(
|
||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
|
||||
shape = DrawerDefaults.shape,
|
||||
)
|
||||
} else {
|
||||
Modifier.graphicsLayer {
|
||||
val scale = lerp(1f, 0.95f, -predictiveState)
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}.offset {
|
||||
IntOffset(lerp(0f, minValue * 0.05f, -predictiveState).roundToInt(), 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier.onGloballyPositioned {
|
||||
val w = it.size.width
|
||||
if (w != 0) {
|
||||
minValue = -it.size.width.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(drawerState.requireOffset().roundToInt(), 0)
|
||||
} then predictiveModifier.align(Alignment.CenterStart),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
drawerContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SideDrawer(
|
||||
drawerContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
drawerState: DrawerState2 = rememberDrawerState(DrawerValue.Closed),
|
||||
drawerState: DrawerState2 = rememberDrawerState2(DrawerValue.Closed),
|
||||
gesturesEnabled: Boolean = true,
|
||||
scrimColor: Color = DrawerDefaults.scrimColor,
|
||||
content: @Composable () -> Unit,
|
||||
+2
-21
@@ -1336,8 +1336,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener allowing notification of load and error events. Extend [DefaultOnImageEventListener]
|
||||
* to simplify implementation.
|
||||
* Add a listener allowing notification of load and error events.
|
||||
*
|
||||
* @param onImageEventListener an [OnImageEventListener] instance.
|
||||
*/
|
||||
@@ -1391,7 +1390,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
/**
|
||||
* An event listener, allowing subclasses and activities to be notified of significant events.
|
||||
*/
|
||||
interface OnImageEventListener {
|
||||
fun interface OnImageEventListener {
|
||||
/**
|
||||
* Called when the dimensions of the image and view are known, and either a preview image,
|
||||
* the full size image, or base layer tiles are loaded. This indicates the scale and translate
|
||||
@@ -1399,16 +1398,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* graphic, or inform a subclass that it is safe to draw overlays.
|
||||
*/
|
||||
fun onReady()
|
||||
|
||||
/**
|
||||
* Indicates an error initiliasing the decoder when using a tiling, or when loading the full
|
||||
* size bitmap when tiling is disabled. This method cannot be relied upon; certain encoding
|
||||
* types of supported image formats can result in corrupt or blank images being loaded and
|
||||
* displayed with no detectable error.
|
||||
*
|
||||
* @param e The exception thrown. This error is also logged by the view.
|
||||
*/
|
||||
fun onImageLoadError(e: Exception)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1467,14 +1456,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
|
||||
private data class ScaleAndTranslate(var scale: Float, val vTranslate: PointF)
|
||||
|
||||
/**
|
||||
* Default implementation of [OnImageEventListener] for extension. This does nothing in any method.
|
||||
*/
|
||||
open class DefaultOnImageEventListener : OnImageEventListener {
|
||||
override fun onReady() {}
|
||||
override fun onImageLoadError(e: Exception) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class used to set additional options for a scale animation. Create an instance using [.animateScale],
|
||||
* then set your options and call [.start].
|
||||
|
||||
@@ -25,11 +25,14 @@ import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.snapshots.Snapshot
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import coil3.EventListener
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.asCoilImage
|
||||
import coil3.gif.AnimatedImageDecoder
|
||||
import coil3.gif.GifDecoder
|
||||
import coil3.network.ktor.KtorNetworkFetcherFactory
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import coil3.serviceLoaderEnabled
|
||||
import coil3.util.DebugLogger
|
||||
@@ -64,6 +67,7 @@ import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import logcat.asLog
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.arch.room.roomDb
|
||||
import splitties.init.appCtx
|
||||
@@ -174,7 +178,17 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
crossfade(300)
|
||||
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
||||
if (drawable != null) error(drawable.asCoilImage(true))
|
||||
if (BuildConfig.DEBUG) logger(DebugLogger())
|
||||
if (BuildConfig.DEBUG) {
|
||||
logger(DebugLogger())
|
||||
} else {
|
||||
eventListener(object : EventListener() {
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
logcat("ImageLoader", LogPriority.ERROR) {
|
||||
"🚨 Failed - ${request.data}\n${result.throwable.asLog()}"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -187,7 +201,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
}
|
||||
|
||||
val noRedirectKtorClient by lazy {
|
||||
HttpClient(ktorClient.engine) {
|
||||
HttpClient(CronetEngine) {
|
||||
followRedirects = false
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.hippo.ehviewer.dao.HistoryInfo
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||
import com.hippo.ehviewer.dao.ProgressInfo
|
||||
import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.dao.Schema17to18
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.unifile.asUniFile
|
||||
@@ -36,7 +37,10 @@ import kotlinx.coroutines.flow.Flow
|
||||
import splitties.arch.room.roomDb
|
||||
|
||||
object EhDB {
|
||||
private val db = roomDb<EhDatabase>("eh.db")
|
||||
private const val DB_NAME = "eh.db"
|
||||
private val db = roomDb<EhDatabase>(DB_NAME) {
|
||||
addMigrations(Schema17to18())
|
||||
}
|
||||
|
||||
private suspend fun putGalleryInfo(galleryInfo: BaseGalleryInfo) {
|
||||
db.galleryDao().upsert(galleryInfo)
|
||||
@@ -256,35 +260,20 @@ object EhDB {
|
||||
db.filterDao().update(filter)
|
||||
}
|
||||
|
||||
suspend fun exportDB(context: Context, uri: Uri) {
|
||||
val ehExportName = "eh.export.db"
|
||||
resource {
|
||||
context.deleteDatabase(ehExportName)
|
||||
roomDb<EhDatabase>(ehExportName)
|
||||
} release {
|
||||
it.close()
|
||||
context.deleteDatabase(ehExportName)
|
||||
} use { newDb ->
|
||||
db.galleryDao().list().let { newDb.galleryDao().insertOrIgnore(it) }
|
||||
db.progressDao().list().let { newDb.progressDao().insertOrIgnore(it) }
|
||||
db.downloadLabelDao().list().let { newDb.downloadLabelDao().insert(it) }
|
||||
db.downloadDirnameDao().list().let { newDb.downloadDirnameDao().insertOrIgnore(it) }
|
||||
db.downloadsDao().list().let { newDb.downloadsDao().insert(it) }
|
||||
db.historyDao().list().let { newDb.historyDao().insertOrIgnore(it) }
|
||||
db.quickSearchDao().list().let { newDb.quickSearchDao().insert(it) }
|
||||
db.localFavoritesDao().list().let { newDb.localFavoritesDao().insertOrIgnore(it) }
|
||||
db.filterDao().list().let { newDb.filterDao().insert(it) }
|
||||
newDb.close()
|
||||
val dbFile = context.getDatabasePath(ehExportName)
|
||||
dbFile.asUniFile() sendTo uri.asUniFile()
|
||||
}
|
||||
fun exportDB(context: Context, uri: Uri) {
|
||||
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
|
||||
val dbFile = context.getDatabasePath(DB_NAME)
|
||||
dbFile.asUniFile() sendTo uri.asUniFile()
|
||||
}
|
||||
|
||||
suspend fun importDB(context: Context, uri: Uri) {
|
||||
val tempDBName = "tmp.db"
|
||||
resource {
|
||||
context.deleteDatabase(tempDBName)
|
||||
roomDb<EhDatabase>(tempDBName) { createFromInputStream { context.contentResolver.openInputStream(uri) } }
|
||||
roomDb<EhDatabase>(tempDBName) {
|
||||
createFromInputStream { context.contentResolver.openInputStream(uri) }
|
||||
addMigrations(Schema17to18())
|
||||
}
|
||||
} release {
|
||||
it.close()
|
||||
context.deleteDatabase(tempDBName)
|
||||
|
||||
@@ -60,7 +60,6 @@ import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import io.ktor.utils.io.pool.useInstance
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.ceil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -71,6 +70,8 @@ import kotlinx.serialization.json.addJsonArray
|
||||
import kotlinx.serialization.json.put
|
||||
import moe.tarsin.coroutines.removeAllSuspend
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import org.jsoup.Jsoup
|
||||
import splitties.init.appCtx
|
||||
|
||||
@@ -84,11 +85,11 @@ private const val U_CONFIG_TEXT = "Selected Profile"
|
||||
fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
|
||||
val dir = AppConfig.externalParseErrorDir ?: return
|
||||
val file = File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt")
|
||||
RandomAccessFile(file, "rw").use { randomAccessFile ->
|
||||
with(randomAccessFile) {
|
||||
writeUTF(e.message + "\n")
|
||||
onLeft { writeUTF(it) }
|
||||
onRight { channel.write(it) }
|
||||
file.sink().buffer().use { sink ->
|
||||
with(sink) {
|
||||
writeUtf8(e.message + "\n")
|
||||
onLeft { writeUtf8(it) }
|
||||
onRight { write(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +101,7 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
// Check sad panda (without panda)
|
||||
val empty = body.fold(
|
||||
{ it.isEmpty() },
|
||||
{ it.limit() == 0 },
|
||||
{ !it.hasRemaining() },
|
||||
)
|
||||
if (empty) {
|
||||
if (EhUtils.isExHentai) {
|
||||
@@ -143,7 +144,7 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
|
||||
val httpContentPool = DirectByteBufferPool(8, 0x80000)
|
||||
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = execute { response ->
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = executeSafely { response ->
|
||||
val body = response.bodyAsUtf8Text()
|
||||
runSuspendCatching {
|
||||
block(body)
|
||||
@@ -152,20 +153,21 @@ suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = execute { response ->
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = executeSafely { response ->
|
||||
httpContentPool.useInstance { buffer ->
|
||||
with(response.bodyAsChannel()) { while (!isClosedForRead) readAvailable(buffer) }
|
||||
buffer.flip()
|
||||
runSuspendCatching {
|
||||
block(buffer)
|
||||
}.onFailure {
|
||||
buffer.rewind()
|
||||
rethrowExactly(response.status.value, buffer.right(), it)
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
object EhEngine {
|
||||
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).execute { response ->
|
||||
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
|
||||
val location = response.headers["Location"] ?: throw InsufficientFundsException()
|
||||
location.takeIf { "bounce_login" !in it } ?: throw NotLoggedInException()
|
||||
}
|
||||
@@ -241,7 +243,7 @@ object EhEngine {
|
||||
append("edit_comment", id.toString())
|
||||
}
|
||||
}
|
||||
}.execute { response ->
|
||||
}.executeSafely { response ->
|
||||
// Ktor does not handle POST redirect, we need to do it manually
|
||||
// https://youtrack.jetbrains.com/issue/KTOR-478
|
||||
val location = response.headers["Location"] ?: url
|
||||
@@ -270,7 +272,7 @@ object EhEngine {
|
||||
append("apply", "Apply Changes")
|
||||
append("update", "1")
|
||||
}
|
||||
}.execute { }
|
||||
}.executeSafely { }
|
||||
}
|
||||
|
||||
suspend fun downloadArchive(gid: Long, token: String, or: String, res: String, isHAtH: Boolean): String? {
|
||||
|
||||
@@ -25,6 +25,7 @@ import io.ktor.client.request.forms.FormDataContent
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.prepareRequest
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.HttpStatement
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
@@ -32,6 +33,7 @@ import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.ParametersBuilder
|
||||
import io.ktor.http.content.TextContent
|
||||
import io.ktor.http.userAgent
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArrayBuilder
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
@@ -42,7 +44,7 @@ import okio.BufferedSource
|
||||
|
||||
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
|
||||
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = execute { it.bodyAsUtf8Text().parseAs<T>() }
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = executeSafely { it.bodyAsUtf8Text().parseAs<T>() }
|
||||
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
|
||||
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
|
||||
|
||||
@@ -82,6 +84,16 @@ suspend inline fun noRedirectEhRequest(
|
||||
apply(builder)
|
||||
}.also { logcat("EhRequest") { url } }
|
||||
|
||||
suspend inline fun <T> HttpStatement.executeSafely(
|
||||
crossinline block: suspend (response: HttpResponse) -> T,
|
||||
) = execute { resp ->
|
||||
try {
|
||||
block(resp)
|
||||
} finally {
|
||||
resp.coroutineContext.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun HttpRequestBuilder.formBody(builder: ParametersBuilder.() -> Unit) {
|
||||
method = HttpMethod.Post
|
||||
val parameters = ParametersBuilder().apply(builder).build()
|
||||
|
||||
@@ -152,7 +152,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
|
||||
private suspend fun save(client: HttpClient, url: String, file: File) {
|
||||
runCatching {
|
||||
client.prepareGet(url).execute {
|
||||
client.prepareGet(url).executeSafely {
|
||||
if (it.status.isSuccess()) {
|
||||
it.bodyAsChannel().copyTo(file)
|
||||
} else {
|
||||
|
||||
@@ -122,7 +122,7 @@ interface GalleryInfo {
|
||||
|
||||
private fun generateSLangFromTitle(title: String): String? {
|
||||
for (i in S_LANGS.indices) {
|
||||
if (S_LANG_PATTERNS[i].matches(title)) {
|
||||
if (S_LANG_PATTERNS[i].containsMatchIn(title)) {
|
||||
return S_LANGS[i]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import com.hippo.ehviewer.image.detectBorder
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
|
||||
private const val CROP_THRESHOLD = 0.75f
|
||||
private const val WEBTOON_THRESHOLD = 3
|
||||
private const val RATIO_THRESHOLD = 2
|
||||
|
||||
private val maybeCropBorderKey = Extras.Key(default = false)
|
||||
|
||||
@@ -48,7 +48,12 @@ object CropBorderInterceptor : Interceptor {
|
||||
val image = result.image
|
||||
if (image is BitmapImage) {
|
||||
val bitmap = image.bitmap
|
||||
val rect = if (image.height / image.width < WEBTOON_THRESHOLD) {
|
||||
val ratio = if (image.height > image.width) {
|
||||
image.height / image.width
|
||||
} else {
|
||||
image.width / image.height
|
||||
}
|
||||
val rect = if (ratio < RATIO_THRESHOLD) {
|
||||
val array = detectBorder(bitmap)
|
||||
val minWidth = image.width * CROP_THRESHOLD
|
||||
val minHeight = image.height * CROP_THRESHOLD
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
||||
@@ -30,24 +29,24 @@ object DownloadThumbInterceptor : Interceptor {
|
||||
}
|
||||
if (withIOContext { thumb.isFile }) {
|
||||
val new = chain.request.newBuilder().data(thumb.uri.toString()).build()
|
||||
when (val result = chain.withRequest(new).proceed()) {
|
||||
is SuccessResult -> return result
|
||||
is ErrorResult -> withIOContext { thumb.delete() }
|
||||
}
|
||||
val result = chain.withRequest(new).proceed()
|
||||
if (result is SuccessResult) return result
|
||||
}
|
||||
val new = chain.request.newBuilder().data(url).build()
|
||||
return chain.withRequest(new).proceed().also {
|
||||
if (it is SuccessResult) {
|
||||
withIOContext {
|
||||
if (thumb.ensureFile()) {
|
||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||
imageCache.read(key) {
|
||||
data.asUniFile() sendTo thumb
|
||||
}
|
||||
val result = chain.withRequest(new).proceed()
|
||||
if (result is SuccessResult) {
|
||||
withIOContext {
|
||||
// Accessing the recreated file immediately after deleting it throws
|
||||
// FileNotFoundException, so we just overwrite the existing file.
|
||||
if (thumb.ensureFile()) {
|
||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||
imageCache.read(key) {
|
||||
data.asUniFile() sendTo thumb
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
return chain.proceed()
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17),
|
||||
AutoMigration(from = 17, to = 18),
|
||||
AutoMigration(from = 18, to = 19, spec = Schema18to19::class),
|
||||
AutoMigration(from = 19, to = 20),
|
||||
AutoMigration(from = 20, to = 21, spec = Schema20to21::class),
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.room.DeleteTable
|
||||
import androidx.room.RenameColumn
|
||||
import androidx.room.RenameTable
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
|
||||
@@ -103,6 +104,15 @@ class Schema12to13 : AutoMigrationSpec {
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "SIMPLE_LANGUAGE")
|
||||
class Schema13to14 : AutoMigrationSpec
|
||||
|
||||
class Schema17to18 : Migration(17, 18) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `_new_GALLERIES` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))")
|
||||
db.execSQL("INSERT INTO `_new_GALLERIES` (`GID`,`TOKEN`,`TITLE`,`TITLE_JPN`,`THUMB`,`CATEGORY`,`POSTED`,`UPLOADER`,`RATING`,`SIMPLE_LANGUAGE`,`FAVORITE_SLOT`) SELECT `GID`,`TOKEN`,`TITLE`,`TITLE_JPN`,`THUMB`,`CATEGORY`,`POSTED`,`UPLOADER`,`RATING`,`SIMPLE_LANGUAGE`,`FAVORITE_SLOT` FROM `GALLERIES` WHERE `TOKEN` IS NOT NULL")
|
||||
db.execSQL("DROP TABLE `GALLERIES`")
|
||||
db.execSQL("ALTER TABLE `_new_GALLERIES` RENAME TO `GALLERIES`")
|
||||
}
|
||||
}
|
||||
|
||||
@RenameColumn(tableName = "DOWNLOADS", fromColumnName = "POSITION", toColumnName = "TIME")
|
||||
class Schema18to19 : AutoMigrationSpec
|
||||
|
||||
|
||||
@@ -148,9 +148,9 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
|
||||
ACTION_CLEAR -> {
|
||||
clear()
|
||||
checkStopSelf()
|
||||
}
|
||||
}
|
||||
checkStopSelf()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = error("No bindService")
|
||||
|
||||
@@ -30,8 +30,8 @@ import coil3.imageLoader
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.size.Dimension
|
||||
import coil3.size.Precision
|
||||
import coil3.size.Scale
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.coil.BitmapImageWithRect
|
||||
import com.hippo.ehviewer.coil.maybeCropBorder
|
||||
@@ -73,15 +73,13 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 2
|
||||
private val targetHeight = appCtx.resources.displayMetrics.heightPixels * 2
|
||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
|
||||
|
||||
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)
|
||||
size(Dimension(targetWidth), Dimension.Undefined)
|
||||
precision(Precision.INEXACT)
|
||||
maybeCropBorder(Settings.cropBorder.value)
|
||||
memoryCachePolicy(CachePolicy.DISABLED)
|
||||
@@ -94,7 +92,7 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
|
||||
suspend fun decode(src: ImageSource): Image? {
|
||||
return runCatching {
|
||||
when (src) {
|
||||
val image = when (src) {
|
||||
is UniFileSource -> {
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
src.source.openFileDescriptor("rw").use {
|
||||
@@ -112,20 +110,19 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
}
|
||||
}
|
||||
}
|
||||
val image = src.right().decodeCoil()
|
||||
if (image is BitmapImage) src.close()
|
||||
Image(image, src)
|
||||
src.right().decodeCoil()
|
||||
}
|
||||
|
||||
is ByteBufferSource -> {
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
rewriteGifSource(src.source)
|
||||
}
|
||||
val image = src.left().decodeCoil()
|
||||
if (image is BitmapImage) src.close()
|
||||
Image(image, src)
|
||||
src.left().decodeCoil()
|
||||
}
|
||||
}
|
||||
Image(image, src).apply {
|
||||
if (innerImage is BitmapImage) src.close()
|
||||
}
|
||||
}.onFailure {
|
||||
src.close()
|
||||
logcat(it)
|
||||
|
||||
@@ -24,8 +24,6 @@ import java.nio.ByteBuffer
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.job
|
||||
@@ -42,7 +40,6 @@ object CronetEngine : HttpClientEngineBase("Cronet") {
|
||||
@InternalAPI
|
||||
override suspend fun execute(data: HttpRequestData) = executeHttpRequest(callContext(), data)
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private suspend fun executeHttpRequest(
|
||||
callContext: CoroutineContext,
|
||||
data: HttpRequestData,
|
||||
@@ -50,7 +47,7 @@ object CronetEngine : HttpClientEngineBase("Cronet") {
|
||||
val requestTime = GMTDate()
|
||||
|
||||
val callback = object : UrlRequest.Callback() {
|
||||
lateinit var chunkChan: Channel<ByteBuffer>
|
||||
val chunkChan = Channel<ByteBuffer>()
|
||||
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
|
||||
continuation.resume(
|
||||
info.toHttpResponseData(
|
||||
@@ -65,9 +62,8 @@ object CronetEngine : HttpClientEngineBase("Cronet") {
|
||||
info.toHttpResponseData(
|
||||
requestTime = requestTime,
|
||||
callContext = callContext,
|
||||
responseBody = GlobalScope.writer(callContext) {
|
||||
responseBody = writer(callContext.job) {
|
||||
pool.useInstance {
|
||||
chunkChan = Channel()
|
||||
request.read(it)
|
||||
chunkChan.consumeEach { buffer ->
|
||||
buffer.flip()
|
||||
|
||||
@@ -99,7 +99,6 @@ fun ComicInfo.toSimpleTags() = listOfNotNull(
|
||||
|
||||
fun ComicInfo.write(file: UniFile) {
|
||||
file.openOutputStream().use {
|
||||
it.channel.truncate(0)
|
||||
OutputStreamWriter(it, Charsets.UTF_8).use { writer ->
|
||||
xmlStreaming.newGenericWriter(writer).use { xmlWriter ->
|
||||
xml.encodeToWriter(xmlWriter, ComicInfo.serializer(), this)
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.ehRequest
|
||||
import com.hippo.ehviewer.client.executeSafely
|
||||
import com.hippo.ehviewer.client.getImageKey
|
||||
import com.hippo.ehviewer.coil.read
|
||||
import com.hippo.ehviewer.coil.suspendEdit
|
||||
@@ -157,7 +158,7 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
notifyProgress(total!!, done, (done - prev).toInt())
|
||||
prev = done
|
||||
}
|
||||
}.execute {
|
||||
}.executeSafely {
|
||||
if (it.status.isSuccess()) {
|
||||
saveFromHttpResponse(index, it)
|
||||
} else {
|
||||
@@ -191,8 +192,7 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
val extension = MimeTypeMap.getFileExtensionFromUrl(url).ifEmpty { "jpg" }
|
||||
return saveResponseMeta(index, extension) { outFile ->
|
||||
outFile.openOutputStream().use {
|
||||
val chan = it.channel.apply { truncate(0) }
|
||||
response.bodyAsChannel().copyTo(chan)
|
||||
response.bodyAsChannel().copyTo(it.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,13 +547,20 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
|
||||
private fun doLaunchDownloadJob(index: Int, force: Boolean, orgImg: Boolean = false) {
|
||||
val state = mPageStateArray[index]
|
||||
if (!force && state == STATE_FINISHED) return
|
||||
if (!force) {
|
||||
if (state == STATE_FINISHED) return
|
||||
if (index in mSpiderDen) return updatePageState(index, STATE_FINISHED)
|
||||
}
|
||||
val currentJob = mFetcherJobMap[index]
|
||||
val skipHath = force && !orgImg && currentJob?.isActive == true
|
||||
if (force) currentJob?.cancel(CancellationException(FORCE_RETRY))
|
||||
if (currentJob?.isActive != true) {
|
||||
mFetcherJobMap[index] = launch {
|
||||
runCatching {
|
||||
delayLock.withLock {
|
||||
delay(mDownloadDelay - lastRequestTime.elapsedNow())
|
||||
lastRequestTime = TimeSource.Monotonic.markNow()
|
||||
}
|
||||
mSemaphore.withPermit {
|
||||
doInJob(index, force, orgImg, skipHath)
|
||||
}
|
||||
@@ -591,19 +598,14 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
?: getPTokenFromMultiPageViewer(index)
|
||||
}
|
||||
updatePageState(index, STATE_DOWNLOADING)
|
||||
if (!force && index in mSpiderDen) {
|
||||
return updatePageState(index, STATE_FINISHED)
|
||||
}
|
||||
if (force) {
|
||||
pTokenLock.withLock {
|
||||
val pToken = mSpiderInfo.pTokenMap[index]
|
||||
if (pToken == TOKEN_FAILED) mSpiderInfo.pTokenMap.remove(index)
|
||||
}
|
||||
}
|
||||
val previousPToken: String?
|
||||
val pToken: String
|
||||
|
||||
pTokenLock.withLock {
|
||||
if (force) {
|
||||
if (mSpiderInfo.pTokenMap[index] == TOKEN_FAILED) {
|
||||
mSpiderInfo.pTokenMap.remove(index)
|
||||
}
|
||||
}
|
||||
pToken = getPToken(index)
|
||||
?: return updatePageState(index, STATE_FAILED, PTOKEN_FAILED_MESSAGE).also {
|
||||
mSpiderInfo.pTokenMap[index] = TOKEN_FAILED
|
||||
@@ -611,11 +613,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
previousPToken = getPToken(index - 1)
|
||||
}
|
||||
|
||||
delayLock.withLock {
|
||||
delay(mDownloadDelay - lastRequestTime.elapsedNow())
|
||||
lastRequestTime = TimeSource.Monotonic.markNow()
|
||||
}
|
||||
|
||||
var skipHathKey: String? = null
|
||||
var originImageUrl: String? = null
|
||||
var error: String? = null
|
||||
@@ -738,31 +735,37 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
val currentJob = mDecodeJobMap[index]
|
||||
if (currentJob?.isActive != true) {
|
||||
mDecodeJobMap[index] = launch {
|
||||
doInJob(index)
|
||||
runCatching {
|
||||
mSemaphore.withPermit {
|
||||
mFetcherJobMap[index]?.join()
|
||||
if (mPageStateArray[index] == STATE_FINISHED) {
|
||||
doInJob(index)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
if (mPageStateArray[index] == STATE_FINISHED) {
|
||||
notifyGetImageFailure(index, DECODE_ERROR)
|
||||
}
|
||||
if (it is CancellationException) {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doInJob(index: Int) {
|
||||
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
|
||||
val src = mSpiderDen.getImageSource(index) ?: return
|
||||
val image = Image.decode(src)
|
||||
checkNotNull(image)
|
||||
runCatching {
|
||||
val src = mSpiderDen.getImageSource(index) ?: return
|
||||
val image = mSemaphore.withPermit { Image.decode(src) }
|
||||
checkNotNull(image)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image.recycle()
|
||||
throw it
|
||||
}
|
||||
notifyGetImageSuccess(index, image)
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
notifyGetImageFailure(index, DECODE_ERROR)
|
||||
if (it is CancellationException) {
|
||||
throw it
|
||||
}
|
||||
image.recycle()
|
||||
throw it
|
||||
}
|
||||
notifyGetImageSuccess(index, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +314,11 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
|
||||
val selected = showSelectItemWithIcon(items, EhUtils.getSuitableTitle(info))
|
||||
with(findActivity<MainActivity>()) {
|
||||
when (selected) {
|
||||
0 -> navToReader(info)
|
||||
0 -> {
|
||||
EhDB.putHistoryInfo(info)
|
||||
navToReader(info)
|
||||
}
|
||||
|
||||
1 -> withUIContext {
|
||||
if (downloaded) {
|
||||
confirmRemoveDownload(info)
|
||||
|
||||
@@ -49,27 +49,30 @@ import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.DrawerState2
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.ModalSideDrawer
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ShapeDefaults
|
||||
import androidx.compose.material3.SideDrawer
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.material3.rememberDrawerState2
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.currentCompositeKeyHash
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -79,8 +82,14 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.BlurEffect
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
@@ -90,6 +99,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
@@ -117,7 +127,7 @@ import com.hippo.ehviewer.ui.destinations.WhatshotScreenDestination
|
||||
import com.hippo.ehviewer.ui.screen.asDst
|
||||
import com.hippo.ehviewer.ui.screen.asDstWith
|
||||
import com.hippo.ehviewer.ui.screen.navWithUrl
|
||||
import com.hippo.ehviewer.ui.screen.navigate
|
||||
import com.hippo.ehviewer.ui.screen.navigateTo
|
||||
import com.hippo.ehviewer.ui.settings.showNewVersion
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
|
||||
@@ -126,6 +136,7 @@ import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider
|
||||
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
||||
import com.hippo.ehviewer.updater.AppUpdater
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.calculateFraction
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
||||
import com.hippo.ehviewer.util.getUrlFromClipboard
|
||||
@@ -134,7 +145,7 @@ import com.hippo.ehviewer.util.isAtLeastS
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.sha1
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.rememberNavHostEngine
|
||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||
import com.ramcosta.composedestinations.utils.currentDestinationAsState
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
@@ -148,7 +159,7 @@ import moe.tarsin.coroutines.runSuspendCatching
|
||||
import splitties.systemservices.clipboardManager
|
||||
import splitties.systemservices.connectivityManager
|
||||
|
||||
private val navItems = arrayOf(
|
||||
private val navItems = arrayOf<Triple<DirectionDestinationSpec, Int, ImageVector>>(
|
||||
Triple(HomePageScreenDestination, R.string.homepage, Icons.Default.Home),
|
||||
Triple(SubscriptionScreenDestination, R.string.subscription, EhIcons.Default.Subscriptions),
|
||||
Triple(WhatshotScreenDestination, R.string.whats_hot, Icons.Default.Whatshot),
|
||||
@@ -203,7 +214,7 @@ class MainActivity : EhActivity() {
|
||||
val configuration = LocalConfiguration.current
|
||||
val dialogState = LocalDialogState.current
|
||||
val navDrawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val sideSheetState = rememberDrawerState(DrawerValue.Closed)
|
||||
val sideSheetState = rememberDrawerState2(DrawerValue.Closed)
|
||||
val snackbarState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val navController = rememberNavController()
|
||||
@@ -225,7 +236,7 @@ class MainActivity : EhActivity() {
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
navController.navigate(DownloadScreenDestination)
|
||||
navController.navigateTo(DownloadScreenDestination)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,25 +270,24 @@ class MainActivity : EhActivity() {
|
||||
if ("text/plain" == type) {
|
||||
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (keyword != null && !navController.navWithUrl(keyword)) {
|
||||
navController.navigate(ListUrlBuilder(mKeyword = keyword).asDst())
|
||||
navController.navigateTo(ListUrlBuilder(mKeyword = keyword).asDst())
|
||||
}
|
||||
} else if (type != null && type.startsWith("image/")) {
|
||||
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
||||
if (null != uri) {
|
||||
withIOContext { uri.asUniFile().sha1() }?.let {
|
||||
navController.navigate(
|
||||
ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
|
||||
hash = it,
|
||||
).asDst(),
|
||||
)
|
||||
}
|
||||
val hash = withIOContext { uri.asUniFile().sha1() }
|
||||
navController.navigateTo(
|
||||
ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
|
||||
hash = hash,
|
||||
).asDst(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
DownloadService.ACTION_START_DOWNLOADSCENE -> {
|
||||
val args = intent.getBundleExtra(DownloadService.ACTION_START_DOWNLOADSCENE_ARGS)
|
||||
navController.navigate(DownloadsScreenDestination)
|
||||
navController.navigateTo(DownloadsScreenDestination)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,7 +318,7 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
val snackMessage = stringResource(R.string.clipboard_gallery_url_snack_message)
|
||||
val snackAction = stringResource(R.string.clipboard_gallery_url_snack_action)
|
||||
LifecycleResumeEffect {
|
||||
LifecycleResumeEffect(scope) {
|
||||
val job = scope.launch {
|
||||
delay(300)
|
||||
val text = clipboardManager.getUrlFromClipboard(applicationContext)
|
||||
@@ -317,11 +327,11 @@ class MainActivity : EhActivity() {
|
||||
val result1 = GalleryDetailUrlParser.parse(text, false)
|
||||
var launch: (() -> Unit)? = null
|
||||
if (result1 != null) {
|
||||
launch = { navController.navigate(result1.gid asDstWith result1.token) }
|
||||
launch = { navController.navigateTo(result1.gid asDstWith result1.token) }
|
||||
}
|
||||
val result2 = GalleryPageUrlParser.parse(text, false)
|
||||
if (result2 != null) {
|
||||
launch = { navController.navigate(ProgressScreenDestination(result2.gid, result2.pToken, result2.page)) }
|
||||
launch = { navController.navigateTo(ProgressScreenDestination(result2.gid, result2.pToken, result2.page)) }
|
||||
}
|
||||
launch?.let {
|
||||
val ret = snackbarState.showSnackbar(snackMessage, snackAction, true)
|
||||
@@ -358,9 +368,11 @@ class MainActivity : EhActivity() {
|
||||
},
|
||||
) {
|
||||
LocalTouchSlopProvider(Settings.touchSlopFactor.toFloat()) {
|
||||
val minOffset = -with(density) { 360.dp.toPx() }
|
||||
ModalNavigationDrawer(
|
||||
drawerContent = {
|
||||
ModalDrawerSheet(
|
||||
drawerState = navDrawerState,
|
||||
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 56).dp),
|
||||
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Start),
|
||||
) {
|
||||
@@ -398,7 +410,13 @@ class MainActivity : EhActivity() {
|
||||
gesturesEnabled = !drawerLocked || navDrawerState.isOpen,
|
||||
) {
|
||||
val sheet = sideSheet.firstOrNull()
|
||||
SideDrawer(
|
||||
val radius by remember {
|
||||
snapshotFlow {
|
||||
val step = calculateFraction(minOffset, 0f, navDrawerState.currentOffset)
|
||||
with(density) { lerp(0, 10, step).dp.toPx() }
|
||||
}
|
||||
}.collectAsState(0f)
|
||||
ModalSideDrawer(
|
||||
drawerContent = {
|
||||
if (sheet != null) {
|
||||
ModalDrawerSheet(
|
||||
@@ -412,6 +430,13 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.graphicsLayer {
|
||||
if (radius != 0f) {
|
||||
renderEffect = BlurEffect(radius, radius, TileMode.Clamp)
|
||||
shape = RectangleShape
|
||||
clip = true
|
||||
}
|
||||
},
|
||||
drawerState = sideSheetState,
|
||||
gesturesEnabled = sheet != null && !drawerLocked,
|
||||
) {
|
||||
@@ -422,12 +447,8 @@ class MainActivity : EhActivity() {
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
navGraph = NavGraphs.root,
|
||||
startRoute = if (Settings.needSignIn) {
|
||||
SignInScreenDestination
|
||||
} else {
|
||||
StartDestination
|
||||
},
|
||||
engine = rememberNavHostEngine(rootDefaultAnimations = rememberEhNavAnim()),
|
||||
startRoute = if (Settings.needSignIn) SignInScreenDestination else StartDestination,
|
||||
defaultTransitions = rememberEhNavAnim(),
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
@@ -508,7 +529,7 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val LocalNavDrawerState = compositionLocalOf<DrawerState2> { error("CompositionLocal LocalNavDrawerState not present!") }
|
||||
val LocalNavDrawerState = compositionLocalOf<DrawerState> { error("CompositionLocal LocalNavDrawerState not present!") }
|
||||
val LocalSideSheetState = compositionLocalOf<DrawerState2> { error("CompositionLocal LocalSideSheetState not present!") }
|
||||
val LocalDrawerLockHandle = compositionLocalOf<SnapshotStateList<Int>> { error("CompositionLocal LocalDrawerLockHandle not present!") }
|
||||
val LocalSnackBarHostState = compositionLocalOf<SnackbarHostState> { error("CompositionLocal LocalSnackBarHostState not present!") }
|
||||
|
||||
@@ -2,17 +2,21 @@ package com.hippo.ehviewer.ui
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.hippo.ehviewer.ui.theme.EhTheme
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.ProvideVectorPainterCache
|
||||
import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
import soup.compose.material.motion.animation.materialSharedAxisXIn
|
||||
import soup.compose.material.motion.animation.materialSharedAxisXOut
|
||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||
@@ -46,14 +50,14 @@ inline fun ComponentActivity.setMD3Content(crossinline content: @Composable () -
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberEhNavAnim(): RootNavGraphDefaultAnimations {
|
||||
fun rememberEhNavAnim(): NavHostAnimatedDestinationStyle {
|
||||
val slideDistance = rememberSlideDistance()
|
||||
return remember {
|
||||
RootNavGraphDefaultAnimations(
|
||||
enterTransition = { materialSharedAxisXIn(true, slideDistance) },
|
||||
exitTransition = { materialSharedAxisXOut(true, slideDistance) },
|
||||
popEnterTransition = { materialSharedAxisXIn(false, slideDistance) },
|
||||
popExitTransition = { materialSharedAxisXOut(false, slideDistance) },
|
||||
)
|
||||
return remember(slideDistance) {
|
||||
object : NavHostAnimatedDestinationStyle() {
|
||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = { materialSharedAxisXIn(true, slideDistance) }
|
||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = { materialSharedAxisXOut(true, slideDistance) }
|
||||
override val popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = { materialSharedAxisXIn(false, slideDistance) }
|
||||
override val popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = { materialSharedAxisXOut(false, slideDistance) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,11 +43,10 @@ import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@RootNavGraph(start = true)
|
||||
@Destination
|
||||
@Destination<RootGraph>(start = true)
|
||||
@Composable
|
||||
fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
LockDrawer(true)
|
||||
|
||||
@@ -18,13 +18,14 @@ import com.hippo.ehviewer.ui.StartDestination
|
||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||
import com.hippo.ehviewer.util.setDefaultSettings
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import io.ktor.http.Url
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun WebViewSignInScreen(navigator: DestinationsNavigator) {
|
||||
LockDrawer(true)
|
||||
|
||||
@@ -82,7 +82,7 @@ fun SearchFilter(
|
||||
selected = isCategoryChecked(it.first),
|
||||
onClick = { onCategoryChanged(category xor it.first) },
|
||||
label = { Text(text = stringResource(id = it.second)) },
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
modifier = Modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ fun GalleryPager(
|
||||
val items = pageLoader.pages
|
||||
when (type) {
|
||||
DEFAULT, LEFT_TO_RIGHT, RIGHT_TO_LEFT -> {
|
||||
val zoomableState = rememberZoomableState(zoomSpec = zoomSpec)
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier,
|
||||
@@ -51,6 +50,7 @@ fun GalleryPager(
|
||||
key = { it },
|
||||
) { index ->
|
||||
val page = items[index]
|
||||
val zoomableState = rememberZoomableState(zoomSpec = zoomSpec)
|
||||
val status by page.status.collectAsState()
|
||||
val boxModifier = if (status == Page.State.READY) {
|
||||
Modifier.zoomable(
|
||||
@@ -79,13 +79,13 @@ fun GalleryPager(
|
||||
}
|
||||
}
|
||||
VERTICAL -> {
|
||||
val zoomableState = rememberZoomableState(zoomSpec = zoomSpec)
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier,
|
||||
key = { it },
|
||||
) { index ->
|
||||
val page = items[index]
|
||||
val zoomableState = rememberZoomableState(zoomSpec = zoomSpec)
|
||||
val status by page.status.collectAsState()
|
||||
val boxModifier = if (status == Page.State.READY) {
|
||||
Modifier.zoomable(
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.ui.composing
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.ui.reader.PageIndicatorText
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderAppBars
|
||||
@@ -53,7 +54,7 @@ import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.kt.unreachable
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
// Workaround for incorrect fling velocity on zoom gesture end
|
||||
|
||||
@@ -114,6 +114,7 @@ import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.util.findActivity
|
||||
import com.hippo.ehviewer.util.mapToLongArray
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
@@ -126,7 +127,7 @@ import moe.tarsin.coroutines.onEachLatest
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
var gridView by Settings.gridView.asMutableState()
|
||||
@@ -265,7 +266,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
},
|
||||
)
|
||||
}
|
||||
itemsIndexed(labelList, key = { _, item -> item.id!! }) { index, (item) ->
|
||||
itemsIndexed(labelList, key = { _, item -> item.id!! }) { index, (item, _, id) ->
|
||||
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
||||
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
|
||||
LaunchedEffect(dismissState) {
|
||||
@@ -287,7 +288,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ReorderableItem(reorderableLabelState, key = item) { isDragging ->
|
||||
ReorderableItem(reorderableLabelState, key = id!!) { isDragging ->
|
||||
SwipeToDismissBox2(
|
||||
state = dismissState,
|
||||
backgroundContent = {},
|
||||
@@ -493,7 +494,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
) {
|
||||
items(list, key = { it.gid }) { info ->
|
||||
val checked = info.gid in checkedInfoMap
|
||||
CheckableItem(checked = checked, modifier = Modifier.animateItemPlacement()) { interactionSource ->
|
||||
CheckableItem(checked = checked, modifier = Modifier.animateItem()) { interactionSource ->
|
||||
DownloadCard(
|
||||
onClick = {
|
||||
if (selectMode) {
|
||||
|
||||
@@ -80,6 +80,7 @@ import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.mapToLongArray
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
@@ -90,7 +91,7 @@ import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.onEachLatest
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
// Immutables
|
||||
|
||||
@@ -7,13 +7,14 @@ import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.widget.TextView
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -64,6 +65,7 @@ import androidx.compose.ui.platform.LocalTextToolbar
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.parseAsHtml
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.max
|
||||
@@ -90,7 +92,6 @@ import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
|
||||
import com.hippo.ehviewer.ui.tools.normalizeSpan
|
||||
import com.hippo.ehviewer.ui.tools.rememberBBCodeTextToolbar
|
||||
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
||||
import com.hippo.ehviewer.ui.tools.toAnnotatedString
|
||||
import com.hippo.ehviewer.ui.tools.toBBCode
|
||||
import com.hippo.ehviewer.ui.tools.updateSpan
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
@@ -98,6 +99,7 @@ import com.hippo.ehviewer.util.TextUrl
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
@@ -143,7 +145,7 @@ private fun Context.generateComment(
|
||||
|
||||
private val MinimumContentPaddingEditText = 88.dp
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
LockDrawer(true)
|
||||
@@ -285,11 +287,7 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
||||
val additionalPadding = if (commenting) {
|
||||
editTextMeasured
|
||||
} else {
|
||||
if (!comments.hasMore) {
|
||||
MinimumContentPaddingEditText
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
0.dp
|
||||
}
|
||||
val voteUpSucceed = stringResource(R.string.vote_up_successfully)
|
||||
val cancelVoteUpSucceed = stringResource(R.string.cancel_vote_up_successfully)
|
||||
@@ -335,7 +333,7 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
||||
}
|
||||
if (comment.editable) {
|
||||
onSelect(editComment) {
|
||||
userComment = TextFieldValue(comment.comment.parseAsHtml().toAnnotatedString())
|
||||
userComment = TextFieldValue(comment.comment.parseAsHtml())
|
||||
commentId = comment.id
|
||||
commenting = true
|
||||
}
|
||||
@@ -358,7 +356,7 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
||||
}
|
||||
|
||||
GalleryCommentCard(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
modifier = Modifier.animateItem(),
|
||||
comment = item,
|
||||
onUserClick = {
|
||||
navigate(
|
||||
@@ -376,28 +374,36 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
||||
}
|
||||
if (comments.hasMore) {
|
||||
item {
|
||||
AnimatedVisibility(refreshing) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center).padding(keylineMargin),
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(!refreshing) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
launchIO {
|
||||
refreshing = true
|
||||
runSuspendCatching { refreshComment(true) }
|
||||
refreshing = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().padding(keylineMargin),
|
||||
Crossfade(targetState = refreshing, modifier = Modifier.padding(keylineMargin), label = "refreshing") {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().height(40.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.click_more_comments))
|
||||
if (it) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = {
|
||||
launchIO {
|
||||
refreshing = true
|
||||
runSuspendCatching { refreshComment(true) }
|
||||
refreshing = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.click_more_comments))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Workaround for comment list lagging when reaching the bottom
|
||||
// https://github.com/FooIbar/EhViewer/issues/994
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(MinimumContentPaddingEditText))
|
||||
}
|
||||
}
|
||||
}
|
||||
PullToRefreshContainer(
|
||||
|
||||
@@ -156,6 +156,7 @@ import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.requestPermission
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
@@ -210,7 +211,7 @@ private fun List<GalleryTagGroup>.getArtistTag(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
LockDrawer(true)
|
||||
@@ -483,7 +484,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
navigator.navigate(info.gid asDstWith info.token)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
if (galleryDetail.newerVersions.isNotEmpty()) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
CrystalCard(
|
||||
@@ -493,7 +494,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
}
|
||||
Text(text = stringResource(id = R.string.newer_version_available))
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||
@@ -639,7 +640,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
fun getAllRatingText(rating: Float, ratingCount: Int): String {
|
||||
return getString(
|
||||
R.string.rating_text,
|
||||
@@ -664,7 +665,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
var rating by remember { mutableFloatStateOf(galleryDetail.rating.coerceAtLeast(.5f)) }
|
||||
var text by remember { mutableIntStateOf(getRatingText(rating)) }
|
||||
Text(text = stringResource(id = text), style = MaterialTheme.typography.bodyLarge)
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
GalleryRatingBar(
|
||||
rating = rating,
|
||||
onRatingChange = {
|
||||
@@ -695,11 +696,11 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
GalleryDetailRating(rating = galleryDetail.rating)
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
Text(text = ratingText)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
val tags = galleryDetail.tags
|
||||
if (tags.isEmpty()) {
|
||||
Box(
|
||||
@@ -760,7 +761,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
if (Settings.showComments) {
|
||||
GalleryDetailComment(galleryDetail.comments.comments)
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.strip_item_padding_v)))
|
||||
@@ -894,7 +895,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
BelowHeader(galleryDetail)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize().padding(keylineMargin),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
@@ -954,7 +955,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
BelowHeader(galleryDetail)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize().padding(keylineMargin),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
|
||||
@@ -140,6 +140,7 @@ import com.hippo.ehviewer.util.pickVisualMedia
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.sha1
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.spec.Direction
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
@@ -154,23 +155,23 @@ import moe.tarsin.coroutines.runSuspendCatching
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun HomePageScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(), navigator)
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SubscriptionScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_SUBSCRIPTION), navigator)
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun WhatshotScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_WHATS_HOT), navigator)
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun ToplistScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_TOPLIST, mKeyword = Settings.recentToplist), navigator)
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
val searchFieldState = rememberTextFieldState()
|
||||
|
||||
@@ -51,11 +51,12 @@ import com.hippo.ehviewer.ui.tools.getOffset
|
||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.util.flattenForEach
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun GalleryPreviewScreen(detail: GalleryDetail, toNextPage: Boolean, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
LockDrawer(true)
|
||||
|
||||
@@ -56,12 +56,13 @@ import com.hippo.ehviewer.ui.tools.SwipeToDismissBox2
|
||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun HistoryScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
val title = stringResource(id = R.string.history)
|
||||
@@ -141,7 +142,7 @@ fun HistoryScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
SwipeToDismissBox2(
|
||||
state = dismissState,
|
||||
backgroundContent = {},
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
modifier = Modifier.animateItem(),
|
||||
) {
|
||||
GalleryInfoListItem(
|
||||
onClick = { navigate(info.asDst()) },
|
||||
|
||||
@@ -40,12 +40,12 @@ fun DestinationsNavigator.popNavigate(direction: Direction) = navigate(direction
|
||||
@MainThread
|
||||
fun NavController.navWithUrl(url: String): Boolean {
|
||||
val dest = urlToDestination(url) ?: return false
|
||||
navigate(dest)
|
||||
navigateTo(dest)
|
||||
return true
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun NavController.navigate(direction: Direction) = navigate(direction.route)
|
||||
fun NavController.navigateTo(direction: Direction) = navigate(direction.route)
|
||||
|
||||
fun BaseGalleryInfo.asDst() = GalleryDetailScreenDestination(GalleryInfoArgs(this))
|
||||
|
||||
|
||||
@@ -27,11 +27,12 @@ import com.hippo.ehviewer.icons.big.SadAndroid
|
||||
import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun ProgressScreen(gid: Long, token: String, page: Int, navigator: DestinationsNavigator) {
|
||||
LockDrawer(true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.hippo.ehviewer.ui.screen
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
@@ -20,15 +19,14 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.text.input.forEachTextValue
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.text.input.textAsFlow
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
@@ -41,18 +39,20 @@ import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.SearchBarHorizontalPadding
|
||||
import androidx.compose.material3.SearchBarInputField
|
||||
import androidx.compose.material3.Text
|
||||
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.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -61,7 +61,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import arrow.fx.coroutines.mapIndexed
|
||||
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -72,13 +71,14 @@ import com.hippo.ehviewer.dao.SearchDao
|
||||
import com.hippo.ehviewer.ui.LocalNavDrawerState
|
||||
import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.rememberCompositionActiveState
|
||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||
import com.jamal.composeprefs3.ui.ifTrueThen
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -117,7 +117,7 @@ fun SearchBarScreen(
|
||||
) {
|
||||
var mSuggestionList by remember { mutableStateOf(emptyList<Suggestion>()) }
|
||||
val mSearchDatabase = searchDatabase.searchDao()
|
||||
var active by rememberSaveable { mutableStateOf(false) }
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val context = LocalContext.current
|
||||
val dialogState = LocalDialogState.current
|
||||
@@ -180,9 +180,9 @@ fun SearchBarScreen(
|
||||
onSearchHidden()
|
||||
}
|
||||
|
||||
if (active) {
|
||||
if (expanded) {
|
||||
LaunchedEffect(Unit) {
|
||||
searchFieldState.forEachTextValue {
|
||||
snapshotFlow { searchFieldState.text }.collectLatest {
|
||||
updateSuggestions()
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ fun SearchBarScreen(
|
||||
|
||||
fun hideSearchView() {
|
||||
onSearchViewHidden()
|
||||
active = false
|
||||
expanded = false
|
||||
}
|
||||
|
||||
fun onApplySearch() {
|
||||
@@ -217,13 +217,13 @@ fun SearchBarScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val scrollAwayModifier = if (!active) {
|
||||
val scrollAwayModifier = if (!expanded) {
|
||||
Modifier.offset { IntOffset(0, searchBarOffsetY()) }
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
// Placeholder, fill immutable SearchBar padding
|
||||
@@ -242,56 +242,66 @@ fun SearchBarScreen(
|
||||
.padding(top = 48.dp) then scrollAwayModifier,
|
||||
)
|
||||
}
|
||||
val onExpandedChange = { v: Boolean ->
|
||||
if (v) {
|
||||
onSearchViewExpanded()
|
||||
} else {
|
||||
onSearchViewHidden()
|
||||
}
|
||||
expanded = v
|
||||
}
|
||||
val activeState = rememberCompositionActiveState()
|
||||
SearchBar(
|
||||
modifier = Modifier.fillMaxWidth().align(Alignment.TopCenter) then scrollAwayModifier
|
||||
modifier = Modifier.align(Alignment.TopCenter) then scrollAwayModifier
|
||||
.windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)),
|
||||
state = searchFieldState,
|
||||
onSearch = {
|
||||
hideSearchView()
|
||||
onApplySearch()
|
||||
},
|
||||
active = active,
|
||||
onActiveChange = {
|
||||
if (it) {
|
||||
onSearchViewExpanded()
|
||||
} else {
|
||||
onSearchViewHidden()
|
||||
}
|
||||
active = it
|
||||
},
|
||||
title = title.ifNotNullThen { Text(title!!, overflow = TextOverflow.Ellipsis) },
|
||||
placeholder = searchFieldHint.ifNotNullThen { Text(searchFieldHint!!) },
|
||||
leadingIcon = {
|
||||
if (active) {
|
||||
IconButton(onClick = { hideSearchView() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
|
||||
}
|
||||
} else {
|
||||
val drawerState = LocalNavDrawerState.current
|
||||
IconButton(onClick = { scope.launchUI { drawerState.open() } }) {
|
||||
Icon(Icons.Default.Menu, contentDescription = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
if (active) {
|
||||
val hasText by searchFieldState.textAsFlow().mapIndexed { index, text ->
|
||||
// Workaround for https://github.com/FooIbar/EhViewer/issues/715
|
||||
if (index == 0) delay(700)
|
||||
text.isNotEmpty()
|
||||
}.collectAsState(false)
|
||||
if (hasText) {
|
||||
IconButton(onClick = { searchFieldState.clearText() }) {
|
||||
Icon(Icons.Default.Close, contentDescription = null)
|
||||
inputField = {
|
||||
SearchBarInputField(
|
||||
state = searchFieldState,
|
||||
onSearch = {
|
||||
hideSearchView()
|
||||
onApplySearch()
|
||||
},
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
modifier = Modifier.widthIn(max = maxWidth - SearchBarHorizontalPadding * 2),
|
||||
label = title.ifNotNullThen {
|
||||
Text(title!!, overflow = TextOverflow.Ellipsis)
|
||||
}.takeUnless {
|
||||
val contentActive by activeState.state
|
||||
expanded || contentActive || searchFieldState.text.isNotEmpty()
|
||||
},
|
||||
placeholder = searchFieldHint.ifNotNullThen { Text(searchFieldHint!!) },
|
||||
leadingIcon = {
|
||||
if (expanded) {
|
||||
IconButton(onClick = { hideSearchView() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
|
||||
}
|
||||
} else {
|
||||
val drawerState = LocalNavDrawerState.current
|
||||
IconButton(onClick = { scope.launchUI { drawerState.open() } }) {
|
||||
Icon(Icons.Default.Menu, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
trailingIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
if (expanded) {
|
||||
if (searchFieldState.text.isNotEmpty()) {
|
||||
IconButton(onClick = { searchFieldState.clearText() }) {
|
||||
Icon(Icons.Default.Close, contentDescription = null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
trailingIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
) {
|
||||
activeState.Anchor()
|
||||
filter?.invoke()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -319,7 +329,7 @@ fun SearchBarScreen(
|
||||
}
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
modifier = Modifier.clickable { it.onClick() }.animateItemPlacement(),
|
||||
modifier = Modifier.clickable { it.onClick() }.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ 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.text.parseAsHtml
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.parseAsHtml
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -35,13 +35,13 @@ import com.hippo.ehviewer.ui.destinations.LicenseScreenDestination
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.toAnnotatedString
|
||||
import com.hippo.ehviewer.updater.AppUpdater
|
||||
import com.hippo.ehviewer.updater.Release
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.installPackage
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import java.io.File
|
||||
@@ -58,9 +58,9 @@ private fun versionCode() = "${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_S
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
private fun author() = stringResource(R.string.settings_about_author_summary).replace('$', '@').parseAsHtml().toAnnotatedString()
|
||||
private fun author() = stringResource(R.string.settings_about_author_summary).replace('$', '@').parseAsHtml()
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AboutScreen(navigator: DestinationsNavigator) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.Crash
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.File
|
||||
@@ -53,7 +54,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AdvancedScreen(navigator: DestinationsNavigator) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -50,6 +50,7 @@ import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.displayPath
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
@@ -58,7 +59,7 @@ import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import splitties.init.appCtx
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun DownloadScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
@@ -37,7 +37,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.compose.ui.text.parseAsHtml
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.asMutableState
|
||||
@@ -54,12 +54,12 @@ import com.hippo.ehviewer.ui.screen.popNavigate
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||
import com.hippo.ehviewer.ui.tools.toAnnotatedString
|
||||
import com.hippo.ehviewer.util.copyTextToClipboard
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.isAtLeastT
|
||||
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
@@ -68,7 +68,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalTime
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun EhScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
@@ -116,7 +116,7 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
||||
Column {
|
||||
val warning = stringResource(id = R.string.settings_eh_identity_cookies_signed)
|
||||
val str = cookies.joinToString("\n") { (k, v) -> "$k: $v" }
|
||||
Text(text = warning.parseAsHtml().toAnnotatedString())
|
||||
Text(text = warning.parseAsHtml())
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
OutlinedTextField(
|
||||
value = str,
|
||||
|
||||
@@ -58,13 +58,14 @@ import com.hippo.ehviewer.dao.FilterMode
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.groupByToObserved
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun FilterScreen(navigator: DestinationsNavigator) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -226,7 +227,7 @@ fun FilterScreen(navigator: DestinationsNavigator) {
|
||||
stickyHeader(key = filterMode) {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp).animateItemPlacement(),
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp).animateItem(),
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
@@ -234,7 +235,7 @@ fun FilterScreen(navigator: DestinationsNavigator) {
|
||||
items(filters, key = { requireNotNull(it.id) }) { filter ->
|
||||
val filterCheckBoxRecomposeScope = currentRecomposeScope
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().animateItemPlacement().clickable { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } },
|
||||
modifier = Modifier.fillMaxWidth().animateItem().clickable { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
|
||||
@@ -21,9 +21,10 @@ 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.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun LicenseScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
@@ -19,9 +19,10 @@ import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.util.setDefaultSettings
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun MyTagsScreen(navigator: DestinationsNavigator) {
|
||||
val url = EhUrl.myTagsUrl
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import arrow.atomic.Atomic
|
||||
import com.hippo.ehviewer.ui.destinations.DirectionDestination
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.ui.settings.PreferenceTokens.PreferenceTextPadding
|
||||
import com.hippo.ehviewer.util.ProgressDialog
|
||||
@@ -45,6 +44,7 @@ import com.jamal.composeprefs3.ui.prefs.SpannedTextPref
|
||||
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
||||
import com.jamal.composeprefs3.ui.prefs.TextPref
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.math.roundToInt
|
||||
@@ -64,7 +64,7 @@ object PreferenceTokens {
|
||||
fun PreferenceHeader(
|
||||
icon: Painter,
|
||||
@StringRes title: Int,
|
||||
childRoute: DirectionDestination,
|
||||
childRoute: DirectionDestinationSpec,
|
||||
navigator: DestinationsNavigator,
|
||||
) {
|
||||
Row(
|
||||
@@ -82,7 +82,7 @@ fun PreferenceHeader(
|
||||
fun PreferenceHeader(
|
||||
icon: ImageVector,
|
||||
@StringRes title: Int,
|
||||
childRoute: DirectionDestination,
|
||||
childRoute: DirectionDestinationSpec,
|
||||
navigator: DestinationsNavigator,
|
||||
) = PreferenceHeader(
|
||||
icon = rememberVectorPainter(image = icon),
|
||||
|
||||
@@ -29,11 +29,12 @@ import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun PrivacyScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
@@ -27,9 +27,10 @@ import com.hippo.ehviewer.ui.destinations.DownloadScreenDestination
|
||||
import com.hippo.ehviewer.ui.destinations.EhScreenDestination
|
||||
import com.hippo.ehviewer.ui.destinations.PrivacyScreenDestination
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SettingsScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
@@ -28,11 +28,12 @@ import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.util.setDefaultSettings
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
private const val APPLY_JS = "javascript:(function(){var apply = document.getElementById(\"apply\").children[0];apply.click();})();"
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun UConfigScreen(navigator: DestinationsNavigator) {
|
||||
LockDrawer(true)
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package com.hippo.ehviewer.ui.tools
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spanned
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -146,25 +141,6 @@ fun AnnotatedString.toBBCode() = buildString {
|
||||
}
|
||||
}
|
||||
|
||||
fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
|
||||
val spanned = this@toAnnotatedString
|
||||
append(spanned.toString())
|
||||
getSpans(0, spanned.length, Any::class.java).forEach { span ->
|
||||
val start = getSpanStart(span)
|
||||
val end = getSpanEnd(span)
|
||||
fun addStyle(spanStyle: SpanStyle) = addStyle(spanStyle, start, end)
|
||||
when (span) {
|
||||
is StrikethroughSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough))
|
||||
is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||
is StyleSpan -> {
|
||||
val s = span.style
|
||||
if (s and Typeface.BOLD != 0) addStyle(SpanStyle(fontWeight = FontWeight.Bold))
|
||||
if (s and Typeface.ITALIC != 0) addStyle(SpanStyle(fontStyle = FontStyle.Italic))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberBBCodeTextToolbar(textFieldValue: MutableState<TextFieldValue>): TextToolbar {
|
||||
var tfv by textFieldValue
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.hippo.ehviewer.ui.tools
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
@JvmInline
|
||||
value class CompositionActiveState(val state: MutableState<Boolean>) {
|
||||
@Composable
|
||||
fun Anchor() = DisposableEffect(state) {
|
||||
state.value = true
|
||||
onDispose {
|
||||
state.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberCompositionActiveState() = remember {
|
||||
CompositionActiveState(mutableStateOf(false))
|
||||
}
|
||||
@@ -100,9 +100,7 @@ class DialogState {
|
||||
content = null
|
||||
}
|
||||
|
||||
// Move to crossinline on next compose compiler update
|
||||
// https://issuetracker.google.com/issues/325502738
|
||||
suspend inline fun <R> dialog(noinline block: @Composable (CancellableContinuation<R>) -> Unit) = suspendCancellableCoroutine { cont ->
|
||||
suspend inline fun <R> dialog(crossinline block: @Composable (CancellableContinuation<R>) -> Unit) = suspendCancellableCoroutine { cont ->
|
||||
cont.invokeOnCancellation { dismiss() }
|
||||
val realContinuation = object : CancellableContinuation<R> by cont {
|
||||
override fun resumeWith(result: Result<R>) {
|
||||
|
||||
@@ -42,7 +42,7 @@ fun ReorderableItemScope.DragHandle(
|
||||
} else if (isAtLeastOMR1) {
|
||||
HapticFeedbackConstants.KEYBOARD_RELEASE
|
||||
} else {
|
||||
HapticFeedbackConstants.VIRTUAL_KEY_RELEASE
|
||||
HapticFeedbackConstants.VIRTUAL_KEY
|
||||
}
|
||||
view.performHapticFeedback(feedbackConstant)
|
||||
onDragStopped()
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.executeAndParseAs
|
||||
import com.hippo.ehviewer.client.executeSafely
|
||||
import com.hippo.ehviewer.util.copyTo
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.prepareGet
|
||||
@@ -36,7 +37,7 @@ object AppUpdater {
|
||||
Settings.lastUpdateTime = now.epochSeconds
|
||||
if (Settings.useCIUpdateChannel) {
|
||||
val curSha = BuildConfig.COMMIT_SHA
|
||||
val branch = ghStatement(API_URL).execute { JSONObject(it.bodyAsText()).getString("default_branch") }
|
||||
val branch = ghStatement(API_URL).executeSafely { JSONObject(it.bodyAsText()).getString("default_branch") }
|
||||
val workflowRunsUrl = "$API_URL/actions/workflows/ci.yml/runs?branch=$branch&event=push&status=success&per_page=1"
|
||||
val workflowRun = ghStatement(workflowRunsUrl).executeAndParseAs<GithubWorkflowRuns>().workflowRuns[0]
|
||||
val shortSha = workflowRun.headSha.take(7)
|
||||
@@ -68,7 +69,7 @@ object AppUpdater {
|
||||
}
|
||||
|
||||
suspend fun downloadUpdate(url: String, file: File) =
|
||||
ghStatement(url).execute { response ->
|
||||
ghStatement(url).executeSafely { response ->
|
||||
if (url.endsWith("zip")) {
|
||||
response.bodyAsChannel().toInputStream().use { stream ->
|
||||
ZipInputStream(stream).use { zip ->
|
||||
|
||||
@@ -4,18 +4,23 @@ import android.os.ParcelFileDescriptor
|
||||
import android.system.Int64Ref
|
||||
import android.system.Os
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.FileDescriptor
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
|
||||
private fun sendFileTotally(from: FileDescriptor, to: FileDescriptor) {
|
||||
private fun sendFileTotally(from: FileDescriptor, to: FileDescriptor): Long {
|
||||
if (isAtLeastP) {
|
||||
Os.sendfile(to, from, Int64Ref(0), Long.MAX_VALUE)
|
||||
} else {
|
||||
FileInputStream(from).use { src ->
|
||||
FileOutputStream(to).use { dst ->
|
||||
src.channel.transferTo(0, Long.MAX_VALUE, dst.channel)
|
||||
}
|
||||
// sendfile may fail on some devices
|
||||
try {
|
||||
return Os.sendfile(to, from, Int64Ref(0), Long.MAX_VALUE)
|
||||
} catch (e: Exception) {
|
||||
logcat("sendfile", e)
|
||||
}
|
||||
}
|
||||
return FileInputStream(from).use { src ->
|
||||
FileOutputStream(to).use { dst ->
|
||||
src.channel.transferTo(0, Long.MAX_VALUE, dst.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,5 +30,5 @@ infix fun ParcelFileDescriptor.sendTo(fd: ParcelFileDescriptor) {
|
||||
}
|
||||
|
||||
infix fun UniFile.sendTo(file: UniFile) = openFileDescriptor("r").use { src ->
|
||||
file.openFileDescriptor("w").use { dst -> src sendTo dst }
|
||||
file.openFileDescriptor("wt").use { dst -> src sendTo dst }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
fun calculateFraction(a: Float, b: Float, pos: Float) = ((pos - a) / (b - a)).coerceIn(0f, 1f)
|
||||
@@ -22,7 +22,7 @@ fun UniFile.openInputStream(): FileInputStream {
|
||||
* Use Native IO/NIO directly if possible, unless you need process file content on JVM!
|
||||
*/
|
||||
fun UniFile.openOutputStream(): FileOutputStream {
|
||||
return AutoCloseOutputStream(openFileDescriptor("w"))
|
||||
return AutoCloseOutputStream(openFileDescriptor("wt"))
|
||||
}
|
||||
|
||||
fun UniFile.sha1() = openFileDescriptor("r").use { sha1(it.fd) }
|
||||
|
||||
@@ -222,7 +222,7 @@ class ReaderActivity : EhActivity() {
|
||||
mPage = savedInstanceState.getInt(KEY_PAGE, -1)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleIntent(intent)
|
||||
lifecycleScope.launchIO {
|
||||
|
||||
@@ -62,7 +62,10 @@ abstract class PageLoader {
|
||||
onRequest(index)
|
||||
}
|
||||
|
||||
val pagesAbsent = ((index - 5).coerceAtLeast(0) until (preloads + index).coerceAtMost(size)).mapNotNull { it.takeIf { cache[it] == null } }
|
||||
val pagesAbsent = ((index - 5).coerceAtLeast(0) until (preloads + index).coerceAtMost(size))
|
||||
.mapNotNullTo(mutableListOf()) { it.takeIf { it != index && cache[it] == null } }
|
||||
// Load forward first, then load backward from the nearest index
|
||||
pagesAbsent.sortBy { (index - it).coerceAtLeast(0) }
|
||||
preloadPages(pagesAbsent, (index - 10).coerceAtLeast(0) to (preloads + index + 10).coerceAtMost(size))
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
private var config: Config? = null
|
||||
|
||||
var onImageLoaded: (() -> Unit)? = null
|
||||
var onImageLoadError: (() -> Unit)? = null
|
||||
var onScaleChanged: ((newScale: Float) -> Unit)? = null
|
||||
var onViewClicked: (() -> Unit)? = null
|
||||
|
||||
/**
|
||||
* For automatic background. Will be set as background color when [onImageLoaded] is called.
|
||||
@@ -68,40 +66,22 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
background = pageBackground
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onImageLoadError() {
|
||||
onImageLoadError?.invoke()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onScaleChanged(newScale: Float) {
|
||||
onScaleChanged?.invoke(newScale)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onViewClicked() {
|
||||
onViewClicked?.invoke()
|
||||
}
|
||||
|
||||
open fun onPageSelected(forward: Boolean) {
|
||||
with(pageView as? SubsamplingScaleImageView) {
|
||||
if (this == null) return
|
||||
if (isReady) {
|
||||
landscapeZoom(forward)
|
||||
} else {
|
||||
setOnImageEventListener(
|
||||
object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||
override fun onReady() {
|
||||
setupZoom(config)
|
||||
landscapeZoom(forward)
|
||||
this@ReaderPageImageView.onImageLoaded()
|
||||
}
|
||||
|
||||
override fun onImageLoadError(e: Exception) {
|
||||
onImageLoadError()
|
||||
}
|
||||
},
|
||||
)
|
||||
setOnImageEventListener {
|
||||
setupZoom(config)
|
||||
landscapeZoom(forward)
|
||||
this@ReaderPageImageView.onImageLoaded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,7 +201,6 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
}
|
||||
},
|
||||
)
|
||||
setOnClickListener { this@ReaderPageImageView.onViewClicked() }
|
||||
}
|
||||
addView(pageView, MATCH_PARENT, MATCH_PARENT)
|
||||
}
|
||||
@@ -248,19 +227,11 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
setMinimumScaleType(config.minimumScaleType)
|
||||
setMinimumDpi(1) // Just so that very small image will be fit for initial load
|
||||
setCropBorders(config.cropBorders)
|
||||
setOnImageEventListener(
|
||||
object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||
override fun onReady() {
|
||||
setupZoom(config)
|
||||
if (isVisibleOnScreen()) landscapeZoom(true)
|
||||
this@ReaderPageImageView.onImageLoaded()
|
||||
}
|
||||
|
||||
override fun onImageLoadError(e: Exception) {
|
||||
this@ReaderPageImageView.onImageLoadError()
|
||||
}
|
||||
},
|
||||
)
|
||||
setOnImageEventListener {
|
||||
setupZoom(config)
|
||||
if (isVisibleOnScreen()) landscapeZoom(true)
|
||||
this@ReaderPageImageView.onImageLoaded()
|
||||
}
|
||||
|
||||
setImage(image, bounds)
|
||||
isVisible = true
|
||||
@@ -292,11 +263,6 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
this@ReaderPageImageView.onViewClicked()
|
||||
return super.onSingleTapConfirmed(e)
|
||||
}
|
||||
},
|
||||
)
|
||||
setOnScaleChangeListener { _, _, _ ->
|
||||
|
||||
+13
-5
@@ -2,15 +2,16 @@ package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.core.view.isVisible
|
||||
@@ -23,7 +24,6 @@ class ReaderProgressIndicator @JvmOverloads constructor(
|
||||
) : AbstractComposeView(context, attrs, defStyleAttr) {
|
||||
|
||||
init {
|
||||
layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.CENTER)
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ class ReaderProgressIndicator @JvmOverloads constructor(
|
||||
@Composable
|
||||
override fun Content() {
|
||||
EhTheme {
|
||||
if (visible) CombinedCircularProgressIndicator(progress = progress)
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
if (visible) CombinedCircularProgressIndicator(progress = progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +49,12 @@ class ReaderProgressIndicator @JvmOverloads constructor(
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
progress = 0f
|
||||
visible = false
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
fun setProgress(@IntRange(from = 0, to = 100) progress: Int) {
|
||||
this.progress = progress / 100f
|
||||
}
|
||||
|
||||
@@ -49,11 +49,6 @@ class PagerPageHolder(
|
||||
*/
|
||||
private var statusJob: Job? = null
|
||||
|
||||
/**
|
||||
* Job for progress changes of the page.
|
||||
*/
|
||||
private var progressJob: Job? = null
|
||||
|
||||
init {
|
||||
addView(progressIndicator)
|
||||
statusJob = scope.launch(Dispatchers.Main) {
|
||||
@@ -69,39 +64,25 @@ class PagerPageHolder(
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
cancelProgressJob()
|
||||
unsubscribeStatus()
|
||||
progressIndicator.hide()
|
||||
}
|
||||
|
||||
private fun launchProgressJob() {
|
||||
progressJob?.cancel()
|
||||
progressJob = scope.launch(Dispatchers.Main) {
|
||||
page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the status of the page changes.
|
||||
*
|
||||
* @param status the new status of the page.
|
||||
*/
|
||||
private fun processStatus(status: Page.State) {
|
||||
private suspend fun processStatus(status: Page.State) {
|
||||
when (status) {
|
||||
Page.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
launchProgressJob()
|
||||
setDownloading()
|
||||
page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
|
||||
}
|
||||
Page.State.READY -> {
|
||||
page.image?.let { setImage(it) }
|
||||
cancelProgressJob()
|
||||
}
|
||||
Page.State.ERROR -> {
|
||||
setError()
|
||||
cancelProgressJob()
|
||||
}
|
||||
Page.State.READY -> page.image?.let { setImage(it) } ?: setError()
|
||||
Page.State.ERROR -> setError()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,11 +94,6 @@ class PagerPageHolder(
|
||||
statusJob = null
|
||||
}
|
||||
|
||||
private fun cancelProgressJob() {
|
||||
progressJob?.cancel()
|
||||
progressJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the page is queued.
|
||||
*/
|
||||
@@ -149,8 +125,6 @@ class PagerPageHolder(
|
||||
*/
|
||||
private fun setImage(image: Image) {
|
||||
progressIndicator.setProgress(0)
|
||||
progressIndicator.hide()
|
||||
removeErrorLayout()
|
||||
setImage(
|
||||
image,
|
||||
Config(
|
||||
@@ -177,15 +151,7 @@ class PagerPageHolder(
|
||||
override fun onImageLoaded() {
|
||||
super.onImageLoaded()
|
||||
progressIndicator.hide()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an image fails to decode.
|
||||
*/
|
||||
override fun onImageLoadError() {
|
||||
super.onImageLoadError()
|
||||
progressIndicator.hide()
|
||||
showErrorLayout()
|
||||
removeErrorLayout()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+15
-79
@@ -1,11 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
@@ -34,14 +32,10 @@ class WebtoonPageHolder(
|
||||
|
||||
/**
|
||||
* Loading progress bar to indicate the current progress.
|
||||
* Needed to keep a minimum height size of the holder, otherwise the adapter would create more
|
||||
* views to fill the screen, which is not wanted.
|
||||
*/
|
||||
private val progressIndicator = createProgressIndicator()
|
||||
|
||||
/**
|
||||
* Progress bar container. Needed to keep a minimum height size of the holder, otherwise the
|
||||
* adapter would create more views to fill the screen, which is not wanted.
|
||||
*/
|
||||
private lateinit var progressContainer: ViewGroup
|
||||
private val progressIndicator = ReaderProgressIndicator(context)
|
||||
|
||||
/**
|
||||
* Error layout to show when the image fails to load.
|
||||
@@ -57,7 +51,7 @@ class WebtoonPageHolder(
|
||||
/**
|
||||
* Page of a chapter.
|
||||
*/
|
||||
private var page: ReaderPage? = null
|
||||
private lateinit var page: ReaderPage
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
@@ -66,16 +60,11 @@ class WebtoonPageHolder(
|
||||
*/
|
||||
private var statusJob: Job? = null
|
||||
|
||||
/**
|
||||
* Job for progress changes of the page.
|
||||
*/
|
||||
private var progressJob: Job? = null
|
||||
|
||||
init {
|
||||
frame.addView(progressIndicator, MATCH_PARENT, defaultHeight)
|
||||
refreshLayoutParams()
|
||||
|
||||
frame.onImageLoaded = { onImageDecoded() }
|
||||
frame.onImageLoadError = { onImageDecodeError() }
|
||||
frame.onScaleChanged = { viewer.activity.hideMenu() }
|
||||
}
|
||||
|
||||
@@ -110,25 +99,11 @@ class WebtoonPageHolder(
|
||||
*/
|
||||
override fun recycle() {
|
||||
statusJob?.cancel()
|
||||
cancelProgressJob()
|
||||
progressIndicator.hide()
|
||||
statusJob = null
|
||||
|
||||
removeErrorLayout()
|
||||
frame.recycle()
|
||||
progressIndicator.setProgress(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the progress of the page and updates view.
|
||||
*/
|
||||
private fun launchProgressJob() {
|
||||
cancelProgressJob()
|
||||
|
||||
val page = page ?: return
|
||||
|
||||
progressJob = scope.launch(Dispatchers.Main) {
|
||||
page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
|
||||
}
|
||||
progressIndicator.reset()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,38 +111,23 @@ class WebtoonPageHolder(
|
||||
*
|
||||
* @param status the new status of the page.
|
||||
*/
|
||||
private fun processStatus(status: Page.State) {
|
||||
private suspend fun processStatus(status: Page.State) {
|
||||
when (status) {
|
||||
Page.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
launchProgressJob()
|
||||
setDownloading()
|
||||
page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
|
||||
}
|
||||
Page.State.READY -> {
|
||||
page?.image?.let { setImage(it) }
|
||||
cancelProgressJob()
|
||||
}
|
||||
Page.State.ERROR -> {
|
||||
setError()
|
||||
cancelProgressJob()
|
||||
}
|
||||
Page.State.READY -> page.image?.let { setImage(it) } ?: setError()
|
||||
Page.State.ERROR -> setError()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from the progress subscription.
|
||||
*/
|
||||
private fun cancelProgressJob() {
|
||||
progressJob?.cancel()
|
||||
progressJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the page is queued.
|
||||
*/
|
||||
private fun setQueued() {
|
||||
progressContainer.isVisible = true
|
||||
progressIndicator.setProgress(0)
|
||||
progressIndicator.show()
|
||||
removeErrorLayout()
|
||||
@@ -178,7 +138,6 @@ class WebtoonPageHolder(
|
||||
* Called when the page is loading.
|
||||
*/
|
||||
private fun setLoading() {
|
||||
progressContainer.isVisible = true
|
||||
progressIndicator.show()
|
||||
removeErrorLayout()
|
||||
}
|
||||
@@ -187,7 +146,6 @@ class WebtoonPageHolder(
|
||||
* Called when the page is downloading
|
||||
*/
|
||||
private fun setDownloading() {
|
||||
progressContainer.isVisible = true
|
||||
progressIndicator.show()
|
||||
removeErrorLayout()
|
||||
}
|
||||
@@ -197,8 +155,6 @@ class WebtoonPageHolder(
|
||||
*/
|
||||
private fun setImage(drawable: Image) {
|
||||
progressIndicator.setProgress(0)
|
||||
progressIndicator.hide()
|
||||
removeErrorLayout()
|
||||
frame.setImage(
|
||||
drawable,
|
||||
ReaderPageImageView.Config(
|
||||
@@ -214,7 +170,6 @@ class WebtoonPageHolder(
|
||||
*/
|
||||
private fun setError() {
|
||||
progressIndicator.hide()
|
||||
progressContainer.isVisible = false
|
||||
initErrorLayout()
|
||||
}
|
||||
|
||||
@@ -222,27 +177,8 @@ class WebtoonPageHolder(
|
||||
* Called when the image is decoded and going to be displayed.
|
||||
*/
|
||||
private fun onImageDecoded() {
|
||||
progressContainer.isVisible = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the image fails to decode.
|
||||
*/
|
||||
private fun onImageDecodeError() {
|
||||
progressContainer.isVisible = false
|
||||
initErrorLayout()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new progress bar.
|
||||
*/
|
||||
private fun createProgressIndicator(): ReaderProgressIndicator {
|
||||
progressContainer = FrameLayout(context)
|
||||
frame.addView(progressContainer, MATCH_PARENT, defaultHeight)
|
||||
|
||||
val progress = ReaderProgressIndicator(context)
|
||||
progressContainer.addView(progress)
|
||||
return progress
|
||||
progressIndicator.hide()
|
||||
removeErrorLayout()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,8 +186,8 @@ class WebtoonPageHolder(
|
||||
*/
|
||||
private fun initErrorLayout() {
|
||||
if (errorLayout == null) {
|
||||
errorLayout = ReaderErrorLayout(context, page!!.errorMsg) {
|
||||
viewer.activity.retryPage(page!!.index)
|
||||
errorLayout = ReaderErrorLayout(context, page.errorMsg) {
|
||||
viewer.activity.retryPage(page.index)
|
||||
}.also {
|
||||
frame.addView(it)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ extern crate sha1;
|
||||
extern crate tl;
|
||||
|
||||
use android_logger::Config;
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, ensure, Result};
|
||||
use jni::objects::JByteBuffer;
|
||||
use jni::sys::{jint, jobject, JavaVM, JNI_VERSION_1_6};
|
||||
use jni::JNIEnv;
|
||||
@@ -114,7 +114,8 @@ where
|
||||
let buffer = deref_mut_direct_bytebuffer(env, str)?;
|
||||
let value = {
|
||||
let html = unsafe { from_utf8_unchecked(&buffer[..limit as usize]) };
|
||||
let dom = tl::parse(html, ParserOptions::default()).map_err(|_| anyhow!("{html}"))?;
|
||||
let dom = tl::parse(html, ParserOptions::default()).map_err(|e| anyhow!(e))?;
|
||||
ensure!(dom.version().is_some(), "{html}");
|
||||
f(&dom, html)?
|
||||
};
|
||||
let mut cursor = Cursor::new(buffer);
|
||||
|
||||
+21
-21
@@ -1,34 +1,34 @@
|
||||
[versions]
|
||||
aboutlibraries = "11.1.0"
|
||||
aboutlibraries = "11.1.1"
|
||||
accompanist = "0.34.0"
|
||||
agp = "8.4.0-alpha13"
|
||||
androidx-activity = "1.8.2"
|
||||
androidx-appcompat = "1.6.1"
|
||||
agp = "8.5.0-alpha03"
|
||||
androidx-activity = "1.9.0-rc01"
|
||||
androidx-appcompat = "1.7.0-alpha03"
|
||||
androidx-biometric = "1.2.0-alpha05"
|
||||
androidx-browser = "1.8.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"
|
||||
androidx-navigation = "2.8.0-alpha04"
|
||||
androidx-paging = "3.3.0-alpha04"
|
||||
androidx-recyclerview = "1.3.2"
|
||||
androidx-core = "1.13.0-rc01"
|
||||
androidx-core-splashscreen = "1.1.0-rc01"
|
||||
androidx-lifecycle = "2.8.0-alpha04"
|
||||
androidx-navigation = "2.8.0-SNAPSHOT"
|
||||
androidx-paging = "3.3.0-beta01"
|
||||
androidx-recyclerview = "1.4.0-alpha01"
|
||||
androidx-room = "2.6.1"
|
||||
androidx-work = "2.9.0"
|
||||
arrow = "1.2.3"
|
||||
androidx-work = "2.10.0-alpha01"
|
||||
arrow = "1.2.4"
|
||||
coil = "3.0.0-alpha06"
|
||||
compose-bom = "2024.01.00-alpha03"
|
||||
compose-compiler = "1.5.11-dev-k2.0.0-Beta4-21f5e479a96"
|
||||
compose-destinations = "1.10.2"
|
||||
kotlin = "2.0.0-Beta4"
|
||||
compose-bom = "2024.04.00-alpha01"
|
||||
compose-compiler = "1.5.11-dev-k2.0.0-Beta5-b5a216d0ac6"
|
||||
compose-destinations = "2.1.0-beta01"
|
||||
kotlin = "2.0.0-Beta5"
|
||||
kotlinx-coroutines = "1.8.0"
|
||||
kotlinx-datetime = "0.6.0-RC.2"
|
||||
kotlinx-serialization = "1.6.3"
|
||||
ksp = "2.0.0-Beta4-1.0.19"
|
||||
ksp = "2.0.0-Beta5-1.0.20"
|
||||
ktor = "3.0.0-beta-1"
|
||||
okio = "3.9.0"
|
||||
splitties = "3.0.0"
|
||||
telephoto = "0.8.0"
|
||||
telephoto = "0.9.0"
|
||||
xmlutil = "0.86.3"
|
||||
|
||||
[libraries]
|
||||
@@ -68,7 +68,7 @@ coil-network = { module = "io.coil-kt.coil3:coil-network-ktor" }
|
||||
compose-bom = { module = "dev.chrisbanes.compose:compose-bom", version.ref = "compose-bom" }
|
||||
# For renovate
|
||||
compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose-compiler" }
|
||||
compose-foundation = { module = "androidx.compose.foundation:foundation-android", version = "1.7.0-SNAPSHOT" }
|
||||
compose-foundation = { module = "androidx.compose.foundation:foundation", version = "1.7.0-SNAPSHOT" }
|
||||
compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
|
||||
compose-material3 = { module = "androidx.compose.material3:material3", version = "1.3.0-SNAPSHOT" }
|
||||
compose-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class" }
|
||||
@@ -101,7 +101,7 @@ okio-jvm = { module = "com.squareup.okio:okio-jvm", version.ref = "okio" }
|
||||
|
||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||
|
||||
reorderable = "sh.calvin.reorderable:reorderable:1.3.3"
|
||||
reorderable = "sh.calvin.reorderable:reorderable:1.4.0"
|
||||
|
||||
serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" }
|
||||
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
@@ -126,7 +126,7 @@ splitties = ["splitties-appctx", "splitties-arch-room", "splitties-preferences",
|
||||
[plugins]
|
||||
aboutlibrariesPlugin = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
composeCompilerReportGenerator = { id = "dev.shreyaspatil.compose-compiler-report-generator", version = "1.1.0" }
|
||||
composeCompilerReportGenerator = { id = "dev.shreyaspatil.compose-compiler-report-generator", version = "1.2.0" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Vendored
+10
-10
@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
+31
-1
@@ -4,9 +4,39 @@
|
||||
"config:base"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "AGP",
|
||||
"matchPackageNames": [
|
||||
"com.android.application"
|
||||
],
|
||||
"ignoreUnstable": false
|
||||
},
|
||||
{
|
||||
"groupName": "AndroidX & Jetpack Compose",
|
||||
"matchPackagePrefixes": [
|
||||
"androidx."
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"dev.chrisbanes.compose:compose-bom"
|
||||
],
|
||||
"ignoreUnstable": false
|
||||
},
|
||||
{
|
||||
"groupName": "Kotlin, KSP and Compose Compiler",
|
||||
"matchPackagePrefixes": [
|
||||
"org.jetbrains.kotlin."
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"com.google.devtools.ksp",
|
||||
"androidx.compose.compiler:compiler"
|
||||
],
|
||||
"recreateWhen": "never"
|
||||
},
|
||||
{
|
||||
"description": "Disable updates for SNAPSHOTs",
|
||||
"matchCategories": ["java"],
|
||||
"matchCategories": [
|
||||
"java"
|
||||
],
|
||||
"allowedVersions": "!/SNAPSHOT$/"
|
||||
}
|
||||
]
|
||||
|
||||
+3
-1
@@ -11,9 +11,11 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://oss.sonatype.org/content/repositories/snapshots")
|
||||
maven("https://jitpack.io")
|
||||
maven("https://androidx.dev/storage/compose-compiler/repository/")
|
||||
maven("https://androidx.dev/snapshots/builds/11527796/artifacts/repository")
|
||||
// TODO: Remove on Compose 1.7.0-alpha06 release
|
||||
maven("https://androidx.dev/snapshots/builds/11644901/artifacts/repository")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user