Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a64ff51ed | ||
|
|
ea9d9cfba7 | ||
|
|
75c6561896 | ||
|
|
8f8db82e9d | ||
|
|
a114a8ac72 | ||
|
|
2c6216057f | ||
|
|
c5cbcf7418 | ||
|
|
5746e3bb57 | ||
|
|
fd3c87be22 | ||
|
|
866fff3e45 | ||
|
|
06df7befb6 | ||
|
|
8021f86473 | ||
|
|
c9919a8b39 | ||
|
|
386015a9ec | ||
|
|
cbce98b34e |
No files matched your search
+120
-18
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
### 重大变更
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user