Compare commits

..
15 Commits
Author SHA1 Message Date
FooIbar 6a64ff51ed Prepare for release 1.11.2
Releases / build (push) Has been cancelled
2024-03-17 14:44:16 +08:00
FooIbar ea9d9cfba7 Prepare changelog for 1.11.2 2024-03-17 14:41:43 +08:00
FooIbar 75c6561896 Fix app updater getting wrong download link 2024-03-16 20:13:42 +08:00
FooIbar 8f8db82e9d Fix disowned gallery uploader parsing (#918)
* Fix disowned gallery uploader parsing

* Run rustfmt

* Refine

* Run rustfmt
2024-03-16 17:48:10 +08:00
FooIbar a114a8ac72 Minor tweak to some settings' visibility 2024-03-16 11:10:08 +08:00
FooIbar 2c6216057f Revert "Merge default and marshmallow flavors"
This reverts commit 5676de24da.

Reason for revert: Desugaring causes regression on API 26+
2024-03-15 19:55:59 +08:00
FooIbar c5cbcf7418 Reintroduce confirm dialog for swipe-to-dismiss (#914)
Observe the state changes instead of using `confirmValueChange`, which
will be called twice.
2024-03-15 18:40:51 +08:00
FooIbar 5746e3bb57 Pin compose foundation version to 1.7.0-alpha03 2024-03-14 16:05:18 +08:00
FooIbar fd3c87be22 Make reader bottom sheet scrollable and fix insets 2024-03-14 16:05:08 +08:00
FooIbar 866fff3e45 Fix favorites modification blocked by snackbar 2024-03-14 16:04:55 +08:00
FooIbar 06df7befb6 Fix tag namespace for cloud favorites search 2024-03-13 21:38:16 +08:00
FooIbar 8021f86473 Address java.util.regex.Pattern usage 2024-03-13 21:34:04 +08:00
renovate[bot]andGG Bond c9919a8b39 Cleanup CronetEngine (#901)
* Cleanup CronetEngine with UploadDataProviders

* Remove WriteChannelContent as no user now

* Cleanup

* Cleanup

* Remove unnecessary invokeOnCancellation

* Refine

* Refine

* Format

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2024-03-13 14:40:37 +08:00
renovate[bot] 386015a9ec Update dependency com.squareup.okio:okio-jvm to v3.9.0 2024-03-13 04:00:26 +00:00
FooIbar cbce98b34e Prepare for next iteration 2024-03-12 22:47:24 +08:00
40 changed files with 474 additions and 342 deletions

No files matched your search

+120 -18
View File
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
jobs:
build:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -27,6 +27,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
- name: Rustfmt Check
working-directory: ./app/src/main/rust
@@ -39,54 +40,155 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
gradle-home-cache-cleanup: true
cache-read-only: true
- name: Spotless Check
run: ./gradlew spotlessCheck
default:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
gradle-home-cache-cleanup: true
- name: CMake Cache
uses: actions/cache@v4
with:
path: app/.cxx
key: cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
restore-keys: cmake-
key: cmake-default-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
restore-keys: cmake-default-
- name: Build
run: ./gradlew assembleRelease
run: ./gradlew assembleDefaultRelease
- name: Upload ARM64
uses: actions/upload-artifact@v4
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
name: default-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v4
with:
name: x86_64-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86_64-release.apk
name: default-x86_64-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v4
with:
name: armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
name: default-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
- name: Upload x86
uses: actions/upload-artifact@v4
with:
name: x86-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86-release.apk
name: default-x86-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-x86-release.apk
- name: Upload mapping
uses: actions/upload-artifact@v4
with:
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release/mapping.txt
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease/mapping.txt
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
with:
name: native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/release/native-debug-symbols.zip
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
marshmallow:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
gradle-home-cache-cleanup: true
- name: CMake Cache
uses: actions/cache@v4
with:
path: app/.cxx
key: cmake-marshmallow-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
restore-keys: cmake-marshmallow-
- name: Build
run: ./gradlew assembleMarshmallowRelease
- name: Upload ARM64
uses: actions/upload-artifact@v4
with:
name: marshmallow-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v4
with:
name: marshmallow-x86_64-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v4
with:
name: marshmallow-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
- name: Upload x86
uses: actions/upload-artifact@v4
with:
name: marshmallow-x86-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86-release.apk
- name: Upload mapping
uses: actions/upload-artifact@v4
with:
name: marshmallow-mapping-${{ github.sha }}
path: app/build/outputs/mapping/marshmallowRelease/mapping.txt
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
with:
name: marshmallow-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
+30 -14
View File
@@ -40,23 +40,39 @@ jobs:
- name: Rename Apks
run: |
mv app/build/outputs/apk/release/app-universal-release.apk EhViewer-${{ github.ref_name }}-universal.apk
mv app/build/outputs/apk/release/app-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-arm64-v8a.apk
mv app/build/outputs/apk/release/app-x86_64-release.apk EhViewer-${{ github.ref_name }}-x86_64.apk
mv app/build/outputs/apk/release/app-x86-release.apk EhViewer-${{ github.ref_name }}-x86.apk
mv app/build/outputs/apk/release/app-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-armeabi-v7a.apk
mv app/build/outputs/mapping/release/mapping.txt EhViewer-${{ github.ref_name }}-mapping.txt
mv app/build/outputs/native-debug-symbols/release/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-native-debug-symbols.zip
mv app/build/outputs/apk/default/release/app-default-universal-release.apk EhViewer-${{ github.ref_name }}-default-universal.apk
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-x86_64.apk
mv app/build/outputs/apk/default/release/app-default-x86-release.apk EhViewer-${{ github.ref_name }}-default-x86.apk
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-x86.apk
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
mv app/build/outputs/mapping/marshmallowRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
mv app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
- name: Releases
uses: softprops/action-gh-release@v2
with:
body: Bump Version
files: |
EhViewer-${{ github.ref_name }}-universal.apk
EhViewer-${{ github.ref_name }}-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-x86_64.apk
EhViewer-${{ github.ref_name }}-x86.apk
EhViewer-${{ github.ref_name }}-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-mapping.txt
EhViewer-${{ github.ref_name }}-native-debug-symbols.zip
EhViewer-${{ github.ref_name }}-default-universal.apk
EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-default-x86_64.apk
EhViewer-${{ github.ref_name }}-default-x86.apk
EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-default-mapping.txt
EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
EhViewer-${{ github.ref_name }}-marshmallow-x86.apk
EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
+20
View File
@@ -1,5 +1,25 @@
# 变更日志
## [1.11.2] - 2024-03-17
### 重大变更
* Default 变种最低支持版本更改为 Android 8.0,重新添加了最低支持 Android 6.0 的 Marshmallow 变种
### 改进
* 重新添加了删除快速搜索/下载标签时的确认对话框
### Bug 修复
* 收藏页面 搜索栏中文标签转英文异常 [#902](https://github.com/FooIbar/EhViewer/issues/902)
* 在收藏或者取消收藏之后,收藏按钮会无反应 [#908](https://github.com/FooIbar/EhViewer/issues/908)
* 横屏页面下阅读设置UI重合 [#909](https://github.com/FooIbar/EhViewer/issues/909)
* Search bar does not work properly in Japanese [#906](https://github.com/FooIbar/EhViewer/issues/906)
* 搜索结果中的 Disowned 画廊不显示上传者
* 使用 CI 频道更新时,有时会获取到错误的更新链接
## [1.11.1] - 2024-03-12
### 重大变更
+5
View File
@@ -66,6 +66,11 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
# Download
| Flavor | Feature |
|-------------|----------------------------------|
| Default | Android 8.0+, full support |
| Marshmallow | Android 6.0-7.1, limited support |
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
+22 -3
View File
@@ -60,10 +60,10 @@ android {
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
minSdk = 23
minSdk = 26
targetSdk = 34
versionCode = 180052
versionName = "1.11.1"
versionCode = 180053
versionName = "1.11.2"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -80,6 +80,7 @@ android {
"nb-rNO",
),
)
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName$versionNameSuffix\"")
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
ndk {
@@ -87,6 +88,17 @@ android {
}
}
flavorDimensions += "api"
productFlavors {
create("default")
create("marshmallow") {
minSdk = 23
applicationIdSuffix = ".m"
versionNameSuffix = "-M"
}
}
externalNativeBuild {
cmake {
path = File("src/main/cpp/CMakeLists.txt")
@@ -251,6 +263,13 @@ 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)
}
@@ -44,7 +44,7 @@ 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.BasicTextField
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
@@ -494,7 +494,7 @@ private fun SearchBarInputField(
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
}
BasicTextField(
BasicTextField2(
state = state,
modifier = modifier
.height(InputFieldHeight)
@@ -115,6 +115,7 @@ object Settings : DataStorePreferences(null) {
val gridView = boolPref("grid_view", false)
val showGalleryPages = boolPref("show_gallery_pages", true)
val qSSaveProgress = boolPref("qs_save_progress", true)
val security = boolPref("require_unlock", false)
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
var downloadScheme by stringOrNullPref("image_scheme", null)
@@ -146,7 +147,6 @@ object Settings : DataStorePreferences(null) {
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
var saveParseErrorBody by boolPref("save_parse_error_body", true)
var saveCrashLog by boolPref("save_crash_log", true)
var security by boolPref("require_unlock", false)
var removeImageFiles by boolPref("include_pic", true)
var needSignIn by boolPref("need_sign_in", true)
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
@@ -21,10 +21,7 @@ import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.util.bodyAsUtf8Text
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.FormBuilder
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.request.header
import io.ktor.client.request.prepareRequest
import io.ktor.client.request.setBody
@@ -96,8 +93,3 @@ inline fun HttpRequestBuilder.jsonBody(builder: JsonObjectBuilder.() -> Unit) {
val json = buildJsonObject(builder).toString()
setBody(TextContent(text = json, contentType = ContentType.Application.Json))
}
fun HttpRequestBuilder.multipartBody(builder: FormBuilder.() -> Unit) {
method = HttpMethod.Post
setBody(MultiPartFormDataContent(formData(builder)))
}
@@ -25,7 +25,6 @@ import androidx.core.graphics.ColorUtils
import arrow.core.memoize
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.GalleryInfo
import java.util.regex.Pattern
import kotlin.math.abs
object EhUtils {
@@ -58,14 +57,14 @@ object EhUtils {
private const val BG_COLOR_UNKNOWN = 0xff000000u
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
private val PATTERN_TITLE_PREFIX = Pattern.compile(
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*",
private val PATTERN_TITLE_PREFIX = Regex(
"""^(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*""",
)
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
Pattern.CASE_INSENSITIVE,
private val PATTERN_TITLE_SUFFIX = Regex(
"""(?:\s+ch.[\s\d-]+)?(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*$""",
RegexOption.IGNORE_CASE,
)
private val CATEGORY_VALUES = hashMapOf(
@@ -170,16 +169,12 @@ object EhUtils {
fun extractTitle(fullTitle: String?): String? {
var title: String = fullTitle ?: return null
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("")
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("")
title = PATTERN_TITLE_PREFIX.replaceFirst(title, "")
title = PATTERN_TITLE_SUFFIX.replaceFirst(title, "")
// Sometimes title is combined by romaji and english translation.
// Only need romaji.
// TODO But not sure every '|' means that
val index = title.indexOf('|')
if (index >= 0) {
title = title.substring(0, index)
}
return title.ifEmpty { null }
return title.substringBeforeLast('|').trim().ifEmpty { null }
}
fun handleThumbUrlResolution(url: String?): String? {
@@ -15,7 +15,6 @@
*/
package com.hippo.ehviewer.client.data
import java.util.regex.Pattern
import moe.tarsin.kt.unreachable
interface GalleryInfo {
@@ -62,36 +61,36 @@ interface GalleryInfo {
"NL",
)
private val S_LANG_PATTERNS = arrayOf(
Pattern.compile(
Regex(
"[(\\[]eng(?:lish)?[)\\]]|英訳",
Pattern.CASE_INSENSITIVE,
RegexOption.IGNORE_CASE,
),
// [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
Pattern.compile(
Regex(
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
Pattern.CASE_INSENSITIVE,
RegexOption.IGNORE_CASE,
),
Pattern.compile(
Regex(
"[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]|スペイン翻訳",
Pattern.CASE_INSENSITIVE,
RegexOption.IGNORE_CASE,
),
Pattern.compile("[(\\[]korean?[)\\]]|韓国翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]portuguese|ポルトガル翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile(
Regex("[(\\[]korean?[)\\]]|韓国翻訳", RegexOption.IGNORE_CASE),
Regex("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", RegexOption.IGNORE_CASE),
Regex("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", RegexOption.IGNORE_CASE),
Regex("[(\\[]portuguese|ポルトガル翻訳", RegexOption.IGNORE_CASE),
Regex(
"[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย|タイ翻訳",
Pattern.CASE_INSENSITIVE,
RegexOption.IGNORE_CASE,
),
Pattern.compile("[(\\[]german[)\\]]|ドイツ翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]italiano?[)\\]]|イタリア翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile(
Regex("[(\\[]german[)\\]]|ドイツ翻訳", RegexOption.IGNORE_CASE),
Regex("[(\\[]italiano?[)\\]]|イタリア翻訳", RegexOption.IGNORE_CASE),
Regex(
"[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]|ベトナム翻訳",
Pattern.CASE_INSENSITIVE,
RegexOption.IGNORE_CASE,
),
Pattern.compile("[(\\[]polish[)\\]]|ポーランド翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
Regex("[(\\[]polish[)\\]]|ポーランド翻訳", RegexOption.IGNORE_CASE),
Regex("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", RegexOption.IGNORE_CASE),
Regex("[(\\[]dutch[)\\]]|オランダ翻訳", RegexOption.IGNORE_CASE),
)
val S_LANG_TAGS = arrayOf(
"language:english",
@@ -123,7 +122,7 @@ interface GalleryInfo {
private fun generateSLangFromTitle(title: String): String? {
for (i in S_LANGS.indices) {
if (S_LANG_PATTERNS[i].matcher(title).find()) {
if (S_LANG_PATTERNS[i].matches(title)) {
return S_LANGS[i]
}
}
@@ -16,27 +16,21 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.util.toLongOrDefault
import java.util.regex.Pattern
/**
* Like http://exhentai.org/g/1234567/a1b2c3d4e5<br></br>
*/
object GalleryDetailUrlParser {
private val URL_STRICT_PATTERN = Pattern.compile(
private val URL_STRICT_PATTERN = Regex(
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/(?:g|mpv)/(\\d+)/([0-9a-f]{10})",
)
private val URL_PATTERN = Pattern.compile("(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)")
private val URL_PATTERN = Regex("(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)")
fun parse(url: String?, strict: Boolean = true): Result? {
url ?: return null
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
val m = pattern.matcher(url)
return if (m.find()) {
val gid = m.group(1)!!.toLongOrDefault(0).takeIf { it > 0 }
gid?.let { Result(it, m.group(2)!!) }
} else {
null
return pattern.find(url)?.destructured?.let { (gid, token) ->
Result(gid.toLong(), token)
}
}
@@ -16,34 +16,21 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.util.toIntOrDefault
import com.hippo.ehviewer.util.toLongOrDefault
import java.util.regex.Pattern
/**
* Like http://exhentai.org/s/91ea4b6d89/901103-12
*/
object GalleryPageUrlParser {
private val URL_STRICT_PATTERN = Pattern.compile(
private val URL_STRICT_PATTERN = Regex(
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/s/([0-9a-f]{10})/(\\d+)-(\\d+)",
)
private val URL_PATTERN = Pattern.compile("([0-9a-f]{10})/(\\d+)-(\\d+)")
private val URL_PATTERN = Regex("([0-9a-f]{10})/(\\d+)-(\\d+)")
fun parse(url: String?, strict: Boolean = true): Result? {
url ?: return null
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
val m = pattern.matcher(url)
return if (m.find()) {
val gid = m.group(2)!!.toLongOrDefault(-1L)
val pToken = m.group(1)!!
val page = m.group(3)!!.toIntOrDefault(0) - 1
if (gid < 0 || page < 0) {
null
} else {
Result(gid, pToken, page)
}
} else {
null
return pattern.find(url)?.destructured?.let { (pToken, gid, page) ->
Result(gid.toLong(), pToken, page.toInt() - 1)
}
}
@@ -94,12 +94,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
if (label == null) {
return false
}
for ((label1) in labelList) {
if (label == label1) {
return true
}
}
return false
return labelList.any { it.label == label }
}
fun containDownloadInfo(gid: Long) = mAllInfoMap.containsKey(gid)
@@ -518,12 +513,14 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
suspend fun deleteLabel(label: String) {
labelList.run {
with(labelList) {
val index = indexOfFirst { it.label == label }
val item = get(index)
EhDB.removeDownloadLabel(item)
subList(index + 1, size).forEach {
it.position--
}
EhDB.removeDownloadLabel(removeAt(index))
removeAt(index)
}
allInfoList.forEach {
if (it.label == label) {
@@ -26,33 +26,27 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.job
import kotlinx.coroutines.suspendCancellableCoroutine
import org.chromium.net.CronetException
import org.chromium.net.UploadDataProvider
import org.chromium.net.UploadDataSink
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
import org.chromium.net.apihelpers.UploadDataProviders
object CronetEngine : HttpClientEngineBase("Cronet") {
override val config = HttpClientEngineConfig()
override val dispatcher = cronetDispatcher
@InternalAPI
override suspend fun execute(data: HttpRequestData): HttpResponseData {
val callContext = callContext()
return executeHttpRequest(callContext, data)
}
override suspend fun execute(data: HttpRequestData) = executeHttpRequest(callContext(), data)
@OptIn(DelicateCoroutinesApi::class)
private suspend fun executeHttpRequest(
callContext: CoroutineContext,
data: HttpRequestData,
): HttpResponseData = suspendCancellableCoroutine { continuation ->
) = suspendCancellableCoroutine { continuation ->
val requestTime = GMTDate()
val callback = object : UrlRequest.Callback() {
@@ -67,23 +61,22 @@ object CronetEngine : HttpClientEngineBase("Cronet") {
}
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
val channel = GlobalScope.writer(callContext) {
pool.useInstance {
chunkChan = Channel(1)
request.read(it)
chunkChan.consumeEach { buffer ->
buffer.flip()
channel.writeFully(buffer)
buffer.clear()
request.read(buffer)
}
}
}.channel
continuation.resume(
info.toHttpResponseData(
requestTime = requestTime,
callContext = callContext,
responseBody = channel,
responseBody = GlobalScope.writer(callContext) {
pool.useInstance {
chunkChan = Channel()
request.read(it)
chunkChan.consumeEach { buffer ->
buffer.flip()
channel.writeFully(buffer)
buffer.clear()
request.read(buffer)
}
}
}.channel,
),
)
}
@@ -97,24 +90,24 @@ object CronetEngine : HttpClientEngineBase("Cronet") {
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
if (::chunkChan.isInitialized) {
chunkChan.close(error)
} else {
if (continuation.isActive) {
continuation.resumeWithException(error)
} else {
chunkChan.close(error)
}
}
}
val request = cronetHttpClient.newUrlRequestBuilder(data.url.toString(), callback, cronetHttpClientExecutor).apply {
cronetHttpClient.newUrlRequestBuilder(data.url.toString(), callback, cronetHttpClientExecutor).apply {
setHttpMethod(data.method.value)
data.headers.flattenForEach { key, value -> addHeader(key, value) }
data.body.contentType?.let { addHeader(HttpHeaders.ContentType, it.toString()) }
data.body.contentLength?.let { addHeader(HttpHeaders.ContentLength, it.toString()) }
data.body.toUploadDataProvider(callContext)?.let { setUploadDataProvider(it, cronetHttpClientExecutor) }
}.build()
request.start()
callContext[Job]!!.invokeOnCompletion { request.cancel() }
continuation.invokeOnCancellation { request.cancel() }
data.body.toUploadDataProvider()?.let { setUploadDataProvider(it, cronetHttpClientExecutor) }
}.build().apply {
start()
callContext.job.invokeOnCompletion { cancel() }
}
}
}
@@ -122,55 +115,26 @@ private fun UrlResponseInfo.toHttpResponseData(
requestTime: GMTDate,
callContext: CoroutineContext,
responseBody: ByteReadChannel = ByteReadChannel.Empty,
): HttpResponseData {
return HttpResponseData(
statusCode = HttpStatusCode.fromValue(httpStatusCode),
requestTime = requestTime,
headers = headers {
allHeaders.forEach { (key, value) ->
appendAll(key, value)
}
},
version = when (negotiatedProtocol) {
"h2" -> HttpProtocolVersion.HTTP_2_0
"h3" -> HttpProtocolVersion.QUIC
"quic/1+spdy/3" -> HttpProtocolVersion.SPDY_3
else -> HttpProtocolVersion.HTTP_1_1
},
body = responseBody,
callContext = callContext,
)
}
) = HttpResponseData(
statusCode = HttpStatusCode.fromValue(httpStatusCode),
requestTime = requestTime,
headers = headers {
allHeaders.forEach { (key, value) ->
appendAll(key, value)
}
},
version = when (negotiatedProtocol) {
"h2" -> HttpProtocolVersion.HTTP_2_0
"h3" -> HttpProtocolVersion.QUIC
"quic/1+spdy/3" -> HttpProtocolVersion.SPDY_3
else -> HttpProtocolVersion.HTTP_1_1
},
body = responseBody,
callContext = callContext,
)
@OptIn(DelicateCoroutinesApi::class)
private fun OutgoingContent.toUploadDataProvider(callContext: CoroutineContext): UploadDataProvider? = when (this) {
private fun OutgoingContent.toUploadDataProvider() = when (this) {
is OutgoingContent.NoContent -> null
is OutgoingContent.ByteArrayContent -> object : UploadDataProvider() {
val buffer = ByteBuffer.wrap(bytes()).slice()
override fun getLength() = buffer.limit().toLong()
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
byteBuffer.put(buffer)
uploadDataSink.onReadSucceeded(false)
}
override fun rewind(uploadDataSink: UploadDataSink) {
buffer.position(0)
uploadDataSink.onRewindSucceeded()
}
}
is OutgoingContent.WriteChannelContent -> object : UploadDataProvider() {
fun launch() = GlobalScope.writer(callContext) { writeTo(channel) }.channel
var chan = launch()
override fun getLength() = contentLength!!
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
GlobalScope.launch(callContext) {
chan.readAvailable(byteBuffer)
uploadDataSink.onReadSucceeded(false)
}
}
override fun rewind(uploadDataSink: UploadDataSink) {
chan = launch()
uploadDataSink.onRewindSucceeded()
}
}
is OutgoingContent.ByteArrayContent -> UploadDataProviders.create(bytes())
else -> error("UnsupportedContentType $this")
}
@@ -128,7 +128,7 @@ fun Context.interceptSecurityOrReturn() {
val lockedDelayTime = lockedResumeTime - lockedLastLeaveTime
if (lockedDelayTime < Settings.securityDelay * 60) {
locked = false
} else if (Settings.security && isAuthenticationSupported() && locked) {
} else if (Settings.security.value && isAuthenticationSupported() && locked) {
startActivity(Intent(this, SecurityActivity::class.java))
}
}
@@ -2,7 +2,10 @@ package com.hippo.ehviewer.ui.reader
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Brightness5
import androidx.compose.material3.Icon
@@ -25,7 +28,7 @@ import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
@Composable
fun ColorFilterSetting() = Column {
fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
val customBrightness = Settings.customBrightness.asMutableState()
SwitchChoice(
title = stringResource(id = R.string.pref_custom_brightness),
@@ -2,8 +2,12 @@ package com.hippo.ehviewer.ui.reader
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import com.hippo.ehviewer.R
@@ -11,7 +15,7 @@ import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
@Composable
fun ReaderGeneralSetting() = Column {
fun ReaderGeneralSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
SpinnerChoice(
title = stringResource(id = R.string.pref_reader_theme),
entries = stringArrayResource(id = R.array.reader_themes),
@@ -4,8 +4,11 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -21,7 +24,7 @@ import com.hippo.ehviewer.collectAsState
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@Composable
fun ReaderModeSetting() = Column {
fun ReaderModeSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
SpinnerChoice(
title = stringResource(id = R.string.pref_category_reading_mode),
entries = stringArrayResource(id = R.array.viewers_selector),
@@ -4,10 +4,10 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.BottomSheetDefaults
@@ -112,7 +112,7 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
ModalBottomSheet(
onDismissRequest = { dispose() },
sheetState = state,
windowInsets = WindowInsets(0),
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
) {
ReaderPageSheetMeta(
retry = { pageLoader.retryPage(page.index) },
@@ -123,7 +123,6 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
saveTo = { launchIO { with(pageLoader) { saveTo(page) } } },
dismiss = { launch { state.hide().also { dispose() } } },
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -183,12 +182,12 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
// Yeah, I know color state should not be read here, but we have to do it...
scrimColor = scrim,
dragHandle = null,
windowInsets = WindowInsets(0),
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
) {
SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
isColorFilter = page == 2
appbarVisible = !isColorFilter
}
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -46,11 +46,13 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SwipeToDismissBoxDefaults
import androidx.compose.material3.SwipeToDismissBoxState
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -61,6 +63,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
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.geometry.Offset
@@ -142,6 +145,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
val activity = remember { findActivity<MainActivity>() }
val density = LocalDensity.current
val view = LocalView.current
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
val allName = stringResource(R.string.download_all)
val defaultName = stringResource(R.string.default_download_label_name)
val title = stringResource(
@@ -261,21 +265,28 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
},
)
}
itemsIndexed(labelList, key = { _, item -> item.label }) { index, (item) ->
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = {
itemsIndexed(labelList, key = { _, item -> item.id!! }) { index, (item) ->
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
LaunchedEffect(dismissState) {
snapshotFlow { dismissState.currentValue }.collect {
if (it == SwipeToDismissBoxValue.EndToStart) {
launch {
runCatching {
awaitPermissionOrCancel(confirmText = R.string.delete) {
Text(text = stringResource(R.string.delete_label, item))
}
}.onSuccess {
DownloadManager.deleteLabel(item)
when (filterState.label) {
item -> switchLabel("")
null -> invalidateKey = !invalidateKey
}
}.onFailure {
dismissState.reset()
}
}
true
},
)
}
}
ReorderableItem(reorderableLabelState, key = item) { isDragging ->
SwipeToDismissBox2(
state = dismissState,
@@ -230,6 +230,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
},
onSearchHidden = { fabHidden = false },
refreshState = refreshState,
tagNamespace = !isLocalFav,
searchBarOffsetY = { searchBarOffsetY },
trailingIcon = {
val sheetState = LocalSideSheetState.current
@@ -10,6 +10,7 @@ import android.os.Parcelable
import android.text.TextUtils.TruncateAt.END
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.annotation.StringRes
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
@@ -163,8 +164,6 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.parcelize.Parcelize
import moe.tarsin.coroutines.runSuspendCatching
import splitties.systemservices.downloadManager
@@ -506,32 +505,28 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
} else {
stringResource(id = R.string.not_favorited)
}
val favoritesLock = remember { Mutex() }
val favoritesLock = remember { MutatorMutex() }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val removeSucceed = stringResource(R.string.remove_from_favorite_success)
val addSucceed = stringResource(R.string.add_to_favorite_success)
val removeFailed = stringResource(R.string.remove_from_favorite_failure)
// val removeFailed = stringResource(R.string.remove_from_favorite_failure)
val addFailed = stringResource(R.string.add_to_favorite_failure)
FilledTertiaryIconToggleButton(
checked = favSlot != NOT_FAVORITED,
onCheckedChange = {
launchIO {
favoritesLock.withLock {
var remove = false
favoritesLock.mutate {
runSuspendCatching {
remove = !modifyFavorites(galleryDetail.galleryInfo)
}.onSuccess {
if (remove) {
showSnackbar(removeSucceed)
} else {
modifyFavorites(galleryDetail.galleryInfo)
}.onSuccess { add ->
if (add) {
showSnackbar(addSucceed)
} else {
showSnackbar(removeSucceed)
}
}.onFailure {
if (remove) {
showSnackbar(removeFailed)
} else {
showSnackbar(addFailed)
}
// TODO: We don't know if it's add or remove
showSnackbar(addFailed)
}
}
}
@@ -46,11 +46,12 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SwipeToDismissBoxDefaults
import androidx.compose.material3.SwipeToDismissBoxState
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
@@ -62,6 +63,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
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.geometry.Offset
@@ -201,6 +203,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
onBack = { showSearchLayout = false },
)
val density = LocalDensity.current
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
val listState = rememberLazyGridState()
val gridState = rememberLazyStaggeredGridState()
val isTopList = remember(urlBuilder) { urlBuilder.mode == MODE_TOPLIST }
@@ -385,21 +388,30 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
}
itemsIndexed(quickSearchList, key = { _, item -> item.id!! }) { index, item ->
ReorderableItem(reorderableLazyListState, key = item.id!!) { isDragging ->
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { value ->
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
LaunchedEffect(dismissState) {
snapshotFlow { dismissState.currentValue }.collect { value ->
if (value == SwipeToDismissBoxValue.EndToStart) {
with(quickSearchList) {
val removeIndex = indexOf(item)
subList(removeIndex + 1, size).forEach {
it.position--
runCatching {
awaitPermissionOrCancel(confirmText = R.string.delete) {
Text(text = stringResource(R.string.delete_quick_search, item.name))
}
removeAt(removeIndex)
}.onSuccess {
EhDB.deleteQuickSearch(item)
with(quickSearchList) {
val removeIndex = indexOf(item)
subList(removeIndex + 1, size).forEach {
it.position--
}
removeAt(removeIndex)
}
}.onFailure {
dismissState.reset()
}
launchIO { EhDB.deleteQuickSearch(item) }
}
true
},
)
}
}
SwipeToDismissBox2(
state = dismissState,
backgroundContent = {},
@@ -264,25 +264,30 @@ fun EhScreen(navigator: DestinationsNavigator) {
entryValueRes = R.array.launch_page_entry_values,
value = Settings::launchPage.observed,
)
val listMode = Settings.listMode.asMutableState()
SimpleMenuPreferenceInt(
title = stringResource(id = R.string.settings_eh_list_mode),
entry = R.array.list_mode_entries,
entryValueRes = R.array.list_mode_entry_values,
value = Settings.listMode.asMutableState(),
)
IntSliderPreference(
maxValue = 60,
minValue = 20,
step = 7,
title = stringResource(id = R.string.list_tile_thumb_size),
value = Settings.listThumbSize::value,
)
SimpleMenuPreferenceInt(
title = stringResource(id = R.string.settings_eh_detail_size),
entry = R.array.detail_size_entries,
entryValueRes = R.array.detail_size_entry_values,
value = Settings.detailSize.asMutableState(),
value = listMode,
)
AnimatedVisibility(visible = listMode.value == 0) {
Column {
IntSliderPreference(
maxValue = 60,
minValue = 20,
step = 7,
title = stringResource(id = R.string.list_tile_thumb_size),
value = Settings.listThumbSize::value,
)
SimpleMenuPreferenceInt(
title = stringResource(id = R.string.settings_eh_detail_size),
entry = R.array.detail_size_entries,
entryValueRes = R.array.detail_size_entry_values,
value = Settings.detailSize.asMutableState(),
)
}
}
IntSliderPreference(
maxValue = 10,
minValue = 1,
@@ -1,5 +1,6 @@
package com.hippo.ehviewer.ui.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
@@ -22,6 +23,7 @@ import androidx.compose.ui.res.stringResource
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.ui.isAuthenticationSupported
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.observed
@@ -54,20 +56,27 @@ fun PrivacyScreen(navigator: DestinationsNavigator) {
snackbarHost = { SnackbarHost(snackbarHostState) },
) {
Column(modifier = Modifier.padding(it).nestedScroll(scrollBehavior.nestedScrollConnection)) {
val security = Settings.security.asMutableState()
SwitchPreference(
title = stringResource(id = R.string.settings_privacy_require_unlock),
value = Settings::security,
enabled = LocalContext.current.isAuthenticationSupported(),
)
val securityDelay = Settings::securityDelay.observed
val summary = if (securityDelay.value == 0) stringResource(id = R.string.settings_privacy_require_unlock_delay_summary_immediately) else stringResource(id = R.string.settings_privacy_require_unlock_delay_summary, securityDelay.value)
IntSliderPreference(
maxValue = 30,
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
summary = summary,
value = securityDelay.rememberedAccessor,
value = security.rememberedAccessor,
enabled = LocalContext.current.isAuthenticationSupported(),
)
AnimatedVisibility(visible = security.value) {
val securityDelay = Settings::securityDelay.observed
val summary = if (securityDelay.value == 0) {
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary_immediately)
} else {
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary, securityDelay.value)
}
IntSliderPreference(
maxValue = 30,
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
summary = summary,
value = securityDelay.rememberedAccessor,
enabled = LocalContext.current.isAuthenticationSupported(),
)
}
SwitchPreference(
title = stringResource(id = R.string.settings_privacy_secure),
summary = stringResource(id = R.string.settings_privacy_secure_summary),
@@ -54,7 +54,7 @@ object AppUpdater {
return Release(shortSha, changelog, archiveUrl)
}
} else {
val curVersion = BuildConfig.VERSION_NAME
val curVersion = BuildConfig.RAW_VERSION_NAME
val release = ghStatement(LATEST_RELEASE_URL).executeAndParseAs<GithubRelease>()
val latestVersion = release.version
val description = release.info
@@ -17,6 +17,7 @@ package com.hippo.ehviewer.util
import android.os.Build
import android.os.Environment
import com.hippo.ehviewer.BuildConfig
import com.hippo.unifile.UniFile
import com.hippo.unifile.asUniFile
import java.io.File
@@ -31,10 +32,12 @@ object AppConfig {
private const val PARSE_ERROR = "parse_error"
private const val CRASH = "crash"
val abi = Build.SUPPORTED_ABIS[0].takeIf {
private val abi = Build.SUPPORTED_ABIS[0].takeIf {
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
} ?: "universal"
fun matchVariant(name: String) = name.contains(BuildConfig.FLAVOR) && name.contains(abi)
private val externalAppDir: File?
get() {
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
@@ -17,57 +17,37 @@ package com.hippo.ehviewer.util
import android.os.Looper
import eu.kanade.tachiyomi.util.system.logcat
import java.io.BufferedReader
import java.io.FileReader
import java.util.regex.Pattern
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
object OSUtils {
private const val PROCFS_MEMFILE = "/proc/meminfo"
private val PROCFS_MEMFILE_FORMAT = Pattern.compile("^([a-zA-Z]*):[ \t]*([0-9]*)[ \t]kB")
private val PROCFS_MEMFILE_FORMAT = Regex("^([a-zA-Z]*):[ \t]*([0-9]*)[ \t]kB")
private const val MEMTOTAL_STRING = "MemTotal"
private var sTotalMem = Long.MIN_VALUE
val appAllocatedMemory: Long
/**
* Get application allocated memory size
*/
get() = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
val appMaxMemory: Long
/**
* Get application max memory size
*/
get() = Runtime.getRuntime().maxMemory()
val totalMemory: Long
/**
* Get device RAM size
*/
get() {
if (sTotalMem == Long.MIN_VALUE) {
runCatching {
BufferedReader(FileReader(PROCFS_MEMFILE), 64).use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
val matcher = PROCFS_MEMFILE_FORMAT.matcher(line!!)
if (matcher.find() && MEMTOTAL_STRING == matcher.group(1)) {
var mem = matcher.group(2)!!.toLongOrDefault(-1L)
if (mem != -1L) {
mem *= 1024
}
sTotalMem = mem
break
}
val totalMemory by lazy {
runCatching {
FileReader(PROCFS_MEMFILE).buffered(64).use { reader ->
reader.lineSequence().forEach { line ->
PROCFS_MEMFILE_FORMAT.find(line)?.destructured?.let { (k, v) ->
if (k == MEMTOTAL_STRING) {
return@runCatching v.toLong() * 1024
}
}
}.onFailure {
logcat(it)
}
if (sTotalMem == Long.MIN_VALUE) {
sTotalMem = -1L
}
}
return sTotalMem
}
-1L
}.onFailure {
logcat(it)
}.getOrElse { -1L }
}
}
val isMainThread: Boolean
@@ -16,36 +16,24 @@
package com.hippo.ehviewer.util
import android.text.Spannable
import android.text.SpannableString
import android.text.style.URLSpan
import java.util.regex.Pattern
object TextUrl {
private val URL_PATTERN =
Pattern.compile("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+)+(:\\d{1,5})?(/[a-zA-Z0-9-_~:#@!&',;=%/*.?+$\\[\\]()]+)?/?")
Regex("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+)+(:\\d{1,5})?(/[a-zA-Z0-9-_~:#@!&',;=%/*.?+$\\[\\]()]+)?/?")
fun handleTextUrl(content: CharSequence): CharSequence {
val m = URL_PATTERN.matcher(content)
var spannable: Spannable? = null
while (m.find()) {
// Ensure spannable
if (spannable == null) {
spannable = if (content is Spannable) {
content
} else {
SpannableString(content)
}
}
val start = m.start()
val end = m.end()
val links = spannable.getSpans(start, end, URLSpan::class.java)
fun handleTextUrl(content: Spannable): Spannable {
URL_PATTERN.findAll(content).forEach { result ->
val start = result.range.first
val end = result.range.last + 1
val links = content.getSpans(start, end, URLSpan::class.java)
if (links.isNotEmpty()) {
// There has been URLSpan already, leave it alone
continue
return@forEach
}
val urlSpan = URLSpan(m.group(0))
spannable.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
val urlSpan = URLSpan(result.groupValues[0])
content.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return spannable ?: content
return content
}
}
@@ -43,10 +43,9 @@ import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@@ -319,13 +318,12 @@ class ReaderActivity : EhActivity() {
dragHandle = null,
// Yeah, I know color state should not be read here, but we have to do it...
scrimColor = scrim,
windowInsets = WindowInsets(0),
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
) {
SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
isColorFilter = page == 2
setMenuVisibility(!isColorFilter)
}
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -650,10 +648,9 @@ class ReaderActivity : EhActivity() {
dialogState.dialog {
ModalBottomSheet(
onDismissRequest = { it.cancel() },
windowInsets = WindowInsets(0),
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
) {
ReaderPageSheet(page) { it.cancel() }
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -2,12 +2,16 @@ package eu.kanade.tachiyomi.ui.reader
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileCopy
import androidx.compose.material.icons.filled.Refresh
@@ -47,12 +51,14 @@ fun ReaderPageSheetMeta(
Spacer(modifier = Modifier.size(32.dp))
Text(text = stringResource(id = text))
}
Item(icon = Icons.Default.Refresh, text = R.string.refresh, onClick = retry)
Item(icon = Icons.Default.Refresh, text = R.string.refresh_original, onClick = retryOrigin)
Item(icon = Icons.Default.Share, text = R.string.action_share, onClick = share)
Item(icon = Icons.Default.FileCopy, text = R.string.action_copy, onClick = copy)
Item(icon = Icons.Default.Save, text = R.string.action_save, onClick = save)
Item(icon = Icons.Default.Save, text = R.string.action_save_to, onClick = saveTo)
Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
Item(icon = Icons.Default.Refresh, text = R.string.refresh, onClick = retry)
Item(icon = Icons.Default.Refresh, text = R.string.refresh_original, onClick = retryOrigin)
Item(icon = Icons.Default.Share, text = R.string.action_share, onClick = share)
Item(icon = Icons.Default.FileCopy, text = R.string.action_copy, onClick = copy)
Item(icon = Icons.Default.Save, text = R.string.action_save, onClick = save)
Item(icon = Icons.Default.Save, text = R.string.action_save_to, onClick = saveTo)
}
}
@Composable
@@ -15,7 +15,7 @@ data class GithubRelease(
@SerialName("assets") val assets: List<GitHubAssets>,
) {
fun getDownloadLink(): String {
val asset = assets.find { it.downloadLink.contains(AppConfig.abi) } ?: assets[0]
val asset = assets.find { AppConfig.matchVariant(it.downloadLink) } ?: assets[0]
return asset.downloadLink
}
}
@@ -21,7 +21,10 @@ data class GithubArtifacts(
@SerialName("artifacts") val artifacts: List<GithubArtifact>,
) {
fun getDownloadLink(): String {
return (artifacts.find { it.name.contains(AppConfig.abi) } ?: artifacts[0]).downloadLink
// The default order is upload order, so we need to sort it
return artifacts.sortedBy { it.name }.run {
find { AppConfig.matchVariant(it.name) } ?: this[0]
}.downloadLink
}
}
@@ -85,6 +85,7 @@
<string name="duplicate_quick_search">已存在相同的快速搜索,名称为“%s”。</string>
<string name="duplicate_name">已存在相同名称</string>
<string name="save_progress">保存进度</string>
<string name="delete_quick_search">删除快速搜索“%s”?</string>
<string name="go_to_hint">第 %1$d 页,共 %2$d 页</string>
<!-- search_min_rating -->
<string name="any">不限</string>
@@ -245,6 +246,7 @@
<string name="label_text_is_invalid">“默认”是无效标签</string>
<string name="label_text_exist">标签已存在</string>
<string name="rename_label_title">重命名标签</string>
<string name="delete_label">删除标签“%s”?</string>
<!-- History -->
<string name="no_history">这里是阅读历史</string>
<string name="clear_all">全部清除</string>
@@ -85,6 +85,7 @@
<string name="duplicate_quick_search">已存在相同的快速搜尋,名稱為“%s”。</string>
<string name="duplicate_name">名稱重複</string>
<string name="save_progress">儲存進度</string>
<string name="delete_quick_search">刪除快速搜尋“%s”?</string>
<string name="go_to_hint">第 %1$d 頁,共 %2$d 頁</string>
<!-- search_min_rating -->
<string name="any">不限</string>
@@ -244,6 +245,7 @@
<string name="label_text_is_invalid">“預設”是無效的標籤</string>
<string name="label_text_exist">標籤已存在</string>
<string name="rename_label_title">重新命名標籤</string>
<string name="delete_label">刪除標籤“%s”?</string>
<!-- History -->
<string name="no_history">歷史紀錄將會顯示在這裡</string>
<string name="clear_all">全部清除</string>
+2
View File
@@ -125,6 +125,7 @@
<string name="duplicate_quick_search">A duplicate quick search exists. The name is \"%s\".</string>
<string name="duplicate_name">This name is already in use.</string>
<string name="save_progress">Save progress</string>
<string name="delete_quick_search">Delete quick search \"%s\"?</string>
<string name="go_to_hint">Page %1$d, total %2$d pages</string>
<!-- search_min_rating -->
<string name="any">Any</string>
@@ -287,6 +288,7 @@
<string name="label_text_is_invalid">\"Default\" is an invalid label</string>
<string name="label_text_exist">Label exists</string>
<string name="rename_label_title">Rename label</string>
<string name="delete_label">Delete label \"%s\"?</string>
<!-- History -->
<string name="no_history">Viewed galleries will be shown here</string>
<string name="clear_all">Clear all</string>
+10 -3
View File
@@ -96,9 +96,16 @@ fn parse_token_and_gid(str: &str) -> Option<(i64, String)> {
}
fn parse_uploader_and_pages(str: &str) -> (Option<String>, bool, i32) {
let uploader = regex!(r#"<a href="https://e[x-]hentai.org/uploader/.*?">(.*?)</a>"#)
.captures(str)
.map(|grp| grp[1].to_string());
let uploader =
regex!(r#"<a href="https://e[x-]hentai.org/uploader/.*?">(.*?)</a>|(\(Disowned\))"#)
.captures(str)
.map(|grp| {
grp.get(1)
.or_else(|| grp.get(2))
.unwrap()
.as_str()
.to_string()
});
let pages = match regex!(r"<div>(\d+) pages</div>").captures(str) {
None => 0,
Some(grp) => grp[1].parse().unwrap_or(0),
+5
View File
@@ -65,6 +65,11 @@
# 下载
| 变种 | 功能 |
|-------------|-----------------------|
| Default | Android 8.0+, 完全支持 |
| Marshmallow | Android 6.0-7.1, 有限支持 |
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
+5
View File
@@ -65,6 +65,11 @@
# 下載
| 變種 | 功能 |
|-------------|-----------------------|
| Default | Android 8.0+, 完全支援 |
| Marshmallow | Android 6.0-7.1, 有限支援 |
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
+1 -1
View File
@@ -26,7 +26,7 @@ kotlinx-datetime = "0.6.0-RC.2"
kotlinx-serialization = "1.6.3"
ksp = "2.0.0-Beta4-1.0.19"
ktor = "3.0.0-beta-1"
okio = "3.8.0"
okio = "3.9.0"
splitties = "3.0.0"
telephoto = "0.8.0"
xmlutil = "0.86.3"