Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf6aa3e8ae | ||
|
|
75fcfde892 | ||
|
|
89be27995e | ||
|
|
6738dc6cdc | ||
|
|
9ff0decb98 | ||
|
|
717d3717ba | ||
|
|
fdbca869a8 | ||
|
|
aafa750ca3 | ||
|
|
afe2bac672 | ||
|
|
b6f5d72db5 | ||
|
|
f0021a7601 | ||
|
|
ea5bfbb346 | ||
|
|
38fbcf4457 | ||
|
|
7c43e8f349 | ||
|
|
93df4e3dd9 | ||
|
|
f7d45a52a7 | ||
|
|
328f96d115 | ||
|
|
22338a72d8 | ||
|
|
256dedcb7c | ||
|
|
95d2438b84 | ||
|
|
f8d27e165e | ||
|
|
e2f024ae29 | ||
|
|
9c28fbeaf8 | ||
|
|
6458eb090a | ||
|
|
f1ca2b6958 | ||
|
|
60f031e7e3 | ||
|
|
e6a44ee0e1 | ||
|
|
7a3a641a07 | ||
|
|
1021efb239 | ||
|
|
edbe6509e1 | ||
|
|
44a6253bfc | ||
|
|
3d6e5df71e | ||
|
|
2733d2e58c | ||
|
|
0c78673870 | ||
|
|
d618948429 | ||
|
|
7584704d22 | ||
|
|
4a4658cc54 | ||
|
|
e9875f1a24 | ||
|
|
cc11257d80 | ||
|
|
4fd64601ef | ||
|
|
7eea490adf | ||
|
|
b2b12f7b93 | ||
|
|
66d026cffa | ||
|
|
5a15a20dd6 | ||
|
|
70450a9725 | ||
|
|
470a3c6a09 | ||
|
|
15f8b2f15d | ||
|
|
717cbe7630 | ||
|
|
d34075d29d | ||
|
|
80d4d66fb5 | ||
|
|
6b20b46761 | ||
|
|
cd764a4a3c | ||
|
|
5422b3d8bd | ||
|
|
0273390d79 | ||
|
|
8f7242b939 | ||
|
|
246dee63b4 | ||
|
|
697918a73a | ||
|
|
67e480220e | ||
|
|
5784eb008e | ||
|
|
82d66c4d57 | ||
|
|
ab8671cc3b | ||
|
|
2e52dda208 | ||
|
|
1ab08d0ae1 | ||
|
|
702be0ed43 | ||
|
|
4067543701 | ||
|
|
074689c60e | ||
|
|
2beb715df3 | ||
|
|
7ea2597899 | ||
|
|
34a8f92865 | ||
|
|
6e2a2de13f | ||
|
|
e073232352 | ||
|
|
6f49b4d23e | ||
|
|
97cad20ab1 | ||
|
|
9300a3d9ce | ||
|
|
28b7f80a8f | ||
|
|
a06d23753a |
No files matched your search
@@ -1,3 +1,4 @@
|
|||||||
[*.{kt,kts}]
|
[*.{kt,kts}]
|
||||||
|
ij_kotlin_imports_layout = *
|
||||||
ktlint_code_style = intellij_idea
|
ktlint_code_style = intellij_idea
|
||||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||||
@@ -64,7 +64,7 @@ body:
|
|||||||
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
|
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
|
||||||
placeholder: 06-15 17:44:53.704 23382 23382 E ...
|
placeholder: 06-15 17:44:53.704 23382 23382 E ...
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: more
|
id: more
|
||||||
@@ -79,6 +79,15 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: site
|
||||||
|
attributes:
|
||||||
|
label: 浏览站点 / Browsing site
|
||||||
|
description: E-Hentai / ExHentai
|
||||||
|
placeholder: E-Hentai
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Setup Java 21
|
- name: Setup Java 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 21
|
java-version: 17
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- 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
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
app/.cxx
|
app/.cxx
|
||||||
app/src/main/cpp/nettle/nettle
|
app/src/main/cpp/nettle/nettle
|
||||||
key: ${{ runner.os }}-cmake-${{ github.run_id }}
|
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||||
restore-keys: ${{ runner.os }}-cmake-
|
restore-keys: ${{ runner.os }}-cmake-
|
||||||
|
|
||||||
- name: Build OSS
|
- name: Build OSS
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Setup Java 21
|
- name: Setup Java 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 21
|
java-version: 17
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
app/.cxx
|
app/.cxx
|
||||||
app/src/main/cpp/nettle/nettle
|
app/src/main/cpp/nettle/nettle
|
||||||
key: ${{ runner.os }}-cmake-${{ github.run_id }}
|
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||||
restore-keys: ${{ runner.os }}-cmake-
|
restore-keys: ${{ runner.os }}-cmake-
|
||||||
|
|
||||||
- name: Gradle Build
|
- name: Gradle Build
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
|
|||||||
| Flavor | Feature |
|
| Flavor | Feature |
|
||||||
|-|-|
|
|-|-|
|
||||||
| Default | Android 9+, full support |
|
| Default | Android 9+, full support |
|
||||||
| Marshmallow | Android 6.0+, limited support* |
|
| Marshmallow | Android 6.0-8.1, limited support* |
|
||||||
| OSS | FOSS dependencies** |
|
| OSS | FOSS dependencies** |
|
||||||
| GMS | Proprietary GMS component |
|
| GMS | Proprietary GMS component |
|
||||||
|
|
||||||
|
|||||||
+3
-15
@@ -64,8 +64,8 @@ android {
|
|||||||
applicationId = "moe.tarsin.ehviewer"
|
applicationId = "moe.tarsin.ehviewer"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 180044
|
versionCode = 180045
|
||||||
versionName = "1.8.10.1"
|
versionName = "1.8.11.0"
|
||||||
resourceConfigurations.addAll(
|
resourceConfigurations.addAll(
|
||||||
listOf(
|
listOf(
|
||||||
"zh",
|
"zh",
|
||||||
@@ -231,7 +231,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.androidx.swiperefreshlayout)
|
implementation(libs.androidx.swiperefreshlayout)
|
||||||
implementation(libs.androidx.work.runtime)
|
implementation(libs.androidx.work.runtime)
|
||||||
implementation(libs.drawer)
|
|
||||||
implementation(libs.photoview) // Dead Dependency
|
implementation(libs.photoview) // Dead Dependency
|
||||||
implementation(libs.directionalviewpager) // Dead Dependency
|
implementation(libs.directionalviewpager) // Dead Dependency
|
||||||
// https://github.com/google/accompanist/releases
|
// https://github.com/google/accompanist/releases
|
||||||
@@ -249,7 +248,6 @@ dependencies {
|
|||||||
implementation(libs.aboutlibraries.core)
|
implementation(libs.aboutlibraries.core)
|
||||||
|
|
||||||
implementation(libs.insetter) // Dead Dependency
|
implementation(libs.insetter) // Dead Dependency
|
||||||
implementation(libs.bundles.rikkax)
|
|
||||||
|
|
||||||
implementation(platform(libs.arrow.stack))
|
implementation(platform(libs.arrow.stack))
|
||||||
implementation(libs.arrow.fx.coroutines)
|
implementation(libs.arrow.fx.coroutines)
|
||||||
@@ -276,7 +274,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(21)
|
jvmToolchain(17)
|
||||||
}
|
}
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
@@ -287,16 +285,6 @@ ksp {
|
|||||||
aboutLibraries {
|
aboutLibraries {
|
||||||
duplicationMode = MERGE
|
duplicationMode = MERGE
|
||||||
duplicationRule = GROUP
|
duplicationRule = GROUP
|
||||||
exclusionPatterns = listOf(
|
|
||||||
"androidx\\..*".toPattern(),
|
|
||||||
".*annotations".toPattern(),
|
|
||||||
"com\\.google\\.code\\..*".toPattern(),
|
|
||||||
"com\\.google\\.guava:listenablefuture".toPattern(),
|
|
||||||
"com\\.google\\.protobuf:protobuf-javalite".toPattern(),
|
|
||||||
"org\\.checkerframework:checker-qual".toPattern(),
|
|
||||||
"org\\.chromium\\.net:cronet-embedded".toPattern(),
|
|
||||||
"org\\.slf4j:slf4j-api".toPattern(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cargo {
|
cargo {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.hippo.ehviewer.cronet
|
package com.hippo.ehviewer.cronet
|
||||||
|
|
||||||
import com.hippo.ehviewer.EhApplication
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
||||||
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
||||||
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import java.nio.ByteBuffer
|
||||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -17,28 +20,15 @@ import org.chromium.net.CronetException
|
|||||||
import org.chromium.net.UrlRequest
|
import org.chromium.net.UrlRequest
|
||||||
import org.chromium.net.UrlResponseInfo
|
import org.chromium.net.UrlResponseInfo
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.channels.FileChannel
|
|
||||||
import kotlin.contracts.InvocationKind
|
|
||||||
import kotlin.contracts.contract
|
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
|
|
||||||
private const val TAG = "CronetRequest"
|
|
||||||
val pool = DirectByteBufferPool(32)
|
|
||||||
|
|
||||||
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
|
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
|
||||||
enableBrotli(true)
|
enableBrotli(true)
|
||||||
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
|
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
|
||||||
setStoragePath(cache.absolutePath)
|
setStoragePath(cache.absolutePath)
|
||||||
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
|
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
|
||||||
setUserAgent(CHROME_USER_AGENT)
|
setUserAgent(Settings.userAgent)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService
|
|
||||||
|
|
||||||
// TODO: Rewrite this to use android.net.http.HttpEngine and make it Android 14 only when released
|
|
||||||
class CronetRequest {
|
class CronetRequest {
|
||||||
lateinit var consumer: (ByteBuffer) -> Unit
|
lateinit var consumer: (ByteBuffer) -> Unit
|
||||||
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
|
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
|
||||||
@@ -47,7 +37,6 @@ class CronetRequest {
|
|||||||
lateinit var readerCont: Continuation<Unit>
|
lateinit var readerCont: Continuation<Unit>
|
||||||
val callback = object : UrlRequest.Callback() {
|
val callback = object : UrlRequest.Callback() {
|
||||||
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
|
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
|
||||||
logcat(tag = TAG) { "Redirected to $url" }
|
|
||||||
req.followRedirect()
|
req.followRedirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,29 +68,6 @@ inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.
|
|||||||
}.apply(conf).build()
|
}.apply(conf).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun CronetRequest.awaitBodyFully(crossinline callback: (ByteBuffer) -> Unit) {
|
|
||||||
val buffer = pool.borrow()
|
|
||||||
return try {
|
|
||||||
suspendCancellableCoroutine { cont ->
|
|
||||||
consumer = {
|
|
||||||
callback(it)
|
|
||||||
buffer.clear()
|
|
||||||
request.read(buffer)
|
|
||||||
}
|
|
||||||
onError = { readerCont.resumeWithException(it) }
|
|
||||||
readerCont = cont
|
|
||||||
request.read(buffer)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
pool.recycle(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun CronetRequest.copyToChannel(chan: FileChannel, crossinline listener: ((Int) -> Unit) = {}) = awaitBodyFully {
|
|
||||||
val bytes = chan.write(it)
|
|
||||||
listener(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <R> CronetRequest.execute(crossinline callback: suspend CronetRequest.(UrlResponseInfo) -> R): R {
|
suspend inline fun <R> CronetRequest.execute(crossinline callback: suspend CronetRequest.(UrlResponseInfo) -> R): R {
|
||||||
contract {
|
contract {
|
||||||
callsInPlace(callback, InvocationKind.EXACTLY_ONCE)
|
callsInPlace(callback, InvocationKind.EXACTLY_ONCE)
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package com.hippo.ehviewer.util
|
|||||||
|
|
||||||
import com.google.android.gms.net.CronetProviderInstaller
|
import com.google.android.gms.net.CronetProviderInstaller
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
|
import org.chromium.net.CronetProvider
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
val isCronetSupported: Boolean
|
val isCronetSupported: Boolean
|
||||||
get() = Settings.enableCronet && CronetProviderInstaller.isInstalled().also {
|
get() = Settings.enableCronet && CronetProvider.getAllProviders(appCtx).any { it.isEnabled }.also {
|
||||||
if (!it) {
|
if (!it) {
|
||||||
CronetProviderInstaller.installProvider(appCtx)
|
CronetProviderInstaller.installProvider(appCtx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package androidx.navigation.ui
|
package androidx.navigation.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.navigation.ActivityNavigator
|
import androidx.navigation.ActivityNavigator
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -10,9 +9,10 @@ import androidx.navigation.NavDestination.Companion.hierarchy
|
|||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
|
|
||||||
@SuppressLint("PrivateResource")
|
@SuppressLint("PrivateResource")
|
||||||
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
|
fun onNavDestinationSelected2(@IdRes id: Int, navController: NavController): Boolean {
|
||||||
|
if (navController.currentDestination?.matchDestination(id) == true) return true
|
||||||
val builder = NavOptions.Builder().setLaunchSingleTop(true)
|
val builder = NavOptions.Builder().setLaunchSingleTop(true)
|
||||||
if (navController.currentDestination!!.parent!!.findNode(item.itemId) is ActivityNavigator.Destination) {
|
if (navController.currentDestination!!.parent!!.findNode(id) is ActivityNavigator.Destination) {
|
||||||
builder.setEnterAnim(R.anim.nav_default_enter_anim)
|
builder.setEnterAnim(R.anim.nav_default_enter_anim)
|
||||||
.setExitAnim(R.anim.nav_default_exit_anim)
|
.setExitAnim(R.anim.nav_default_exit_anim)
|
||||||
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
|
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
|
||||||
@@ -26,9 +26,9 @@ fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boo
|
|||||||
val options = builder.build()
|
val options = builder.build()
|
||||||
return try {
|
return try {
|
||||||
// TODO provide proper API instead of using Exceptions as Control-Flow.
|
// TODO provide proper API instead of using Exceptions as Control-Flow.
|
||||||
navController.navigate(item.itemId, null, options)
|
navController.navigate(id, null, options)
|
||||||
// Return true only if the destination we've navigated to matches the MenuItem
|
// Return true only if the destination we've navigated to matches the MenuItem
|
||||||
navController.currentDestination?.matchDestination(item.itemId) == true
|
navController.currentDestination?.matchDestination(id) == true
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -23,14 +23,6 @@ import com.davemorrissey.labs.subscaleview.decoder.Decoder
|
|||||||
import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder
|
import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder
|
||||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.locks.ReadWriteLock
|
import java.util.concurrent.locks.ReadWriteLock
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||||
@@ -39,6 +31,14 @@ import kotlin.math.max
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays an image subsampled as necessary to avoid loading too much image data into memory. After zooming in,
|
* Displays an image subsampled as necessary to avoid loading too much image data into memory. After zooming in,
|
||||||
@@ -601,8 +601,8 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
|
|
||||||
private fun onTouchEventInternal(event: MotionEvent): Boolean {
|
private fun onTouchEventInternal(event: MotionEvent): Boolean {
|
||||||
val touchCount = event.pointerCount
|
val touchCount = event.pointerCount
|
||||||
when (event.action) {
|
when (event.actionMasked) {
|
||||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_1_DOWN, MotionEvent.ACTION_POINTER_2_DOWN -> {
|
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
|
||||||
anim = null
|
anim = null
|
||||||
requestDisallowInterceptTouchEvent(true)
|
requestDisallowInterceptTouchEvent(true)
|
||||||
maxTouchCount = maxTouchCount.coerceAtLeast(touchCount)
|
maxTouchCount = maxTouchCount.coerceAtLeast(touchCount)
|
||||||
@@ -773,7 +773,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_2_UP -> {
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
|
||||||
longClickJob?.cancel()
|
longClickJob?.cancel()
|
||||||
if (isQuickScaling) {
|
if (isQuickScaling) {
|
||||||
isQuickScaling = false
|
isQuickScaling = false
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import com.hippo.ehviewer.util.isAtLeastP
|
|||||||
import com.hippo.ehviewer.util.isAtLeastQ
|
import com.hippo.ehviewer.util.isAtLeastQ
|
||||||
import com.hippo.ehviewer.util.isCronetSupported
|
import com.hippo.ehviewer.util.isCronetSupported
|
||||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.kt.unreachable
|
import moe.tarsin.kt.unreachable
|
||||||
@@ -91,7 +92,7 @@ class EhApplication : Application(), ImageLoaderFactory {
|
|||||||
migrateCookies()
|
migrateCookies()
|
||||||
}
|
}
|
||||||
launchIO {
|
launchIO {
|
||||||
EhTagDatabase.update()
|
EhTagDatabase
|
||||||
}
|
}
|
||||||
launchIO {
|
launchIO {
|
||||||
EhDB
|
EhDB
|
||||||
@@ -170,6 +171,7 @@ class EhApplication : Application(), ImageLoaderFactory {
|
|||||||
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
|
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
|
||||||
}
|
}
|
||||||
chunker { alwaysReadResponseBody(false) }
|
chunker { alwaysReadResponseBody(false) }
|
||||||
|
addInterceptor(UncaughtExceptionInterceptor())
|
||||||
addInterceptor(CloudflareInterceptor(appCtx))
|
addInterceptor(CloudflareInterceptor(appCtx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
package com.hippo.ehviewer
|
package com.hippo.ehviewer
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
@@ -12,8 +15,6 @@ import kotlinx.coroutines.flow.debounce
|
|||||||
import kotlinx.coroutines.flow.flatMapMerge
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
import splitties.preferences.DataStorePreferences
|
import splitties.preferences.DataStorePreferences
|
||||||
import splitties.preferences.edit
|
import splitties.preferences.edit
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
object Settings : DataStorePreferences(null) {
|
object Settings : DataStorePreferences(null) {
|
||||||
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
|
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
|
||||||
@@ -24,6 +25,7 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var favCount by intArrayPref("fav_count", 10).emitTo(_favFlow)
|
var favCount by intArrayPref("fav_count", 10).emitTo(_favFlow)
|
||||||
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
|
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
|
||||||
|
|
||||||
|
val needSignInFlow: Flow<Boolean>
|
||||||
var downloadScheme by stringOrNullPref("image_scheme", null)
|
var downloadScheme by stringOrNullPref("image_scheme", null)
|
||||||
var downloadAuthority by stringOrNullPref("image_authority", null)
|
var downloadAuthority by stringOrNullPref("image_authority", null)
|
||||||
var downloadPath by stringOrNullPref("image_path", null)
|
var downloadPath by stringOrNullPref("image_path", null)
|
||||||
@@ -41,6 +43,7 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var thumbResolution by intPref("thumb_resolution_2", 0)
|
var thumbResolution by intPref("thumb_resolution_2", 0)
|
||||||
var readCacheSize by intPref("read_cache_size_2", 640)
|
var readCacheSize by intPref("read_cache_size_2", 640)
|
||||||
var launchPage by intPref("launch_page_2", 0)
|
var launchPage by intPref("launch_page_2", 0)
|
||||||
|
var commentThreshold by intPref("comment_threshold", -101)
|
||||||
var forceEhThumb by boolPref("force_eh_thumb", false)
|
var forceEhThumb by boolPref("force_eh_thumb", false)
|
||||||
var showComments by boolPref("show_gallery_comments", true)
|
var showComments by boolPref("show_gallery_comments", true)
|
||||||
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
|
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
|
||||||
@@ -59,7 +62,7 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var saveCrashLog by boolPref("save_crash_log", true)
|
var saveCrashLog by boolPref("save_crash_log", true)
|
||||||
var security by boolPref("require_unlock", false)
|
var security by boolPref("require_unlock", false)
|
||||||
var removeImageFiles by boolPref("include_pic", true)
|
var removeImageFiles by boolPref("include_pic", true)
|
||||||
var needSignIn by boolPref("need_sign_in", true)
|
var needSignIn by boolPref("need_sign_in", true).also { needSignInFlow = it.valueFlow() }
|
||||||
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
|
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
|
||||||
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
||||||
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
||||||
@@ -75,6 +78,8 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
|
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
|
||||||
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
|
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
|
||||||
var updateIntervalDays by intPref("update_interval_days", 0)
|
var updateIntervalDays by intPref("update_interval_days", 0)
|
||||||
|
var recentToplist by stringPref("recent_toplist", "11")
|
||||||
|
var userAgent by stringPref("user_agent", CHROME_USER_AGENT)
|
||||||
var recentDownloadLabel by stringOrNullPref("recent_download_label", null)
|
var recentDownloadLabel by stringOrNullPref("recent_download_label", null)
|
||||||
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
|
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
|
||||||
var displayName by stringOrNullPref("display_name", null)
|
var displayName by stringOrNullPref("display_name", null)
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
package com.hippo.ehviewer.client
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
import androidx.compose.ui.util.fastAny
|
||||||
|
import androidx.compose.ui.util.fastFilter
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.compose.ui.util.fastJoinToString
|
||||||
|
import androidx.compose.ui.util.fastMapNotNull
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
@@ -24,13 +29,15 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
|||||||
object EhCookieStore : CookieJar {
|
object EhCookieStore : CookieJar {
|
||||||
private val manager = CookieManager.getInstance()
|
private val manager = CookieManager.getInstance()
|
||||||
fun signOut() = manager.removeAllCookies(null)
|
fun signOut() = manager.removeAllCookies(null)
|
||||||
fun contains(url: HttpUrl, name: String) = get(url).any { it.name == name }
|
fun contains(url: HttpUrl, name: String) = get(url).fastAny { it.name == name }
|
||||||
|
|
||||||
fun get(url: HttpUrl): List<Cookie> {
|
fun get(url: HttpUrl): List<Cookie> {
|
||||||
val cookies = manager.getCookie(url.toString())
|
val cookies = manager.getCookie(url.toString())
|
||||||
|
|
||||||
return if (cookies != null && cookies.isNotEmpty()) {
|
return if (!cookies.isNullOrEmpty()) {
|
||||||
cookies.split(";").mapNotNull { Cookie.parse(url, it) }.filterNot { it.name == KEY_UTMP_NAME }
|
cookies.split(";").fastMapNotNull { setCookie ->
|
||||||
|
Cookie.parse(url, setCookie)?.takeUnless { it.name == KEY_UTMP_NAME }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
@@ -57,9 +64,12 @@ object EhCookieStore : CookieJar {
|
|||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
fun copyNecessaryCookies() {
|
fun copyNecessaryCookies() {
|
||||||
val cookie = get(EhUrl.HOST_E.toHttpUrl()).filter { it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH || it.name == KEY_IGNEOUS }
|
val cookies = get(EhUrl.HOST_E.toHttpUrl())
|
||||||
cookie.forEach { manager.setCookie(EhUrl.HOST_EX, it.toString()) }
|
cookies.fastForEach {
|
||||||
flush()
|
if (it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH) {
|
||||||
|
manager.setCookie(EhUrl.HOST_EX, it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteCookie(url: HttpUrl, name: String) {
|
fun deleteCookie(url: HttpUrl, name: String) {
|
||||||
@@ -67,35 +77,29 @@ object EhCookieStore : CookieJar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addCookie(cookie: Cookie) {
|
fun addCookie(cookie: Cookie) {
|
||||||
if (EhUrl.DOMAIN_E in cookie.domain) manager.setCookie(EhUrl.HOST_E, cookie.toString()) else manager.setCookie(EhUrl.DOMAIN_EX, cookie.toString())
|
manager.setCookie(if (EhUrl.DOMAIN_E == cookie.domain) EhUrl.HOST_E else EhUrl.HOST_EX, cookie.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flush() = manager.flush()
|
fun flush() = manager.flush()
|
||||||
|
|
||||||
fun getCookieHeader(url: HttpUrl): String {
|
fun getCookieHeader(url: HttpUrl): String {
|
||||||
val cookies = loadForRequest(url)
|
return loadForRequest(url).fastJoinToString("; ") {
|
||||||
val cookieHeader = StringBuilder()
|
"${it.name}=${it.value}"
|
||||||
var i = 0
|
|
||||||
val size = cookies.size
|
|
||||||
while (i < size) {
|
|
||||||
if (i > 0) {
|
|
||||||
cookieHeader.append("; ")
|
|
||||||
}
|
|
||||||
val cookie = cookies[i]
|
|
||||||
cookieHeader.append(cookie.name).append('=').append(cookie.value)
|
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
return cookieHeader.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.filterNot { it.name == KEY_UTMP_NAME }.forEach { manager.setCookie(url.toString(), it.toString()) }.also { flush() }
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.fastForEach {
|
||||||
|
if (it.name != KEY_UTMP_NAME) {
|
||||||
|
manager.setCookie(url.toString(), it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
val checkTips = EhUrl.DOMAIN_E in url.host
|
val checkTips = EhUrl.DOMAIN_E in url.host
|
||||||
return get(url).run {
|
return get(url).run {
|
||||||
if (checkTips) {
|
if (checkTips) {
|
||||||
filterNot { it.name == KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
|
fastFilter { it.name != KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
|
||||||
} else {
|
} else {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ import com.hippo.ehviewer.util.AppConfig
|
|||||||
import com.hippo.ehviewer.util.StatusCodeException
|
import com.hippo.ehviewer.util.StatusCodeException
|
||||||
import com.hippo.ehviewer.util.isCronetSupported
|
import com.hippo.ehviewer.util.isCronetSupported
|
||||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||||
|
import io.ktor.utils.io.pool.useInstance
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlin.math.ceil
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.serialization.json.add
|
import kotlinx.serialization.json.add
|
||||||
@@ -68,9 +72,6 @@ import okhttp3.Request
|
|||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
private const val TAG = "EhEngine"
|
private const val TAG = "EhEngine"
|
||||||
private const val MAX_REQUEST_SIZE = 25
|
private const val MAX_REQUEST_SIZE = 25
|
||||||
@@ -116,33 +117,27 @@ private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
val httpContentPool = DirectByteBufferPool(16, 16384)
|
val httpContentPool = DirectByteBufferPool(16, 0x20000)
|
||||||
|
|
||||||
suspend inline fun <T> fetchCompat(url: String, referer: String? = null, crossinline parser: (ByteBuffer) -> T): T {
|
suspend inline fun <T> fetchCompat(url: String, referer: String? = null, crossinline parser: (ByteBuffer) -> T): T {
|
||||||
return if (isCronetSupported) {
|
return if (isCronetSupported) {
|
||||||
cronetRequest(url, referer).execute {
|
cronetRequest(url, referer).execute {
|
||||||
val buffer = httpContentPool.borrow()
|
httpContentPool.useInstance { buffer ->
|
||||||
try {
|
|
||||||
awaitBodyFully {
|
awaitBodyFully {
|
||||||
buffer.put(it)
|
buffer.put(it)
|
||||||
}
|
}
|
||||||
buffer.flip()
|
buffer.flip()
|
||||||
parser(buffer)
|
parser(buffer)
|
||||||
} finally {
|
|
||||||
httpContentPool.recycle(buffer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ehRequest(url, referer).execute {
|
ehRequest(url, referer).execute {
|
||||||
val buffer = httpContentPool.borrow()
|
httpContentPool.useInstance { buffer ->
|
||||||
try {
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (body.source().read(buffer) == -1) break
|
if (body.source().read(buffer) == -1) break
|
||||||
}
|
}
|
||||||
buffer.flip()
|
buffer.flip()
|
||||||
parser(buffer)
|
parser(buffer)
|
||||||
} finally {
|
|
||||||
httpContentPool.recycle(buffer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -490,7 +485,7 @@ object EhEngine {
|
|||||||
var hasPages = false
|
var hasPages = false
|
||||||
var hasRated = false
|
var hasRated = false
|
||||||
for (gi in list) {
|
for (gi in list) {
|
||||||
if (gi.simpleTags != null) {
|
if (!gi.simpleTags.isNullOrEmpty()) {
|
||||||
hasTags = true
|
hasTags = true
|
||||||
}
|
}
|
||||||
if (gi.pages != 0) {
|
if (gi.pages != 0) {
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ object EhFilter : CoroutineScope {
|
|||||||
val filters = async { EhDB.getAllFilter() as MutableList }
|
val filters = async { EhDB.getAllFilter() as MutableList }
|
||||||
private suspend inline fun anyActive(mode: FilterMode, predicate: (Filter) -> Boolean) = filters.await().any { it.mode == mode && it.enable && predicate(it) }
|
private suspend inline fun anyActive(mode: FilterMode, predicate: (Filter) -> Boolean) = filters.await().any { it.mode == mode && it.enable && predicate(it) }
|
||||||
private fun <R> Filter.launchOps(
|
private fun <R> Filter.launchOps(
|
||||||
callback: ((R) -> Unit)? = null,
|
callback: (suspend (R) -> Unit)? = null,
|
||||||
ops: suspend Filter.() -> R,
|
ops: suspend Filter.() -> R,
|
||||||
) = launch { ops().let { callback?.invoke(it) } }
|
) = launch { ops().let { callback?.invoke(it) } }
|
||||||
fun Filter.remember(callback: ((Boolean) -> Unit)? = null) = launchOps(callback) {
|
fun Filter.remember(callback: (suspend (Boolean) -> Unit)? = null) = launchOps(callback) {
|
||||||
EhDB.addFilter(this).also { if (it) filters.await().add(this) }
|
EhDB.addFilter(this).also { if (it) filters.await().add(this) }
|
||||||
}
|
}
|
||||||
fun Filter.trigger(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
|
fun Filter.trigger(callback: (suspend (Unit) -> Unit)? = null) = launchOps(callback) {
|
||||||
enable = !enable
|
enable = !enable
|
||||||
EhDB.updateFilter(this)
|
EhDB.updateFilter(this)
|
||||||
}
|
}
|
||||||
fun Filter.forget(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
|
fun Filter.forget(callback: (suspend (Unit) -> Unit)? = null) = launchOps(callback) {
|
||||||
EhDB.deleteFilter(this)
|
EhDB.deleteFilter(this)
|
||||||
filters.await().remove(this)
|
filters.await().remove(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ import android.util.Log
|
|||||||
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
|
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient
|
import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -38,13 +43,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.executeAsync
|
import okhttp3.executeAsync
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import kotlin.contracts.InvocationKind
|
|
||||||
import kotlin.contracts.contract
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
|
|
||||||
inline fun ehRequest(url: String, referer: String? = null, origin: String? = null, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply {
|
inline fun ehRequest(url: String, referer: String? = null, origin: String? = null, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply {
|
||||||
addHeader("User-Agent", CHROME_USER_AGENT)
|
addHeader("User-Agent", Settings.userAgent)
|
||||||
addHeader("Accept", CHROME_ACCEPT)
|
addHeader("Accept", CHROME_ACCEPT)
|
||||||
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
|
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
|
||||||
referer?.let { addHeader("Referer", it) }
|
referer?.let { addHeader("Referer", it) }
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import com.hippo.ehviewer.R
|
|||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.copyToFile
|
import com.hippo.ehviewer.util.copyToFile
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -42,8 +44,6 @@ import okio.blackholeSink
|
|||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
object EhTagDatabase : CoroutineScope {
|
object EhTagDatabase : CoroutineScope {
|
||||||
private const val NAMESPACE_PREFIX = "n"
|
private const val NAMESPACE_PREFIX = "n"
|
||||||
@@ -55,7 +55,6 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
)
|
)
|
||||||
private lateinit var tagGroups: Map<String, Map<String, String>>
|
private lateinit var tagGroups: Map<String, Map<String, String>>
|
||||||
private val updateLock = Mutex()
|
private val updateLock = Mutex()
|
||||||
private val dbLock = Mutex()
|
|
||||||
override val coroutineContext = Dispatchers.IO + Job()
|
override val coroutineContext = Dispatchers.IO + Job()
|
||||||
|
|
||||||
var initialized by mutableStateOf(false)
|
var initialized by mutableStateOf(false)
|
||||||
@@ -202,42 +201,42 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun issueUpdateInMemoryData() {
|
private fun issueUpdateInMemoryData(file: File? = null) {
|
||||||
dbLock.withLock {
|
val dataFile = file ?: getMetadata(appCtx)?.let { metadata ->
|
||||||
getMetadata(appCtx)?.let { urls ->
|
val dataName = metadata[2]
|
||||||
val dataName = urls[2]
|
val dir = AppConfig.getFilesDir("tag-translations")
|
||||||
val dir = AppConfig.getFilesDir("tag-translations")
|
File(dir, dataName).takeIf { it.exists() }
|
||||||
val dataFile = File(dir, dataName).takeIf { it.exists() } ?: return
|
} ?: return
|
||||||
runSuspendCatching {
|
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
|
||||||
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
|
initialized = true
|
||||||
initialized = true
|
|
||||||
}.onFailure {
|
|
||||||
it.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launch {
|
launch {
|
||||||
issueUpdateInMemoryData()
|
updateLock.withLock {
|
||||||
|
runSuspendCatching {
|
||||||
|
issueUpdateInMemoryData()
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
}
|
||||||
|
updateInternal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateInternal() {
|
private suspend fun updateInternal() {
|
||||||
val urls = getMetadata(appCtx)
|
getMetadata(appCtx)?.let { metadata ->
|
||||||
urls?.let {
|
val sha1Name = metadata[0]
|
||||||
val sha1Name = urls[0]
|
val sha1Url = metadata[1]
|
||||||
val sha1Url = urls[1]
|
val dataName = metadata[2]
|
||||||
val dataName = urls[2]
|
val dataUrl = metadata[3]
|
||||||
val dataUrl = urls[3]
|
|
||||||
|
|
||||||
val dir = AppConfig.getFilesDir("tag-translations")
|
val dir = AppConfig.getFilesDir("tag-translations")
|
||||||
checkNotNull(dir)
|
checkNotNull(dir)
|
||||||
val sha1File = File(dir, sha1Name)
|
val sha1File = File(dir, sha1Name)
|
||||||
val dataFile = File(dir, dataName)
|
val dataFile = File(dir, dataName)
|
||||||
|
|
||||||
runCatching {
|
runSuspendCatching {
|
||||||
// Check current sha1 and current data
|
// Check current sha1 and current data
|
||||||
val sha1 = getFileContent(sha1File)
|
val sha1 = getFileContent(sha1File)
|
||||||
if (!checkData(sha1, dataFile)) {
|
if (!checkData(sha1, dataFile)) {
|
||||||
@@ -254,7 +253,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
if (tempSha1 == sha1) {
|
if (tempSha1 == sha1) {
|
||||||
// The data is the same
|
// The data is the same
|
||||||
FileUtils.delete(tempSha1File)
|
FileUtils.delete(tempSha1File)
|
||||||
return@runCatching
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save new data
|
// Save new data
|
||||||
@@ -265,7 +264,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
if (!checkData(tempSha1, tempDataFile)) {
|
if (!checkData(tempSha1, tempDataFile)) {
|
||||||
FileUtils.delete(tempSha1File)
|
FileUtils.delete(tempSha1File)
|
||||||
FileUtils.delete(tempDataFile)
|
FileUtils.delete(tempDataFile)
|
||||||
return@runCatching
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace current sha1 and current data with new sha1 and new data
|
// Replace current sha1 and current data with new sha1 and new data
|
||||||
@@ -275,7 +274,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
tempDataFile.renameTo(dataFile)
|
tempDataFile.renameTo(dataFile)
|
||||||
|
|
||||||
// Read new EhTagDatabase
|
// Read new EhTagDatabase
|
||||||
issueUpdateInMemoryData()
|
issueUpdateInMemoryData(dataFile)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
it.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,14 @@ object EhUrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val apiUrl: String
|
val apiUrl: String
|
||||||
get() = when (Settings.gallerySite) {
|
get() = if (Settings.forceEhThumb) {
|
||||||
SITE_E -> API_E
|
API_E
|
||||||
SITE_EX -> API_EX
|
} else {
|
||||||
else -> API_E
|
when (Settings.gallerySite) {
|
||||||
|
SITE_E -> API_E
|
||||||
|
SITE_EX -> API_EX
|
||||||
|
else -> API_E
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val referer: String
|
val referer: String
|
||||||
|
|||||||
@@ -146,10 +146,6 @@ object EhUtils {
|
|||||||
Settings.needSignIn = true
|
Settings.needSignIn = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun needSignedIn(): Boolean {
|
|
||||||
return Settings.needSignIn
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSuitableTitle(gi: GalleryInfo): String {
|
fun getSuitableTitle(gi: GalleryInfo): String {
|
||||||
return if (Settings.showJpnTitle) {
|
return if (Settings.showJpnTitle) {
|
||||||
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
|
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
|
||||||
|
|||||||
@@ -43,63 +43,24 @@ interface GalleryInfo {
|
|||||||
?: title?.let { generateSLangFromTitle(it) }
|
?: title?.let { generateSLangFromTitle(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateSLangFromTags(simpleTags: List<String>): String? {
|
|
||||||
for (tag in simpleTags) {
|
|
||||||
for (i in S_LANGS.indices) {
|
|
||||||
if (S_LANG_TAGS[i] == tag) {
|
|
||||||
return S_LANGS[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateSLangFromTitle(title: String): String? {
|
|
||||||
for (i in S_LANGS.indices) {
|
|
||||||
if (S_LANG_PATTERNS[i].matcher(title).find()) {
|
|
||||||
return S_LANGS[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* ISO 639-1
|
|
||||||
*/
|
|
||||||
const val S_LANG_JA = "JA"
|
|
||||||
const val S_LANG_EN = "EN"
|
|
||||||
const val S_LANG_ZH = "ZH"
|
|
||||||
const val S_LANG_NL = "NL"
|
|
||||||
const val S_LANG_FR = "FR"
|
|
||||||
const val S_LANG_DE = "DE"
|
|
||||||
const val S_LANG_HU = "HU"
|
|
||||||
const val S_LANG_IT = "IT"
|
|
||||||
const val S_LANG_KO = "KO"
|
|
||||||
const val S_LANG_PL = "PL"
|
|
||||||
const val S_LANG_PT = "PT"
|
|
||||||
const val S_LANG_RU = "RU"
|
|
||||||
const val S_LANG_ES = "ES"
|
|
||||||
const val S_LANG_TH = "TH"
|
|
||||||
const val S_LANG_VI = "VI"
|
|
||||||
|
|
||||||
val S_LANGS = arrayOf(
|
val S_LANGS = arrayOf(
|
||||||
S_LANG_EN,
|
"EN",
|
||||||
S_LANG_ZH,
|
"ZH",
|
||||||
S_LANG_ES,
|
"ES",
|
||||||
S_LANG_KO,
|
"KO",
|
||||||
S_LANG_RU,
|
"RU",
|
||||||
S_LANG_FR,
|
"FR",
|
||||||
S_LANG_PT,
|
"PT",
|
||||||
S_LANG_TH,
|
"TH",
|
||||||
S_LANG_DE,
|
"DE",
|
||||||
S_LANG_IT,
|
"IT",
|
||||||
S_LANG_VI,
|
"VI",
|
||||||
S_LANG_PL,
|
"PL",
|
||||||
S_LANG_HU,
|
"HU",
|
||||||
S_LANG_NL,
|
"NL",
|
||||||
)
|
)
|
||||||
val S_LANG_PATTERNS = arrayOf(
|
private val S_LANG_PATTERNS = arrayOf(
|
||||||
Pattern.compile(
|
Pattern.compile(
|
||||||
"[(\\[]eng(?:lish)?[)\\]]|英訳",
|
"[(\\[]eng(?:lish)?[)\\]]|英訳",
|
||||||
Pattern.CASE_INSENSITIVE,
|
Pattern.CASE_INSENSITIVE,
|
||||||
@@ -131,7 +92,7 @@ interface GalleryInfo {
|
|||||||
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
|
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
|
||||||
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
|
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
|
||||||
)
|
)
|
||||||
val S_LANG_TAGS = arrayOf(
|
private val S_LANG_TAGS = arrayOf(
|
||||||
"language:english",
|
"language:english",
|
||||||
"language:chinese",
|
"language:chinese",
|
||||||
"language:spanish",
|
"language:spanish",
|
||||||
@@ -148,6 +109,26 @@ interface GalleryInfo {
|
|||||||
"language:dutch",
|
"language:dutch",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun generateSLangFromTags(simpleTags: List<String>): String? {
|
||||||
|
for (tag in simpleTags) {
|
||||||
|
for (i in S_LANGS.indices) {
|
||||||
|
if (S_LANG_TAGS[i] == tag) {
|
||||||
|
return S_LANGS[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateSLangFromTitle(title: String): String? {
|
||||||
|
for (i in S_LANGS.indices) {
|
||||||
|
if (S_LANG_PATTERNS[i].matcher(title).find()) {
|
||||||
|
return S_LANGS[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const val NOT_FAVORITED = -2
|
const val NOT_FAVORITED = -2
|
||||||
const val LOCAL_FAVORITED = -1
|
const val LOCAL_FAVORITED = -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import com.hippo.ehviewer.dao.QuickSearch
|
|||||||
import com.hippo.ehviewer.ui.main.AdvanceTable
|
import com.hippo.ehviewer.ui.main.AdvanceTable
|
||||||
import com.hippo.ehviewer.util.encodeUTF8
|
import com.hippo.ehviewer.util.encodeUTF8
|
||||||
import com.hippo.ehviewer.util.toIntOrDefault
|
import com.hippo.ehviewer.util.toIntOrDefault
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ListUrlBuilder(
|
data class ListUrlBuilder(
|
||||||
@@ -320,66 +320,67 @@ data class ListUrlBuilder(
|
|||||||
// Pages
|
// Pages
|
||||||
if (pageFrom != -1 || pageTo != -1) {
|
if (pageFrom != -1 || pageTo != -1) {
|
||||||
addQueryParameter("f_sp", "on")
|
addQueryParameter("f_sp", "on")
|
||||||
addQueryParameter(
|
addQueryParameterIfNotBlank(
|
||||||
"f_spf",
|
"f_spf",
|
||||||
(if (pageFrom != -1) pageFrom.toString() else "").toString(),
|
pageFrom.takeUnless { it == -1 }?.toString(),
|
||||||
)
|
)
|
||||||
addQueryParameter(
|
addQueryParameterIfNotBlank(
|
||||||
"f_spt",
|
"f_spt",
|
||||||
(if (pageTo != -1) pageTo.toString() else "").toString(),
|
pageTo.takeUnless { it == -1 }?.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.toString()
|
}.toString()
|
||||||
|
|
||||||
MODE_UPLOADER -> {
|
MODE_UPLOADER -> {
|
||||||
val sb = StringBuilder(EhUrl.host)
|
buildString {
|
||||||
mKeyword?.let {
|
append(EhUrl.host)
|
||||||
sb.append("uploader/")
|
mKeyword?.let {
|
||||||
sb.append(encodeUTF8(it))
|
append("uploader/").append(encodeUTF8(it))
|
||||||
|
}
|
||||||
|
mPrev?.let {
|
||||||
|
append("?prev=").append(it)
|
||||||
|
}
|
||||||
|
mNext?.let {
|
||||||
|
append("?next=").append(it)
|
||||||
|
}
|
||||||
|
mJumpTo?.let {
|
||||||
|
append("&seek=").append(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mPrev?.let {
|
|
||||||
sb.append("?prev=").append(it)
|
|
||||||
}
|
|
||||||
mNext?.let {
|
|
||||||
sb.append("?next=").append(it)
|
|
||||||
}
|
|
||||||
mJumpTo?.let {
|
|
||||||
sb.append("&seek=").append(it)
|
|
||||||
}
|
|
||||||
sb.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MODE_TAG -> {
|
MODE_TAG -> {
|
||||||
val sb = StringBuilder(EhUrl.host)
|
buildString {
|
||||||
mKeyword?.let {
|
append(EhUrl.host)
|
||||||
sb.append("tag/")
|
mKeyword?.let {
|
||||||
sb.append(encodeUTF8(it))
|
append("tag/").append(encodeUTF8(it))
|
||||||
|
}
|
||||||
|
mPrev?.let {
|
||||||
|
append("?prev=").append(it)
|
||||||
|
}
|
||||||
|
mNext?.let {
|
||||||
|
append("?next=").append(it)
|
||||||
|
}
|
||||||
|
mJumpTo?.let {
|
||||||
|
append("&seek=").append(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mPrev?.let {
|
|
||||||
sb.append("?prev=").append(it)
|
|
||||||
}
|
|
||||||
mNext?.let {
|
|
||||||
sb.append("?next=").append(it)
|
|
||||||
}
|
|
||||||
mJumpTo?.let {
|
|
||||||
sb.append("&seek=").append(it)
|
|
||||||
}
|
|
||||||
sb.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||||
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
|
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
|
||||||
MODE_TOPLIST -> {
|
MODE_TOPLIST -> {
|
||||||
val sb = StringBuilder(EhUrl.HOST_E)
|
buildString {
|
||||||
sb.append("toplist.php?tl=")
|
append(EhUrl.HOST_E)
|
||||||
mKeyword.orEmpty().let {
|
append("toplist.php?tl=")
|
||||||
sb.append(encodeUTF8(it))
|
mKeyword.orEmpty().let {
|
||||||
|
append(encodeUTF8(it))
|
||||||
|
}
|
||||||
|
mJumpTo?.let {
|
||||||
|
append("&p=").append(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mJumpTo?.let {
|
|
||||||
sb.append("&p=").append(it)
|
|
||||||
}
|
|
||||||
sb.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package com.hippo.ehviewer.client.parser
|
|||||||
|
|
||||||
import arrow.core.unzip
|
import arrow.core.unzip
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.EhFilter
|
import com.hippo.ehviewer.client.EhFilter
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.client.EhUtils.getCategory
|
import com.hippo.ehviewer.client.EhUtils.getCategory
|
||||||
@@ -41,6 +42,10 @@ import com.hippo.ehviewer.util.toFloatOrDefault
|
|||||||
import com.hippo.ehviewer.util.toIntOrDefault
|
import com.hippo.ehviewer.util.toIntOrDefault
|
||||||
import com.hippo.ehviewer.util.trimAnd
|
import com.hippo.ehviewer.util.trimAnd
|
||||||
import com.hippo.ehviewer.util.unescapeXml
|
import com.hippo.ehviewer.util.unescapeXml
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Locale
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
@@ -48,10 +53,6 @@ import org.jsoup.nodes.Node
|
|||||||
import org.jsoup.select.Elements
|
import org.jsoup.select.Elements
|
||||||
import org.jsoup.select.NodeTraversor
|
import org.jsoup.select.NodeTraversor
|
||||||
import org.jsoup.select.NodeVisitor
|
import org.jsoup.select.NodeVisitor
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
object GalleryDetailParser {
|
object GalleryDetailParser {
|
||||||
private val PATTERN_ERROR = Regex("<div class=\"d\">\n<p>([^<]+)</p>")
|
private val PATTERN_ERROR = Regex("<div class=\"d\">\n<p>([^<]+)</p>")
|
||||||
@@ -394,7 +395,7 @@ object GalleryDetailParser {
|
|||||||
// filter comment
|
// filter comment
|
||||||
if (!comment.uploader) {
|
if (!comment.uploader) {
|
||||||
val sEhFilter = EhFilter
|
val sEhFilter = EhFilter
|
||||||
if (sEhFilter.filterCommenter(comment.user!!) || sEhFilter.filterComment(comment.comment!!)) {
|
if (comment.score <= Settings.commentThreshold || sEhFilter.filterCommenter(comment.user!!) || sEhFilter.filterComment(comment.comment!!)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ object GalleryPageApiParser {
|
|||||||
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
|
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
|
||||||
groupValues[1].trim().unescapeXml()
|
groupValues[1].trim().unescapeXml()
|
||||||
}
|
}
|
||||||
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKey)?.run {
|
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKeyAndOriginImageUrl)?.run {
|
||||||
groupValues[1].trim().unescapeXml()
|
groupValues[1].trim().unescapeXml()
|
||||||
}
|
}
|
||||||
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.originImageUrl)?.run {
|
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.skipHathKeyAndOriginImageUrl)?.run {
|
||||||
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
|
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
|
||||||
}
|
}
|
||||||
check(!imageUrl.isNullOrEmpty())
|
check(!imageUrl.isNullOrEmpty())
|
||||||
@@ -49,9 +49,7 @@ object GalleryPageApiParser {
|
|||||||
@SerialName("i3")
|
@SerialName("i3")
|
||||||
val imageUrl: String,
|
val imageUrl: String,
|
||||||
@SerialName("i6")
|
@SerialName("i6")
|
||||||
val skipHathKey: String,
|
val skipHathKeyAndOriginImageUrl: String,
|
||||||
@SerialName("i7")
|
|
||||||
val originImageUrl: String,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
|
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package com.hippo.ehviewer.client.parser
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
object HomeParser {
|
object HomeParser {
|
||||||
private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
|
private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.hippo.ehviewer.coil
|
|||||||
import coil.intercept.Interceptor
|
import coil.intercept.Interceptor
|
||||||
import coil.request.ImageResult
|
import coil.request.ImageResult
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlinx.coroutines.CancellableContinuation
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -11,8 +13,6 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
|
|
||||||
object MergeInterceptor : Interceptor {
|
object MergeInterceptor : Interceptor {
|
||||||
private val pendingContinuationMap: HashMap<String, MutableList<CancellableContinuation<Unit>>> = hashMapOf()
|
private val pendingContinuationMap: HashMap<String, MutableList<CancellableContinuation<Unit>>> = hashMapOf()
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.hippo.ehviewer.cronet
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.EhApplication
|
||||||
|
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||||
|
import io.ktor.utils.io.pool.useInstance
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
|
val pool = DirectByteBufferPool(32)
|
||||||
|
|
||||||
|
val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService
|
||||||
|
|
||||||
|
suspend inline fun CronetRequest.awaitBodyFully(crossinline callback: (ByteBuffer) -> Unit) {
|
||||||
|
return pool.useInstance { buffer ->
|
||||||
|
suspendCancellableCoroutine { cont ->
|
||||||
|
consumer = {
|
||||||
|
callback(it)
|
||||||
|
buffer.clear()
|
||||||
|
request.read(buffer)
|
||||||
|
}
|
||||||
|
onError = { readerCont.resumeWithException(it) }
|
||||||
|
readerCont = cont
|
||||||
|
request.read(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun CronetRequest.copyToChannel(chan: FileChannel, crossinline listener: ((Int) -> Unit) = {}) = awaitBodyFully {
|
||||||
|
val bytes = chan.write(it)
|
||||||
|
listener(bytes)
|
||||||
|
}
|
||||||
@@ -24,12 +24,12 @@ import com.hippo.ehviewer.Settings
|
|||||||
import com.hippo.ehviewer.client.EhCookieStore
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import splitties.init.appCtx
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
private val signedIn
|
private val signedIn
|
||||||
get() = EhCookieStore.hasSignedIn()
|
get() = EhCookieStore.hasSignedIn()
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import com.hippo.ehviewer.util.runAssertingNotMainThread
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import java.util.LinkedList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
@@ -50,7 +51,6 @@ import kotlinx.coroutines.flow.shareIn
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import splitties.preferences.edit
|
import splitties.preferences.edit
|
||||||
import java.util.LinkedList
|
|
||||||
|
|
||||||
object DownloadManager : OnSpiderListener {
|
object DownloadManager : OnSpiderListener {
|
||||||
// All download info list
|
// All download info list
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import com.hippo.ehviewer.jni.providePassword
|
|||||||
import com.hippo.ehviewer.jni.releaseByteBuffer
|
import com.hippo.ehviewer.jni.releaseByteBuffer
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import java.nio.ByteBuffer
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -48,7 +49,6 @@ import kotlinx.coroutines.sync.Semaphore
|
|||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import moe.tarsin.coroutines.NamedMutex
|
import moe.tarsin.coroutines.NamedMutex
|
||||||
import moe.tarsin.coroutines.withLock
|
import moe.tarsin.coroutines.withLock
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
class ArchivePageLoader(context: Context, private val uri: Uri, passwdFlow: Flow<String>) : PageLoader2(), CoroutineScope {
|
class ArchivePageLoader(context: Context, private val uri: Uri, passwdFlow: Flow<String>) : PageLoader2(), CoroutineScope {
|
||||||
override val coroutineContext = Dispatchers.IO + Job()
|
override val coroutineContext = Dispatchers.IO + Job()
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ import java.util.Locale
|
|||||||
class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpiderListener {
|
class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpiderListener {
|
||||||
private lateinit var mSpiderQueen: SpiderQueen
|
private lateinit var mSpiderQueen: SpiderQueen
|
||||||
override fun start() {
|
override fun start() {
|
||||||
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
|
if (!::mSpiderQueen.isInitialized) {
|
||||||
mSpiderQueen.addOnSpiderListener(this)
|
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
|
||||||
|
mSpiderQueen.addOnSpiderListener(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
@file:Suppress("ktlint:standard:property-naming")
|
||||||
|
|
||||||
|
package com.hippo.ehviewer.icons
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
object EhIcons {
|
||||||
|
object Filled
|
||||||
|
|
||||||
|
object Big {
|
||||||
|
object Filled
|
||||||
|
|
||||||
|
val Default = Filled
|
||||||
|
}
|
||||||
|
|
||||||
|
val Default = Filled
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun bigIcon(
|
||||||
|
name: String,
|
||||||
|
block: ImageVector.Builder.() -> ImageVector.Builder,
|
||||||
|
): ImageVector = ImageVector.Builder(
|
||||||
|
name = name,
|
||||||
|
defaultWidth = BigIconDimension.dp,
|
||||||
|
defaultHeight = BigIconDimension.dp,
|
||||||
|
viewportWidth = MaterialIconDimension,
|
||||||
|
viewportHeight = MaterialIconDimension,
|
||||||
|
).block().build()
|
||||||
|
|
||||||
|
const val BigIconDimension = 120f
|
||||||
|
const val MaterialIconDimension = 24f
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.hippo.ehviewer.icons.big
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.materialPath
|
||||||
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
|
import com.hippo.ehviewer.icons.bigIcon
|
||||||
|
import com.hippo.ehviewer.util.unsafeLazy
|
||||||
|
|
||||||
|
val EhIcons.Big.Filled.History by unsafeLazy {
|
||||||
|
bigIcon(name = "History") {
|
||||||
|
materialPath {
|
||||||
|
moveTo(5.5F, 2.0F)
|
||||||
|
horizontalLineTo(22.0F)
|
||||||
|
verticalLineTo(0.0F)
|
||||||
|
horizontalLineTo(5.0F)
|
||||||
|
curveTo(3.3F, 0.0F, 2.0F, 1.3F, 2.0F, 3.0F)
|
||||||
|
verticalLineTo(21.0F)
|
||||||
|
curveTo(2.0F, 22.7F, 3.3F, 24.0F, 5.0F, 24.0F)
|
||||||
|
horizontalLineTo(22.0F)
|
||||||
|
verticalLineTo(4.0F)
|
||||||
|
horizontalLineTo(5.5F)
|
||||||
|
curveTo(4.1F, 4.0F, 4.1F, 2.0F, 5.5F, 2.0F)
|
||||||
|
close()
|
||||||
|
moveTo(20.0F, 6.0F)
|
||||||
|
verticalLineTo(22.0F)
|
||||||
|
horizontalLineTo(6.0F)
|
||||||
|
verticalLineTo(6.0F)
|
||||||
|
horizontalLineTo(20.0F)
|
||||||
|
moveTo(12.5F, 11.8F)
|
||||||
|
verticalLineTo(14.4F)
|
||||||
|
lineTo(14.9F, 15.8F)
|
||||||
|
lineTo(15.3F, 15.2F)
|
||||||
|
lineTo(13.3F, 14.0F)
|
||||||
|
verticalLineTo(11.9F)
|
||||||
|
moveTo(13.3F, 9.3F)
|
||||||
|
curveTo(11.5F, 9.3F, 10.0F, 10.3F, 9.2F, 11.7F)
|
||||||
|
lineTo(8.0F, 10.5F)
|
||||||
|
verticalLineTo(13.8F)
|
||||||
|
horizontalLineTo(11.3F)
|
||||||
|
lineTo(9.9F, 12.4F)
|
||||||
|
curveTo(10.5F, 11.1F, 11.8F, 10.3F, 13.3F, 10.3F)
|
||||||
|
curveTo(15.4F, 10.3F, 17.1F, 12.0F, 17.1F, 14.1F)
|
||||||
|
reflectiveCurveTo(15.4F, 17.9F, 13.3F, 17.9F)
|
||||||
|
curveTo(11.7F, 17.9F, 10.3F, 16.9F, 9.8F, 15.4F)
|
||||||
|
horizontalLineTo(8.7F)
|
||||||
|
curveTo(9.3F, 17.4F, 11.1F, 18.9F, 13.3F, 18.9F)
|
||||||
|
curveTo(15.9F, 18.9F, 18.1F, 16.8F, 18.1F, 14.1F)
|
||||||
|
curveTo(18.0F, 11.4F, 15.9F, 9.3F, 13.3F, 9.3F)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.hippo.ehviewer.icons.big
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.materialPath
|
||||||
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
|
import com.hippo.ehviewer.icons.bigIcon
|
||||||
|
import com.hippo.ehviewer.util.unsafeLazy
|
||||||
|
|
||||||
|
val EhIcons.Big.Filled.SadAndroid by unsafeLazy {
|
||||||
|
bigIcon(name = "SadAndroid") {
|
||||||
|
materialPath {
|
||||||
|
moveTo(16.5F, 10.8F)
|
||||||
|
curveToRelative(-0.61F, 0.0F, -1.1F, 0.49F, -1.1F, 1.1F)
|
||||||
|
curveToRelative(0.0F, 0.61F, 0.49F, 1.1F, 1.1F, 1.1F)
|
||||||
|
reflectiveCurveToRelative(1.1F, -0.49F, 1.1F, -1.1F)
|
||||||
|
curveTo(17.6F, 11.29F, 17.11F, 10.8F, 16.5F, 10.8F)
|
||||||
|
close()
|
||||||
|
moveTo(8.2F, 10.1F)
|
||||||
|
curveToRelative(-0.61F, 0.0F, -1.1F, 0.49F, -1.1F, 1.1F)
|
||||||
|
reflectiveCurveToRelative(0.49F, 1.1F, 1.1F, 1.1F)
|
||||||
|
reflectiveCurveToRelative(1.1F, -0.49F, 1.1F, -1.1F)
|
||||||
|
reflectiveCurveTo(8.81F, 10.1F, 8.2F, 10.1F)
|
||||||
|
close()
|
||||||
|
moveTo(22.1F, 16.4F)
|
||||||
|
curveToRelative(-0.46F, -3.33F, -2.24F, -5.93F, -4.81F, -7.45F)
|
||||||
|
lineTo(19.5F, 6.3F)
|
||||||
|
curveToRelative(0.1F, -0.1F, 0.1F, -0.3F, 0.0F, -0.4F)
|
||||||
|
curveToRelative(-0.1F, -0.1F, -0.3F, -0.1F, -0.4F, 0.0F)
|
||||||
|
lineToRelative(-2.27F, 2.79F)
|
||||||
|
curveToRelative(-1.26F, -0.66F, -2.69F, -1.07F, -4.23F, -1.19F)
|
||||||
|
curveToRelative(-1.4F, -0.15F, -2.77F, -0.01F, -4.05F, 0.37F)
|
||||||
|
lineTo(6.7F, 4.7F)
|
||||||
|
curveTo(6.6F, 4.6F, 6.4F, 4.5F, 6.3F, 4.6F)
|
||||||
|
curveTo(6.2F, 4.7F, 6.1F, 4.9F, 6.2F, 5.0F)
|
||||||
|
lineToRelative(1.79F, 3.05F)
|
||||||
|
curveToRelative(-2.83F, 1.02F, -5.12F, 3.3F, -6.19F, 6.55F)
|
||||||
|
lineToRelative(15.44F, 1.37F)
|
||||||
|
curveToRelative(0.0F, 0.02F, -0.01F, 0.04F, -0.01F, 0.05F)
|
||||||
|
curveToRelative(0.0F, 0.59F, 0.48F, 1.07F, 1.07F, 1.07F)
|
||||||
|
curveToRelative(0.54F, 0.0F, 0.97F, -0.41F, 1.04F, -0.93F)
|
||||||
|
lineTo(22.1F, 16.4F)
|
||||||
|
close()
|
||||||
|
moveTo(2.63F, 14.07F)
|
||||||
|
curveToRelative(1.51F, -3.74F, 4.83F, -6.03F, 8.84F, -6.03F)
|
||||||
|
curveToRelative(0.35F, 0.0F, 0.71F, 0.02F, 1.07F, 0.06F)
|
||||||
|
curveToRelative(4.55F, 0.36F, 7.94F, 3.33F, 8.84F, 7.64F)
|
||||||
|
lineToRelative(-2.12F, -0.19F)
|
||||||
|
curveToRelative(-0.27F, -0.68F, -0.95F, -1.46F, -0.95F, -1.46F)
|
||||||
|
reflectiveCurveToRelative(-0.58F, 0.66F, -0.89F, 1.29F)
|
||||||
|
lineTo(2.63F, 14.07F)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.hippo.ehviewer.icons.filled
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.materialIcon
|
||||||
|
import androidx.compose.material.icons.materialPath
|
||||||
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
|
import com.hippo.ehviewer.util.unsafeLazy
|
||||||
|
|
||||||
|
val EhIcons.Filled.SadPanda by unsafeLazy {
|
||||||
|
materialIcon(name = "SadPanda") {
|
||||||
|
materialPath {
|
||||||
|
moveTo(21.1F, 7.8F)
|
||||||
|
curveTo(22.2F, 7.4F, 23.0F, 6.3F, 23.0F, 5.0F)
|
||||||
|
curveTo(23.0F, 3.3F, 21.7F, 2.0F, 20.0F, 2.0F)
|
||||||
|
curveTo(18.9F, 2.0F, 17.9F, 2.6F, 17.4F, 3.6F)
|
||||||
|
lineTo(21.1F, 7.8F)
|
||||||
|
moveTo(6.6F, 3.6F)
|
||||||
|
curveTo(6.1F, 2.6F, 5.1F, 2.0F, 4.0F, 2.0F)
|
||||||
|
curveTo(2.3F, 2.0F, 1.0F, 3.3F, 1.0F, 5.0F)
|
||||||
|
curveTo(1.0F, 6.3F, 1.8F, 7.4F, 2.9F, 7.8F)
|
||||||
|
lineTo(6.6F, 3.6F)
|
||||||
|
moveTo(10.1F, 13.0F)
|
||||||
|
lineTo(8.0F, 15.1F)
|
||||||
|
curveTo(7.4F, 15.7F, 6.5F, 15.7F, 5.9F, 15.1F)
|
||||||
|
lineTo(5.9F, 15.1F)
|
||||||
|
curveTo(5.3F, 14.5F, 5.3F, 13.6F, 5.9F, 13.0F)
|
||||||
|
lineTo(8.0F, 10.9F)
|
||||||
|
curveTo(8.6F, 10.3F, 9.5F, 10.3F, 10.1F, 10.9F)
|
||||||
|
lineTo(10.1F, 10.9F)
|
||||||
|
curveTo(10.7F, 11.5F, 10.7F, 12.4F, 10.1F, 13.0F)
|
||||||
|
moveTo(16.0F, 15.1F)
|
||||||
|
lineTo(13.9F, 13.0F)
|
||||||
|
curveTo(13.3F, 12.4F, 13.3F, 11.5F, 13.9F, 10.9F)
|
||||||
|
lineTo(13.9F, 10.9F)
|
||||||
|
curveTo(14.5F, 10.3F, 15.4F, 10.3F, 16.0F, 10.9F)
|
||||||
|
lineTo(18.1F, 13.0F)
|
||||||
|
curveTo(18.7F, 13.6F, 18.7F, 14.5F, 18.1F, 15.1F)
|
||||||
|
lineTo(18.1F, 15.1F)
|
||||||
|
curveTo(17.5F, 15.7F, 16.6F, 15.7F, 16.0F, 15.1F)
|
||||||
|
moveTo(12.0F, 17.0F)
|
||||||
|
curveTo(12.8F, 17.0F, 13.5F, 17.4F, 13.5F, 18.0F)
|
||||||
|
reflectiveCurveTo(12.8F, 19.0F, 12.0F, 19.0F)
|
||||||
|
reflectiveCurveTo(10.5F, 18.6F, 10.5F, 18.0F)
|
||||||
|
reflectiveCurveTo(11.2F, 17.0F, 12.0F, 17.0F)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.hippo.ehviewer.icons.filled
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.materialIcon
|
||||||
|
import androidx.compose.material.icons.materialPath
|
||||||
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
|
import com.hippo.ehviewer.util.unsafeLazy
|
||||||
|
|
||||||
|
val EhIcons.Filled.Subscriptions by unsafeLazy {
|
||||||
|
materialIcon(name = "Subscriptions") {
|
||||||
|
materialPath {
|
||||||
|
moveTo(20.0F, 8.0F)
|
||||||
|
horizontalLineTo(4.0F)
|
||||||
|
verticalLineTo(6.0F)
|
||||||
|
horizontalLineToRelative(16.0F)
|
||||||
|
verticalLineTo(8.0F)
|
||||||
|
moveTo(18.0F, 2.0F)
|
||||||
|
horizontalLineTo(6.0F)
|
||||||
|
verticalLineToRelative(2.0F)
|
||||||
|
horizontalLineToRelative(12.0F)
|
||||||
|
verticalLineTo(2.0F)
|
||||||
|
moveTo(20.0F, 10.0F)
|
||||||
|
horizontalLineTo(4.0F)
|
||||||
|
curveToRelative(-1.1F, 0.0F, -2.0F, 0.9F, -2.0F, 2.0F)
|
||||||
|
verticalLineToRelative(8.0F)
|
||||||
|
curveToRelative(0.0F, 1.1F, 0.9F, 2.0F, 2.0F, 2.0F)
|
||||||
|
horizontalLineToRelative(16.0F)
|
||||||
|
curveToRelative(1.1F, 0.0F, 2.0F, -0.9F, 2.0F, -2.0F)
|
||||||
|
verticalLineToRelative(-8.0F)
|
||||||
|
curveTo(22.0F, 10.9F, 21.1F, 10.0F, 20.0F, 10.0F)
|
||||||
|
close()
|
||||||
|
moveTo(11.0F, 14.0F)
|
||||||
|
horizontalLineTo(7.0F)
|
||||||
|
verticalLineToRelative(1.0F)
|
||||||
|
horizontalLineToRelative(4.0F)
|
||||||
|
verticalLineToRelative(2.0F)
|
||||||
|
horizontalLineTo(7.0F)
|
||||||
|
verticalLineToRelative(1.0F)
|
||||||
|
horizontalLineToRelative(4.0F)
|
||||||
|
verticalLineToRelative(2.0F)
|
||||||
|
horizontalLineTo(5.0F)
|
||||||
|
verticalLineToRelative(-8.0F)
|
||||||
|
horizontalLineToRelative(6.0F)
|
||||||
|
verticalLineTo(14.0F)
|
||||||
|
close()
|
||||||
|
moveTo(19.0F, 20.0F)
|
||||||
|
horizontalLineToRelative(-2.0F)
|
||||||
|
verticalLineToRelative(-3.0F)
|
||||||
|
horizontalLineToRelative(-2.0F)
|
||||||
|
verticalLineToRelative(3.0F)
|
||||||
|
horizontalLineToRelative(-2.0F)
|
||||||
|
verticalLineToRelative(-8.0F)
|
||||||
|
horizontalLineToRelative(2.0F)
|
||||||
|
verticalLineToRelative(3.0F)
|
||||||
|
horizontalLineToRelative(2.0F)
|
||||||
|
verticalLineToRelative(-3.0F)
|
||||||
|
horizontalLineToRelative(2.0F)
|
||||||
|
verticalLineTo(20.0F)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,18 +46,17 @@ import com.hippo.ehviewer.util.isAtLeastP
|
|||||||
import com.hippo.ehviewer.util.isAtLeastU
|
import com.hippo.ehviewer.util.isAtLeastU
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.hippo.unifile.openInputStream
|
import com.hippo.unifile.openInputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlin.math.min
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
class Image private constructor(drawable: Drawable, private val src: AutoCloseable) {
|
class Image private constructor(drawable: Drawable, private val src: AutoCloseable) {
|
||||||
var mObtainedDrawable: Drawable? = drawable
|
var mObtainedDrawable: Drawable? = drawable
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val size: Int
|
val size = drawable.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 }
|
||||||
get() = mObtainedDrawable!!.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 }
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package com.hippo.ehviewer.legacy
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
|
||||||
private val OBSOLETE_CACHE_DIRS = arrayOf(
|
private val OBSOLETE_CACHE_DIRS = arrayOf(
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.hippo.ehviewer.legacy
|
package com.hippo.ehviewer.legacy
|
||||||
|
|
||||||
import com.hippo.ehviewer.spider.SpiderInfo
|
import com.hippo.ehviewer.spider.SpiderInfo
|
||||||
|
import java.io.InputStream
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
||||||
val source = inputStream.source().buffer()
|
val source = inputStream.source().buffer()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package com.hippo.ehviewer.spider
|
|||||||
|
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.ParcelFileDescriptor.MODE_READ_WRITE
|
import android.os.ParcelFileDescriptor.MODE_READ_WRITE
|
||||||
|
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
|
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
@@ -38,14 +39,13 @@ import com.hippo.ehviewer.util.isCronetSupported
|
|||||||
import com.hippo.ehviewer.util.sendTo
|
import com.hippo.ehviewer.util.sendTo
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.hippo.unifile.openOutputStream
|
import com.hippo.unifile.openOutputStream
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.io.path.readText
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.ForwardingSink
|
import okio.ForwardingSink
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.io.path.readText
|
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
|
|
||||||
|
|
||||||
class SpiderDen(mGalleryInfo: GalleryInfo) {
|
class SpiderDen(mGalleryInfo: GalleryInfo) {
|
||||||
private val mGid = mGalleryInfo.gid
|
private val mGid = mGalleryInfo.gid
|
||||||
@@ -59,7 +59,7 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
|
|||||||
suspend fun setMode(value: Int) {
|
suspend fun setMode(value: Int) {
|
||||||
mode = value
|
mode = value
|
||||||
if (mode == SpiderQueen.MODE_DOWNLOAD && downloadDir == null) {
|
if (mode == SpiderQueen.MODE_DOWNLOAD && downloadDir == null) {
|
||||||
downloadDir = getGalleryDownloadDir(mGid)!!.apply { check(ensureDir()) { "Download directory $uri is not valid directory!" } }
|
downloadDir = getGalleryDownloadDir(mGid)?.takeIf { it.ensureDir() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import com.hippo.ehviewer.legacy.readLegacySpiderInfo
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.hippo.unifile.openInputStream
|
import com.hippo.unifile.openInputStream
|
||||||
import com.hippo.unifile.openOutputStream
|
import com.hippo.unifile.openOutputStream
|
||||||
|
import java.io.File
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.cbor.Cbor
|
import kotlinx.serialization.cbor.Cbor
|
||||||
import kotlinx.serialization.decodeFromByteArray
|
import kotlinx.serialization.decodeFromByteArray
|
||||||
import kotlinx.serialization.encodeToByteArray
|
import kotlinx.serialization.encodeToByteArray
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class SpiderInfo(
|
class SpiderInfo(
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
|
|||||||
import com.hippo.ehviewer.client.EhUrl.referer
|
import com.hippo.ehviewer.client.EhUrl.referer
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
import com.hippo.ehviewer.client.ehRequest
|
import com.hippo.ehviewer.client.ehRequest
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
|
||||||
import com.hippo.ehviewer.client.exception.QuotaExceededException
|
import com.hippo.ehviewer.client.exception.QuotaExceededException
|
||||||
import com.hippo.ehviewer.client.execute
|
import com.hippo.ehviewer.client.execute
|
||||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePages
|
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePages
|
||||||
@@ -42,6 +41,9 @@ import com.hippo.ehviewer.image.Image
|
|||||||
import com.hippo.ehviewer.util.ExceptionUtils
|
import com.hippo.ehviewer.util.ExceptionUtils
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -59,9 +61,6 @@ import kotlinx.coroutines.sync.withPermit
|
|||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.time.Instant
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {
|
class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {
|
||||||
override val coroutineContext = Dispatchers.IO + Job()
|
override val coroutineContext = Dispatchers.IO + Job()
|
||||||
@@ -650,7 +649,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (imageUrl == null) {
|
if (imageUrl == null) {
|
||||||
runCatching {
|
runSuspendCatching {
|
||||||
EhEngine.getGalleryPageApi(
|
EhEngine.getGalleryPageApi(
|
||||||
mSpiderInfo.gid,
|
mSpiderInfo.gid,
|
||||||
index,
|
index,
|
||||||
@@ -664,13 +663,8 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
originImageUrl = it.originImageUrl
|
originImageUrl = it.originImageUrl
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
if (it is ParseException && "Key mismatch" == it.message) {
|
forceHtml = true
|
||||||
// Show key is wrong, enter a new loop to get the new show key
|
return@repeat
|
||||||
if (showKey == localShowKey) showKey = null
|
|
||||||
return@repeat
|
|
||||||
} else {
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import com.hippo.ehviewer.ui.scene.BaseScene
|
|||||||
import com.hippo.ehviewer.ui.tools.DialogState
|
import com.hippo.ehviewer.ui.tools.DialogState
|
||||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||||
import com.hippo.ehviewer.util.LongList
|
import com.hippo.ehviewer.util.LongList
|
||||||
|
import com.hippo.ehviewer.util.findActivity
|
||||||
import com.hippo.ehviewer.util.requestPermission
|
import com.hippo.ehviewer.util.requestPermission
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
@@ -68,7 +69,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import rikka.core.util.ContextUtils.requireActivity
|
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
object CommonOperations {
|
object CommonOperations {
|
||||||
@@ -314,7 +314,7 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
|
|||||||
title = EhUtils.getSuitableTitle(info),
|
title = EhUtils.getSuitableTitle(info),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
with(requireActivity<MainActivity>(context)) {
|
with(context.findActivity<MainActivity>()) {
|
||||||
when (selected) {
|
when (selected) {
|
||||||
0 -> navToReader(info)
|
0 -> navToReader(info)
|
||||||
1 -> withUIContext {
|
1 -> withUIContext {
|
||||||
|
|||||||
@@ -19,13 +19,12 @@ import android.content.res.Resources.Theme
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.navigation.ActivityNavigator
|
import androidx.navigation.ActivityNavigator
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||||
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
||||||
import rikka.insets.WindowInsetsHelper
|
|
||||||
import rikka.layoutinflater.view.LayoutInflaterFactory
|
|
||||||
|
|
||||||
abstract class EhActivity : AppCompatActivity() {
|
abstract class EhActivity : AppCompatActivity() {
|
||||||
@StyleRes
|
@StyleRes
|
||||||
@@ -43,8 +42,9 @@ abstract class EhActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate).addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !isNightMode()
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightNavigationBars = !isNightMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
|
|||||||
import com.hippo.ehviewer.client.thumbUrl
|
import com.hippo.ehviewer.client.thumbUrl
|
||||||
import com.hippo.ehviewer.databinding.InfoSheetBinding
|
import com.hippo.ehviewer.databinding.InfoSheetBinding
|
||||||
import com.hippo.ehviewer.ui.scene.navWithUrl
|
import com.hippo.ehviewer.ui.scene.navWithUrl
|
||||||
import com.hippo.ehviewer.util.tellClipboardWithToast
|
import com.hippo.ehviewer.util.addTextToClipboard
|
||||||
|
|
||||||
private const val INDEX_URL = 2
|
private const val INDEX_URL = 2
|
||||||
private const val INDEX_PARENT = 9
|
private const val INDEX_PARENT = 9
|
||||||
@@ -83,7 +83,7 @@ class GalleryInfoBottomSheet(private val detail: GalleryDetail) : BottomSheetDia
|
|||||||
navController.navWithUrl(content)
|
navController.navWithUrl(content)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
context tellClipboardWithToast content
|
context.addTextToClipboard(content, true)
|
||||||
if (index == INDEX_URL) {
|
if (index == INDEX_URL) {
|
||||||
// Save it to avoid detect the gallery
|
// Save it to avoid detect the gallery
|
||||||
Settings.clipboardTextHashCode = data[index].hashCode()
|
Settings.clipboardTextHashCode = data[index].hashCode()
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.ui
|
package com.hippo.ehviewer.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.assist.AssistContent
|
import android.app.assist.AssistContent
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -26,59 +25,111 @@ import android.graphics.Bitmap
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBars
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material.icons.filled.FormatListNumbered
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.Whatshot
|
||||||
|
import androidx.compose.material3.DrawerValue
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ModalDrawerSheet
|
||||||
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
|
import androidx.compose.material3.NavigationDrawerItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberDrawerState
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.currentRecomposeScope
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidViewBinding
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.customview.widget.Openable
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.ui.NavigationUI
|
|
||||||
import androidx.navigation.ui.onNavDestinationSelected2
|
import androidx.navigation.ui.onNavDestinationSelected2
|
||||||
import androidx.navigation.ui.setupWithNavController
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.Settings.launchPage
|
import com.hippo.ehviewer.Settings.launchPage
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
|
||||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||||
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
|
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
|
||||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
|
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
|
||||||
import com.hippo.ehviewer.databinding.ActivityMainBinding
|
import com.hippo.ehviewer.databinding.ActivityMainBinding
|
||||||
import com.hippo.ehviewer.download.DownloadService
|
import com.hippo.ehviewer.download.DownloadService
|
||||||
import com.hippo.ehviewer.download.downloadLocation
|
import com.hippo.ehviewer.download.downloadLocation
|
||||||
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
|
import com.hippo.ehviewer.icons.filled.Subscriptions
|
||||||
import com.hippo.ehviewer.image.Image.Companion.decodeBitmap
|
import com.hippo.ehviewer.image.Image.Companion.decodeBitmap
|
||||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||||
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
|
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
|
||||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||||
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
|
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
|
||||||
import com.hippo.ehviewer.ui.scene.GalleryListScene
|
|
||||||
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
|
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
|
||||||
import com.hippo.ehviewer.ui.scene.ProgressScene
|
import com.hippo.ehviewer.ui.scene.ProgressScene
|
||||||
import com.hippo.ehviewer.ui.scene.navAnimated
|
import com.hippo.ehviewer.ui.scene.navAnimated
|
||||||
import com.hippo.ehviewer.ui.scene.navWithUrl
|
import com.hippo.ehviewer.ui.scene.navWithUrl
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.addTextToClipboard
|
import com.hippo.ehviewer.util.addTextToClipboard
|
||||||
|
import com.hippo.ehviewer.util.buildWindowInsets
|
||||||
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
||||||
import com.hippo.ehviewer.util.getUrlFromClipboard
|
import com.hippo.ehviewer.util.getUrlFromClipboard
|
||||||
|
import com.hippo.ehviewer.util.set
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import splitties.systemservices.clipboardManager
|
import splitties.systemservices.clipboardManager
|
||||||
import splitties.systemservices.connectivityManager
|
import splitties.systemservices.connectivityManager
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
private val navItems = arrayOf(
|
||||||
|
Triple(R.id.nav_homepage, R.string.homepage, Icons.Default.Home),
|
||||||
|
Triple(R.id.nav_subscription, R.string.subscription, EhIcons.Default.Subscriptions),
|
||||||
|
Triple(R.id.nav_whats_hot, R.string.whats_hot, Icons.Default.Whatshot),
|
||||||
|
Triple(R.id.nav_toplist, R.string.toplist, Icons.Default.FormatListNumbered),
|
||||||
|
Triple(R.id.nav_favourite, R.string.favourite, Icons.Default.Favorite),
|
||||||
|
Triple(R.id.nav_history, R.string.history, Icons.Default.History),
|
||||||
|
Triple(R.id.nav_downloads, R.string.downloads, Icons.Default.Download),
|
||||||
|
Triple(R.id.nav_settings, R.string.settings, Icons.Default.Settings),
|
||||||
|
)
|
||||||
|
|
||||||
class MainActivity : EhActivity() {
|
class MainActivity : EhActivity() {
|
||||||
private lateinit var binding: ActivityMainBinding
|
|
||||||
private lateinit var navController: NavController
|
private lateinit var navController: NavController
|
||||||
|
private val isInitializedFlow = MutableStateFlow(false)
|
||||||
|
|
||||||
private fun saveImageToTempFile(uri: Uri): File? {
|
private fun saveImageToTempFile(uri: Uri): File? {
|
||||||
val bitmap = runCatching {
|
val bitmap = runCatching {
|
||||||
@@ -97,17 +148,15 @@ class MainActivity : EhActivity() {
|
|||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
lifecycleScope.launchUI {
|
lifecycleScope.launchUI {
|
||||||
|
isInitializedFlow.first { it }
|
||||||
if (!handleIntent(intent)) {
|
if (!handleIntent(intent)) {
|
||||||
if (intent != null && Intent.ACTION_VIEW == intent.action) {
|
if (intent != null && Intent.ACTION_VIEW == intent.action) {
|
||||||
if (intent.data != null) {
|
if (intent.data != null) {
|
||||||
val url = intent.data.toString()
|
val url = intent.data.toString()
|
||||||
EditTextDialogBuilder(this@MainActivity, url, "")
|
EditTextDialogBuilder(this@MainActivity, url, "")
|
||||||
.setTitle(R.string.error_cannot_parse_the_url)
|
.setTitle(R.string.error_cannot_parse_the_url)
|
||||||
.setPositiveButton(android.R.string.copy) { _: DialogInterface?, _: Int ->
|
.setPositiveButton(android.R.string.copy) { _, _ ->
|
||||||
this@MainActivity.addTextToClipboard(
|
this@MainActivity.addTextToClipboard(url)
|
||||||
url,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
@@ -161,58 +210,106 @@ class MainActivity : EhActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setNavGraph() {
|
var drawerLocked by mutableStateOf(false)
|
||||||
navController.apply {
|
private var openDrawerFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
graph = navInflater.inflate(R.navigation.nav_graph).apply {
|
private var recomposeFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
when (
|
|
||||||
when (val value = launchPage) {
|
fun recompose() {
|
||||||
0 -> GalleryListScene.ACTION_HOMEPAGE
|
recomposeFlow.tryEmit(Unit)
|
||||||
1 -> GalleryListScene.ACTION_SUBSCRIPTION
|
|
||||||
2 -> GalleryListScene.ACTION_WHATS_HOT
|
|
||||||
3 -> GalleryListScene.ACTION_TOP_LIST
|
|
||||||
else -> throw IllegalStateException("Unexpected value: $value")
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
GalleryListScene.ACTION_HOMEPAGE -> setStartDestination(R.id.nav_homepage)
|
|
||||||
GalleryListScene.ACTION_SUBSCRIPTION -> setStartDestination(R.id.nav_subscription)
|
|
||||||
GalleryListScene.ACTION_WHATS_HOT -> setStartDestination(R.id.nav_whats_hot)
|
|
||||||
GalleryListScene.ACTION_TOP_LIST -> setStartDestination(R.id.nav_toplist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
setContentView(binding.root)
|
setMD3Content {
|
||||||
val navHostFragment =
|
val configuration = LocalConfiguration.current
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
navController = navHostFragment.navController
|
val scope = rememberCoroutineScope()
|
||||||
if (!EhUtils.needSignedIn()) setNavGraph()
|
val recomposeScope = currentRecomposeScope
|
||||||
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
|
fun isSelected(id: Int) = ::navController.isInitialized && id == navController.currentDestination?.id
|
||||||
binding.navView.setupWithNavController(navController)
|
fun closeDrawer() = scope.launch { drawerState.close() }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
// Trick: Tweak NavigationUI to disable multiple backstack
|
openDrawerFlow.collect {
|
||||||
binding.navView.setNavigationItemSelectedListener {
|
drawerState.open()
|
||||||
val navigationView = binding.navView
|
}
|
||||||
val handled = navigationView.checkedItem?.itemId == it.itemId || onNavDestinationSelected2(it, navController)
|
}
|
||||||
if (handled) {
|
LaunchedEffect(Unit) {
|
||||||
val parent = navigationView.parent
|
recomposeFlow.collect {
|
||||||
if (parent is Openable) {
|
recomposeScope.invalidate()
|
||||||
parent.close()
|
}
|
||||||
} else {
|
}
|
||||||
@SuppressLint("RestrictedApi")
|
BackHandler(drawerState.isOpen) {
|
||||||
val bottomSheetBehavior = NavigationUI.findBottomSheetBehavior(navigationView)
|
closeDrawer()
|
||||||
if (bottomSheetBehavior != null) {
|
}
|
||||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
ModalNavigationDrawer(
|
||||||
|
drawerContent = {
|
||||||
|
ModalDrawerSheet(
|
||||||
|
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 56).dp),
|
||||||
|
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(scrollState).navigationBarsPadding(),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.sadpanda_low_poly),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
)
|
||||||
|
navItems.forEach { (id, stringId, icon) ->
|
||||||
|
NavigationDrawerItem(
|
||||||
|
label = {
|
||||||
|
Text(text = stringResource(id = stringId))
|
||||||
|
},
|
||||||
|
selected = isSelected(id),
|
||||||
|
onClick = {
|
||||||
|
closeDrawer()
|
||||||
|
onNavDestinationSelected2(id, navController)
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
|
icon = {
|
||||||
|
Icon(imageVector = icon, contentDescription = null)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
drawerState = drawerState,
|
||||||
|
gesturesEnabled = !drawerLocked || drawerState.isOpen,
|
||||||
|
) {
|
||||||
|
val insets = buildWindowInsets {
|
||||||
|
set(
|
||||||
|
WindowInsetsCompat.Type.statusBars(),
|
||||||
|
WindowInsets.statusBars,
|
||||||
|
)
|
||||||
|
set(
|
||||||
|
WindowInsetsCompat.Type.navigationBars(),
|
||||||
|
WindowInsets.navigationBars,
|
||||||
|
)
|
||||||
|
set(
|
||||||
|
WindowInsetsCompat.Type.ime(),
|
||||||
|
WindowInsets.ime,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AndroidViewBinding(factory = { inflater, parent, attachToParent ->
|
||||||
|
ActivityMainBinding.inflate(inflater, parent, attachToParent).apply {
|
||||||
|
val navHostFragment = fragmentContainer.getFragment<NavHostFragment>()
|
||||||
|
navController = navHostFragment.navController.apply {
|
||||||
|
graph = navInflater.inflate(R.navigation.nav_graph).apply {
|
||||||
|
check(launchPage in 0..3)
|
||||||
|
setStartDestination(navItems[launchPage].first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isInitializedFlow.value = true
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
ViewCompat.dispatchApplyWindowInsets(root, insets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handled
|
|
||||||
}
|
}
|
||||||
// Trick End
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
if (intent.action != Intent.ACTION_MAIN) {
|
if (intent.action != Intent.ACTION_MAIN) {
|
||||||
@@ -220,7 +317,10 @@ class MainActivity : EhActivity() {
|
|||||||
}
|
}
|
||||||
checkDownloadLocation()
|
checkDownloadLocation()
|
||||||
if (Settings.meteredNetworkWarning) {
|
if (Settings.meteredNetworkWarning) {
|
||||||
checkMeteredNetwork()
|
lifecycleScope.launchUI {
|
||||||
|
isInitializedFlow.first { it }
|
||||||
|
checkMeteredNetwork()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
if (!Settings.appLinkVerifyTip) {
|
if (!Settings.appLinkVerifyTip) {
|
||||||
@@ -282,7 +382,7 @@ class MainActivity : EhActivity() {
|
|||||||
if (connectivityManager.isActiveNetworkMetered) {
|
if (connectivityManager.isActiveNetworkMetered) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
binding.drawView,
|
findViewById(R.id.fragment_container),
|
||||||
R.string.metered_network_warning,
|
R.string.metered_network_warning,
|
||||||
Snackbar.LENGTH_LONG,
|
Snackbar.LENGTH_LONG,
|
||||||
)
|
)
|
||||||
@@ -298,25 +398,19 @@ class MainActivity : EhActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val loginLauncher =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
setNavGraph()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
if (EhUtils.needSignedIn()) {
|
if (Settings.needSignIn) {
|
||||||
loginLauncher.launch(Intent(this, ConfigureActivity::class.java))
|
startActivity(Intent(this, ConfigureActivity::class.java))
|
||||||
}
|
}
|
||||||
super.onResume()
|
super.onResume()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
delay(300)
|
delay(300)
|
||||||
mDrawerOnBackPressedCallback.remove()
|
|
||||||
onBackPressedDispatcher.addCallback(mDrawerOnBackPressedCallback)
|
|
||||||
checkClipboardUrl()
|
checkClipboardUrl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkClipboardUrl() {
|
private suspend fun checkClipboardUrl() {
|
||||||
|
isInitializedFlow.first { it }
|
||||||
val text = clipboardManager.getUrlFromClipboard(this)
|
val text = clipboardManager.getUrlFromClipboard(this)
|
||||||
val hashCode = text?.hashCode() ?: 0
|
val hashCode = text?.hashCode() ?: 0
|
||||||
if (text != null && hashCode != 0 && Settings.clipboardTextHashCode != hashCode) {
|
if (text != null && hashCode != 0 && Settings.clipboardTextHashCode != hashCode) {
|
||||||
@@ -341,7 +435,7 @@ class MainActivity : EhActivity() {
|
|||||||
launch?.let {
|
launch?.let {
|
||||||
withUIContext {
|
withUIContext {
|
||||||
val snackbar = Snackbar.make(
|
val snackbar = Snackbar.make(
|
||||||
binding.drawView,
|
findViewById(R.id.fragment_container),
|
||||||
R.string.clipboard_gallery_url_snack_message,
|
R.string.clipboard_gallery_url_snack_message,
|
||||||
Snackbar.LENGTH_SHORT,
|
Snackbar.LENGTH_SHORT,
|
||||||
)
|
)
|
||||||
@@ -355,42 +449,8 @@ class MainActivity : EhActivity() {
|
|||||||
Settings.clipboardTextHashCode = hashCode
|
Settings.clipboardTextHashCode = hashCode
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addAboveSnackView(view: View) {
|
fun openDrawer() {
|
||||||
binding.absnacker.addAboveSnackView(view)
|
openDrawerFlow.tryEmit(Unit)
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAboveSnackView(view: View) {
|
|
||||||
binding.absnacker.removeAboveSnackView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {
|
|
||||||
binding.drawView.setDrawerLockMode(lockMode, edgeGravity)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDrawerLockMode(edgeGravity: Int): Int {
|
|
||||||
return binding.drawView.getDrawerLockMode(edgeGravity)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDrawerOpen(drawerGravity: Int) = binding.drawView.isDrawerOpen(drawerGravity)
|
|
||||||
|
|
||||||
fun openDrawer(drawerGravity: Int) {
|
|
||||||
binding.drawView.openDrawer(drawerGravity)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeDrawer(drawerGravity: Int) {
|
|
||||||
binding.drawView.closeDrawer(drawerGravity)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleDrawer(drawerGravity: Int) {
|
|
||||||
if (isDrawerOpen(drawerGravity)) {
|
|
||||||
closeDrawer(drawerGravity)
|
|
||||||
} else {
|
|
||||||
openDrawer(drawerGravity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearNavCheckedItem() {
|
|
||||||
binding.navView.setCheckedItem(R.id.nav_stub)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
|
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
|
||||||
@@ -401,17 +461,19 @@ class MainActivity : EhActivity() {
|
|||||||
* If activity is running, show snack bar, otherwise show toast
|
* If activity is running, show snack bar, otherwise show toast
|
||||||
*/
|
*/
|
||||||
fun showTip(message: CharSequence, length: Int, useToast: Boolean = false) {
|
fun showTip(message: CharSequence, length: Int, useToast: Boolean = false) {
|
||||||
findViewById<View>(R.id.snackbar)?.takeUnless { useToast }?.apply {
|
if (!useToast) {
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
this,
|
findViewById(R.id.fragment_container),
|
||||||
message,
|
message,
|
||||||
if (length == BaseScene.LENGTH_LONG) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT,
|
if (length == BaseScene.LENGTH_LONG) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT,
|
||||||
).show()
|
).show()
|
||||||
} ?: Toast.makeText(
|
} else {
|
||||||
this,
|
Toast.makeText(
|
||||||
message,
|
this,
|
||||||
if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT,
|
message,
|
||||||
).show()
|
if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mShareUrl: String? = null
|
var mShareUrl: String? = null
|
||||||
@@ -419,20 +481,4 @@ class MainActivity : EhActivity() {
|
|||||||
super.onProvideAssistContent(outContent)
|
super.onProvideAssistContent(outContent)
|
||||||
mShareUrl?.let { outContent?.webUri = Uri.parse(mShareUrl) }
|
mShareUrl?.let { outContent?.webUri = Uri.parse(mShareUrl) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mDrawerOnBackPressedCallback =
|
|
||||||
object : OnBackPressedCallback(false), DrawerLayout.DrawerListener {
|
|
||||||
val slideThreshold = 0.05
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
binding.drawView.closeDrawers()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
|
|
||||||
isEnabled = slideOffset > slideThreshold
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawerOpened(drawerView: View) {}
|
|
||||||
override fun onDrawerClosed(drawerView: View) {}
|
|
||||||
override fun onDrawerStateChanged(newState: Int) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
package com.hippo.ehviewer.ui.legacy
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout.AttachedBehavior
|
|
||||||
import androidx.core.util.ObjectsCompat
|
|
||||||
import androidx.core.view.GravityCompat
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
|
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
|
||||||
import com.drakeet.drawer.FullDraggableHelper
|
|
||||||
import com.google.android.material.snackbar.Snackbar.SnackbarLayout
|
|
||||||
import com.hippo.ehviewer.util.dp2px
|
|
||||||
|
|
||||||
class AboveSnackerLayout : FrameLayout, AttachedBehavior, FullDraggableHelper.Callback {
|
|
||||||
private val helper: FullDraggableHelper
|
|
||||||
private var drawerLayout: DrawerLayout? = null
|
|
||||||
private var mAboveSnackViewList: MutableList<View>? = null
|
|
||||||
|
|
||||||
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {
|
|
||||||
helper = FullDraggableHelper(context, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) :
|
|
||||||
super(context!!, attrs, defStyle) {
|
|
||||||
helper = FullDraggableHelper(context, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
|
||||||
super.onAttachedToWindow()
|
|
||||||
ensureDrawerLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureDrawerLayout() {
|
|
||||||
val parent = parent.parent
|
|
||||||
check(parent is DrawerLayout) { "This $this must be added to a DrawerLayout" }
|
|
||||||
drawerLayout = parent
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
return helper.onInterceptTouchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
return helper.onTouchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDrawerMainContainer(): View {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isDrawerOpen(gravity: Int): Boolean {
|
|
||||||
return drawerLayout!!.isDrawerOpen(gravity)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasEnabledDrawer(gravity: Int): Boolean {
|
|
||||||
return drawerLayout!!.getDrawerLockMode(gravity) == DrawerLayout.LOCK_MODE_UNLOCKED &&
|
|
||||||
findDrawerWithGravity(gravity) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun offsetDrawer(gravity: Int, offset: Float) {
|
|
||||||
setDrawerToOffset(gravity, offset)
|
|
||||||
drawerLayout!!.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun smoothOpenDrawer(gravity: Int) {
|
|
||||||
drawerLayout!!.openDrawer(gravity, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun smoothCloseDrawer(gravity: Int) {
|
|
||||||
drawerLayout!!.closeDrawer(gravity, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawerDragging() {
|
|
||||||
val drawerListeners = drawerListeners
|
|
||||||
if (drawerListeners != null) {
|
|
||||||
val listenerCount = drawerListeners.size
|
|
||||||
for (i in listenerCount - 1 downTo 0) {
|
|
||||||
drawerListeners[i].onDrawerStateChanged(DrawerLayout.STATE_DRAGGING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val drawerListeners: List<DrawerListener>?
|
|
||||||
get() = try {
|
|
||||||
val listeners = DrawerLayout::class.java.getDeclaredField("mListeners")
|
|
||||||
listeners.isAccessible = true
|
|
||||||
listeners[drawerLayout] as List<DrawerListener>
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// throw to let developer know the api is changed
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setDrawerToOffset(gravity: Int, offset: Float) {
|
|
||||||
val drawerView = findDrawerWithGravity(gravity)
|
|
||||||
val slideOffsetPercent =
|
|
||||||
(offset / ObjectsCompat.requireNonNull(drawerView).width).coerceIn(0f, 1f)
|
|
||||||
try {
|
|
||||||
val method = DrawerLayout::class.java.getDeclaredMethod(
|
|
||||||
"moveDrawerToOffset",
|
|
||||||
View::class.java,
|
|
||||||
Float::class.javaPrimitiveType,
|
|
||||||
)
|
|
||||||
method.isAccessible = true
|
|
||||||
method.invoke(drawerLayout, drawerView, slideOffsetPercent)
|
|
||||||
drawerView!!.visibility = VISIBLE
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// throw to let developer know the api is changed
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copied from DrawerLayout
|
|
||||||
private fun findDrawerWithGravity(gravity: Int): View? {
|
|
||||||
val absHorizontalGravity = GravityCompat.getAbsoluteGravity(
|
|
||||||
gravity,
|
|
||||||
ViewCompat.getLayoutDirection(drawerLayout!!),
|
|
||||||
) and Gravity.HORIZONTAL_GRAVITY_MASK
|
|
||||||
val childCount = drawerLayout!!.childCount
|
|
||||||
for (i in 0 until childCount) {
|
|
||||||
val child = drawerLayout!!.getChildAt(i)
|
|
||||||
val childAbsGravity = getDrawerViewAbsoluteGravity(child)
|
|
||||||
if (childAbsGravity and Gravity.HORIZONTAL_GRAVITY_MASK == absHorizontalGravity) {
|
|
||||||
return child
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copied from DrawerLayout
|
|
||||||
private fun getDrawerViewAbsoluteGravity(drawerView: View): Int {
|
|
||||||
val gravity = (drawerView.layoutParams as DrawerLayout.LayoutParams).gravity
|
|
||||||
return GravityCompat.getAbsoluteGravity(
|
|
||||||
gravity,
|
|
||||||
ViewCompat.getLayoutDirection(drawerLayout!!),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addAboveSnackView(view: View) {
|
|
||||||
if (null == mAboveSnackViewList) {
|
|
||||||
mAboveSnackViewList = ArrayList()
|
|
||||||
}
|
|
||||||
mAboveSnackViewList!!.add(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAboveSnackView(view: View) {
|
|
||||||
if (null == mAboveSnackViewList) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mAboveSnackViewList!!.remove(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
val aboveSnackViewCount: Int
|
|
||||||
get() = if (null == mAboveSnackViewList) 0 else mAboveSnackViewList!!.size
|
|
||||||
|
|
||||||
fun getAboveSnackViewAt(index: Int): View? {
|
|
||||||
return if (null == mAboveSnackViewList || index < 0 || index >= mAboveSnackViewList!!.size) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
mAboveSnackViewList!![index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getBehavior(): Behavior {
|
|
||||||
return Behavior()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Behavior : CoordinatorLayout.Behavior<AboveSnackerLayout>() {
|
|
||||||
override fun layoutDependsOn(
|
|
||||||
parent: CoordinatorLayout,
|
|
||||||
child: AboveSnackerLayout,
|
|
||||||
dependency: View,
|
|
||||||
): Boolean {
|
|
||||||
return dependency is SnackbarLayout
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDependentViewChanged(
|
|
||||||
parent: CoordinatorLayout,
|
|
||||||
child: AboveSnackerLayout,
|
|
||||||
dependency: View,
|
|
||||||
): Boolean {
|
|
||||||
for (i in 0 until child.aboveSnackViewCount) {
|
|
||||||
val view = child.getAboveSnackViewAt(i)
|
|
||||||
if (view != null) {
|
|
||||||
val translationY =
|
|
||||||
(dependency.translationY - dependency.height - dp2px(view.context, 8f))
|
|
||||||
.coerceAtMost(0f)
|
|
||||||
ViewCompat.animate(view).setInterpolator(FastOutSlowInInterpolator())
|
|
||||||
.translationY(translationY).setDuration(150).start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDependentViewRemoved(
|
|
||||||
parent: CoordinatorLayout,
|
|
||||||
child: AboveSnackerLayout,
|
|
||||||
dependency: View,
|
|
||||||
) {
|
|
||||||
for (i in 0 until child.aboveSnackViewCount) {
|
|
||||||
val view = child.getAboveSnackViewAt(i)
|
|
||||||
if (view != null) {
|
|
||||||
ViewCompat.animate(view).setInterpolator(FastOutSlowInInterpolator())
|
|
||||||
.translationY(0f).setDuration(75).start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,8 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.TabRow
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||||
|
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
@@ -45,6 +47,7 @@ import com.hippo.ehviewer.ui.main.ImageSearch
|
|||||||
import com.hippo.ehviewer.ui.main.NormalSearch
|
import com.hippo.ehviewer.ui.main.NormalSearch
|
||||||
import com.hippo.ehviewer.ui.main.SearchAdvanced
|
import com.hippo.ehviewer.ui.main.SearchAdvanced
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
|
import com.hippo.ehviewer.util.findActivity
|
||||||
import com.hippo.ehviewer.util.pickVisualMedia
|
import com.hippo.ehviewer.util.pickVisualMedia
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -71,6 +74,7 @@ class SearchLayout @JvmOverloads constructor(
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val windowSizeClass = calculateWindowSizeClass(activity = context.findActivity())
|
||||||
fun selectImage() = coroutineScope.launch {
|
fun selectImage() = coroutineScope.launch {
|
||||||
context.pickVisualMedia(ActivityResultContracts.PickVisualMedia.ImageOnly)?.let {
|
context.pickVisualMedia(ActivityResultContracts.PickVisualMedia.ImageOnly)?.let {
|
||||||
vm.path = it.toString()
|
vm.path = it.toString()
|
||||||
@@ -93,6 +97,11 @@ class SearchLayout @JvmOverloads constructor(
|
|||||||
isAdvanced = vm.isAdvancedMode,
|
isAdvanced = vm.isAdvancedMode,
|
||||||
onAdvancedChanged = { vm.isAdvancedMode = it },
|
onAdvancedChanged = { vm.isAdvancedMode = it },
|
||||||
showInfo = { BaseDialogBuilder(context).setMessage(R.string.search_tip).show() },
|
showInfo = { BaseDialogBuilder(context).setMessage(R.string.search_tip).show() },
|
||||||
|
maxItemsInEachRow = when (windowSizeClass.widthSizeClass) {
|
||||||
|
WindowWidthSizeClass.Compact -> 2
|
||||||
|
WindowWidthSizeClass.Medium -> 3
|
||||||
|
else -> 5
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ import com.hippo.ehviewer.util.ExceptionUtils
|
|||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import java.util.Locale
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CookieSignInScene(windowSizeClass: WindowSizeClass) {
|
fun CookieSignInScene(windowSizeClass: WindowSizeClass) {
|
||||||
@@ -101,7 +101,6 @@ fun CookieSignInScene(windowSizeClass: WindowSizeClass) {
|
|||||||
EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_E))
|
EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_E))
|
||||||
EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_EX))
|
EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_EX))
|
||||||
if (igneous.isNotBlank()) EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX))
|
if (igneous.isNotBlank()) EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX))
|
||||||
EhCookieStore.flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun login() {
|
fun login() {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ suspend fun postLogin() = coroutineScope {
|
|||||||
launch {
|
launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
EhEngine.getUConfig(EhUrl.URL_UCONFIG_E)
|
EhEngine.getUConfig(EhUrl.URL_UCONFIG_E)
|
||||||
|
EhCookieStore.flush()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
it.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -36,6 +37,7 @@ suspend fun postLogin() = coroutineScope {
|
|||||||
Settings.gallerySite = EhUrl.SITE_EX
|
Settings.gallerySite = EhUrl.SITE_EX
|
||||||
// Explicitly use ex url since https://github.com/Ehviewer-Overhauled/Ehviewer/issues/1239#issuecomment-1632584525
|
// Explicitly use ex url since https://github.com/Ehviewer-Overhauled/Ehviewer/issues/1239#issuecomment-1632584525
|
||||||
EhEngine.getUConfig(EhUrl.URL_UCONFIG_EX)
|
EhEngine.getUConfig(EhUrl.URL_UCONFIG_EX)
|
||||||
|
EhCookieStore.flush()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Settings.gallerySite = EhUrl.SITE_E
|
Settings.gallerySite = EhUrl.SITE_E
|
||||||
Settings.needSignIn = false
|
Settings.needSignIn = false
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -35,6 +34,8 @@ import com.hippo.ehviewer.R
|
|||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
|
import com.hippo.ehviewer.icons.big.SadAndroid
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GalleryDetailHeaderInfoCard(
|
fun GalleryDetailHeaderInfoCard(
|
||||||
@@ -160,7 +161,7 @@ fun GalleryDetailErrorTip(error: String, onClick: () -> Unit) = Column(
|
|||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.big_sad_pandroid),
|
imageVector = EhIcons.Big.Default.SadAndroid,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.clickable(onClick = onClick),
|
modifier = Modifier.clickable(onClick = onClick),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.hippo.ehviewer.ui.main
|
package com.hippo.ehviewer.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Help
|
import androidx.compose.material.icons.automirrored.filled.Help
|
||||||
@@ -55,16 +57,24 @@ fun NormalSearch(
|
|||||||
isAdvanced: Boolean,
|
isAdvanced: Boolean,
|
||||||
onAdvancedChanged: (Boolean) -> Unit,
|
onAdvancedChanged: (Boolean) -> Unit,
|
||||||
showInfo: () -> Unit,
|
showInfo: () -> Unit,
|
||||||
|
maxItemsInEachRow: Int,
|
||||||
) {
|
) {
|
||||||
FlowRow(modifier = Modifier.wrapContentHeight(align = Alignment.Top)) {
|
FlowRow(
|
||||||
|
modifier = Modifier.wrapContentHeight(align = Alignment.Top),
|
||||||
|
maxItemsInEachRow = maxItemsInEachRow,
|
||||||
|
) {
|
||||||
categoryTable.forEach {
|
categoryTable.forEach {
|
||||||
val selected = category and it.first != 0
|
val selected = category and it.first != 0
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = { onCategoryChanged(if (selected) category xor it.first else category or it.first) },
|
onClick = { onCategoryChanged(if (selected) category xor it.first else category or it.first) },
|
||||||
label = { Text(text = stringResource(id = it.second)) },
|
label = { Text(text = stringResource(id = it.second)) },
|
||||||
leadingIcon = { if (selected) Icon(imageVector = Icons.Default.Check, contentDescription = null) },
|
leadingIcon = {
|
||||||
modifier = Modifier.padding(horizontal = 4.dp).align(alignment = Alignment.CenterVertically),
|
AnimatedVisibility(selected) {
|
||||||
|
Icon(imageVector = Icons.Default.Check, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp).align(alignment = Alignment.CenterVertically).widthIn(min = 145.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ import android.util.SparseArray
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.view.GravityCompat
|
|
||||||
import androidx.core.view.SoftwareKeyboardControllerCompat
|
import androidx.core.view.SoftwareKeyboardControllerCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelStoreOwner
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||||
@@ -41,60 +39,20 @@ abstract class BaseScene : Fragment() {
|
|||||||
private var drawerView: View? = null
|
private var drawerView: View? = null
|
||||||
private var drawerViewState: SparseArray<Parcelable>? = null
|
private var drawerViewState: SparseArray<Parcelable>? = null
|
||||||
private var sideSheetDialog: SideSheetDialog? = null
|
private var sideSheetDialog: SideSheetDialog? = null
|
||||||
fun addAboveSnackView(view: View) {
|
|
||||||
val activity = activity
|
val isDrawerLocked
|
||||||
if (activity is MainActivity) {
|
get() = mainActivity?.drawerLocked == true
|
||||||
activity.addAboveSnackView(view)
|
|
||||||
}
|
fun lockDrawer() {
|
||||||
|
mainActivity?.drawerLocked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAboveSnackView(view: View) {
|
fun unlockDrawer() {
|
||||||
val activity = activity
|
mainActivity?.drawerLocked = false
|
||||||
if (activity is MainActivity) {
|
|
||||||
activity.removeAboveSnackView(view)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {
|
fun openDrawer() {
|
||||||
val activity = activity
|
mainActivity?.openDrawer()
|
||||||
if (activity is MainActivity) {
|
|
||||||
activity.setDrawerLockMode(lockMode, edgeGravity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDrawerLockMode(edgeGravity: Int): Int? {
|
|
||||||
val activity = activity
|
|
||||||
return if (activity is MainActivity) {
|
|
||||||
activity.getDrawerLockMode(edgeGravity)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDrawerOpen(drawerGravity: Int): Boolean {
|
|
||||||
val activity = activity
|
|
||||||
return activity is MainActivity && activity.isDrawerOpen(drawerGravity)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openDrawer(drawerGravity: Int) {
|
|
||||||
val activity = activity
|
|
||||||
if (activity is MainActivity) {
|
|
||||||
activity.openDrawer(drawerGravity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeDrawer(drawerGravity: Int) {
|
|
||||||
val activity = activity
|
|
||||||
if (activity is MainActivity) {
|
|
||||||
activity.closeDrawer(drawerGravity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleDrawer(drawerGravity: Int) {
|
|
||||||
val activity = activity
|
|
||||||
if (activity is MainActivity) {
|
|
||||||
activity.toggleDrawer(drawerGravity)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openSideSheet() {
|
fun openSideSheet() {
|
||||||
@@ -107,20 +65,14 @@ abstract class BaseScene : Fragment() {
|
|||||||
fun closeSideSheet() = sideSheetDialog!!.hide()
|
fun closeSideSheet() = sideSheetDialog!!.hide()
|
||||||
|
|
||||||
fun showTip(message: CharSequence?, length: Int) {
|
fun showTip(message: CharSequence?, length: Int) {
|
||||||
val activity = activity
|
mainActivity?.showTip(message!!, length)
|
||||||
if (activity is MainActivity) {
|
|
||||||
activity.showTip(message!!, length)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showTip(@StringRes id: Int, length: Int) {
|
fun showTip(@StringRes id: Int, length: Int) {
|
||||||
val activity = activity
|
mainActivity?.showTip(id, length)
|
||||||
if (activity is MainActivity) {
|
|
||||||
activity.showTip(id, length)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open val showLeftDrawer = true
|
open val enableDrawerGestures = false
|
||||||
|
|
||||||
private fun createDrawerView(savedInstanceState: Bundle?): View? {
|
private fun createDrawerView(savedInstanceState: Bundle?): View? {
|
||||||
drawerView = onCreateDrawerView(layoutInflater)
|
drawerView = onCreateDrawerView(layoutInflater)
|
||||||
@@ -156,10 +108,10 @@ abstract class BaseScene : Fragment() {
|
|||||||
view.background = requireActivity().theme.resolveDrawable(android.R.attr.windowBackground)
|
view.background = requireActivity().theme.resolveDrawable(android.R.attr.windowBackground)
|
||||||
|
|
||||||
// Update left drawer locked state
|
// Update left drawer locked state
|
||||||
if (showLeftDrawer) {
|
if (enableDrawerGestures) {
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
|
unlockDrawer()
|
||||||
} else {
|
} else {
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
|
lockDrawer()
|
||||||
}
|
}
|
||||||
hideSoftInput()
|
hideSoftInput()
|
||||||
createDrawerView(savedInstanceState)?.let {
|
createDrawerView(savedInstanceState)?.let {
|
||||||
@@ -176,6 +128,7 @@ abstract class BaseScene : Fragment() {
|
|||||||
setContentView(it)
|
setContentView(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mainActivity?.recompose()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -184,14 +137,7 @@ abstract class BaseScene : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val mainActivity: MainActivity?
|
val mainActivity: MainActivity?
|
||||||
get() {
|
get() = activity as? MainActivity
|
||||||
val activity = activity
|
|
||||||
return if (activity is MainActivity) {
|
|
||||||
activity
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideSoftInput() = activity?.window?.decorView?.run { SoftwareKeyboardControllerCompat(this) }?.hide()
|
fun hideSoftInput() = activity?.window?.decorView?.run { SoftwareKeyboardControllerCompat(this) }?.hide()
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.hippo.ehviewer.R
|
|
||||||
import com.hippo.ehviewer.databinding.SceneToolbarBinding
|
import com.hippo.ehviewer.databinding.SceneToolbarBinding
|
||||||
|
|
||||||
abstract class BaseToolbarScene : BaseScene(), ToolBarScene {
|
abstract class BaseToolbarScene : BaseScene(), ToolBarScene {
|
||||||
@@ -61,12 +60,7 @@ abstract class BaseToolbarScene : BaseScene(), ToolBarScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
return if (item.itemId == R.id.action_open_side_sheet) {
|
return false
|
||||||
openSideSheet()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationClick() {
|
override fun onNavigationClick() {
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.GravityCompat
|
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
@@ -81,6 +79,7 @@ import com.hippo.ehviewer.databinding.ItemDownloadBinding
|
|||||||
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
|
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
|
||||||
import com.hippo.ehviewer.databinding.SceneDownloadBinding
|
import com.hippo.ehviewer.databinding.SceneDownloadBinding
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
|
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||||
import com.hippo.ehviewer.download.DownloadService
|
import com.hippo.ehviewer.download.DownloadService
|
||||||
import com.hippo.ehviewer.download.DownloadService.Companion.clear
|
import com.hippo.ehviewer.download.DownloadService.Companion.clear
|
||||||
import com.hippo.ehviewer.download.downloadDir
|
import com.hippo.ehviewer.download.downloadDir
|
||||||
@@ -115,7 +114,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import rikka.core.res.resolveColor
|
import rikka.core.res.resolveColor
|
||||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
|
||||||
|
|
||||||
@SuppressLint("RtlHardcoded")
|
@SuppressLint("RtlHardcoded")
|
||||||
class DownloadsScene :
|
class DownloadsScene :
|
||||||
@@ -145,6 +143,9 @@ class DownloadsScene :
|
|||||||
private val dialogState = DialogState()
|
private val dialogState = DialogState()
|
||||||
|
|
||||||
override val fabLayout get() = binding.fabLayout
|
override val fabLayout get() = binding.fabLayout
|
||||||
|
override val fastScroller get() = binding.fastScroller
|
||||||
|
override val recyclerView get() = binding.recyclerView
|
||||||
|
override val contentView get() = binding.content
|
||||||
|
|
||||||
private fun initLabels() {
|
private fun initLabels() {
|
||||||
context ?: return
|
context ?: return
|
||||||
@@ -267,12 +268,10 @@ class DownloadsScene :
|
|||||||
).apply {
|
).apply {
|
||||||
addCustomChoiceListener({
|
addCustomChoiceListener({
|
||||||
binding.fabLayout.isExpanded = true
|
binding.fabLayout.isExpanded = true
|
||||||
// Lock drawer
|
lockDrawer()
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
|
|
||||||
}) {
|
}) {
|
||||||
binding.fabLayout.isExpanded = false
|
binding.fabLayout.isExpanded = false
|
||||||
// Unlock drawer
|
unlockDrawer()
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
|
|
||||||
}
|
}
|
||||||
restoreSelection(savedInstanceState)
|
restoreSelection(savedInstanceState)
|
||||||
}
|
}
|
||||||
@@ -346,7 +345,6 @@ class DownloadsScene :
|
|||||||
fabLayout.setHidePrimaryFab(true)
|
fabLayout.setHidePrimaryFab(true)
|
||||||
fabLayout.setAutoCancel(false)
|
fabLayout.setAutoCancel(false)
|
||||||
fabLayout.setOnClickFabListener(this@DownloadsScene)
|
fabLayout.setOnClickFabListener(this@DownloadsScene)
|
||||||
addAboveSnackView(fabLayout)
|
|
||||||
updateForLabel()
|
updateForLabel()
|
||||||
}
|
}
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
@@ -369,7 +367,6 @@ class DownloadsScene :
|
|||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
binding.recyclerView.stopScroll()
|
binding.recyclerView.stopScroll()
|
||||||
removeAboveSnackView(binding.fabLayout)
|
|
||||||
mViewTransition = null
|
mViewTransition = null
|
||||||
mAdapter = null
|
mAdapter = null
|
||||||
mLabelAdapter = null
|
mLabelAdapter = null
|
||||||
@@ -378,7 +375,7 @@ class DownloadsScene :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationClick() {
|
override fun onNavigationClick() {
|
||||||
toggleDrawer(GravityCompat.START)
|
openDrawer()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMenuResId(): Int {
|
override fun getMenuResId(): Int {
|
||||||
|
|||||||
@@ -33,14 +33,11 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.currentRecomposeScope
|
import androidx.compose.runtime.currentRecomposeScope
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.GravityCompat
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsAnimationCompat
|
import androidx.core.view.WindowInsetsAnimationCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -88,22 +85,23 @@ import com.hippo.ehviewer.util.lazyMut
|
|||||||
import com.hippo.ehviewer.util.setValue
|
import com.hippo.ehviewer.util.setValue
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import kotlinx.coroutines.Job
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
|
||||||
import rikka.core.res.resolveColor
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
import rikka.core.res.resolveColor
|
||||||
|
|
||||||
// Note that we do not really follow mvvm structure, just use it as ... storage
|
// Note that we do not really follow mvvm structure, just use it as ... storage
|
||||||
class VMStorage : ViewModel() {
|
class VMStorage : ViewModel() {
|
||||||
var urlBuilder = FavListUrlBuilder(favCat = Settings.recentFavCat)
|
var urlBuilder = FavListUrlBuilder(favCat = Settings.recentFavCat)
|
||||||
var shouldScrollToTop = false
|
|
||||||
private val cloudDataFlow = Pager(PagingConfig(25)) {
|
private val cloudDataFlow = Pager(PagingConfig(25)) {
|
||||||
object : PagingSource<String, BaseGalleryInfo>() {
|
object : PagingSource<String, BaseGalleryInfo>() {
|
||||||
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
|
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
|
||||||
@@ -120,7 +118,6 @@ class VMStorage : ViewModel() {
|
|||||||
} else {
|
} else {
|
||||||
urlBuilder.setIndex(key, false)
|
urlBuilder.setIndex(key, false)
|
||||||
}
|
}
|
||||||
shouldScrollToTop = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val r = runSuspendCatching {
|
val r = runSuspendCatching {
|
||||||
@@ -156,19 +153,14 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private var mAdapter: GalleryAdapter? = null
|
private var mAdapter: GalleryAdapter? = null
|
||||||
private val tracker get() = mAdapter!!.tracker!!
|
private val tracker get() = mAdapter!!.tracker!!
|
||||||
private val showNormalFabsRunnable = Runnable {
|
private var showNormalFabsJob: Job? = null
|
||||||
updateJumpFab() // index: 0, 2
|
|
||||||
binding.fabLayout.run {
|
|
||||||
setSecondaryFabVisibilityAt(1, true)
|
|
||||||
for (i in 3..6) {
|
|
||||||
setSecondaryFabVisibilityAt(i, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val dialogState = DialogState()
|
private val dialogState = DialogState()
|
||||||
|
|
||||||
override val fabLayout get() = binding.fabLayout
|
override val fabLayout get() = binding.fabLayout
|
||||||
|
override val fastScroller get() = binding.fastScroller
|
||||||
|
override val recyclerView get() = binding.recyclerView
|
||||||
|
override val contentView get() = binding.contentLayout.contentView
|
||||||
|
|
||||||
private fun onItemClick(position: Int) {
|
private fun onItemClick(position: Int) {
|
||||||
// Skip if in search mode
|
// Skip if in search mode
|
||||||
@@ -233,7 +225,7 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View {
|
): View {
|
||||||
_binding = SceneFavoritesBinding.inflate(inflater, container!!)
|
_binding = SceneFavoritesBinding.inflate(inflater, container!!)
|
||||||
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
|
container.addView(ComposeWithViewLifecycle().apply { setMD3Content { dialogState.Intercept() } })
|
||||||
setOnApplySearch {
|
setOnApplySearch {
|
||||||
if (!tracker.isInCustomChoice) {
|
if (!tracker.isInCustomChoice) {
|
||||||
switchFav(urlBuilder.favCat, it)
|
switchFav(urlBuilder.favCat, it)
|
||||||
@@ -309,7 +301,6 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
if (tracker.isInCustomChoice) tracker.clearSelection()
|
if (tracker.isInCustomChoice) tracker.clearSelection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addAboveSnackView(this)
|
|
||||||
}
|
}
|
||||||
binding.fastScroller.setOnDragHandlerListener(object : OnDragHandlerListener {
|
binding.fastScroller.setOnDragHandlerListener(object : OnDragHandlerListener {
|
||||||
override fun onStartDragHandler() {}
|
override fun onStartDragHandler() {}
|
||||||
@@ -336,6 +327,7 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||||
binding.tip.setCompoundDrawables(null, drawable, null, null)
|
binding.tip.setCompoundDrawables(null, drawable, null, null)
|
||||||
binding.tip.setOnClickListener { mAdapter?.retry() }
|
binding.tip.setOnClickListener { mAdapter?.retry() }
|
||||||
|
binding.refreshLayout.setProgressViewOffset(true, 0, 64.dpToPx)
|
||||||
binding.refreshLayout.setOnRefreshListener { switchFav(urlBuilder.favCat) }
|
binding.refreshLayout.setOnRefreshListener { switchFav(urlBuilder.favCat) }
|
||||||
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
|
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
|
||||||
val empty = getString(R.string.gallery_list_empty_hit)
|
val empty = getString(R.string.gallery_list_empty_hit)
|
||||||
@@ -345,10 +337,7 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
is LoadState.Loading -> {
|
is LoadState.Loading -> {
|
||||||
showSearchBar()
|
showSearchBar()
|
||||||
if (!binding.refreshLayout.isRefreshing) {
|
if (!binding.refreshLayout.isRefreshing) {
|
||||||
// https://github.com/FooIbar/EhViewer/issues/45
|
transition.showView(1)
|
||||||
// transition.showView(1)
|
|
||||||
transition.showView(0, false)
|
|
||||||
binding.refreshLayout.isRefreshing = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is LoadState.Error -> {
|
is LoadState.Error -> {
|
||||||
@@ -362,11 +351,7 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
binding.tip.text = empty
|
binding.tip.text = empty
|
||||||
transition.showView(2)
|
transition.showView(2)
|
||||||
} else {
|
} else {
|
||||||
transition.showView(0, false)
|
transition.showView(0)
|
||||||
if (vm.shouldScrollToTop) {
|
|
||||||
vm.shouldScrollToTop = false
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,15 +369,13 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
// Delay expanding action to make layout work fine
|
// Delay expanding action to make layout work fine
|
||||||
SimpleHandler.post { binding.fabLayout.isExpanded = true }
|
SimpleHandler.post { binding.fabLayout.isExpanded = true }
|
||||||
binding.refreshLayout.isEnabled = false
|
binding.refreshLayout.isEnabled = false
|
||||||
// Lock drawer
|
lockDrawer()
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
|
|
||||||
}) {
|
}) {
|
||||||
showNormalFab()
|
showNormalFab()
|
||||||
binding.fabLayout.setAutoCancel(true)
|
binding.fabLayout.setAutoCancel(true)
|
||||||
binding.fabLayout.isExpanded = false
|
binding.fabLayout.isExpanded = false
|
||||||
binding.refreshLayout.isEnabled = true
|
binding.refreshLayout.isEnabled = true
|
||||||
// Unlock drawer
|
unlockDrawer()
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
|
|
||||||
}
|
}
|
||||||
restoreSelection(savedInstanceState)
|
restoreSelection(savedInstanceState)
|
||||||
}
|
}
|
||||||
@@ -451,12 +434,12 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
binding.recyclerView.stopScroll()
|
binding.recyclerView.stopScroll()
|
||||||
removeAboveSnackView(binding.fabLayout)
|
|
||||||
mAdapter = null
|
mAdapter = null
|
||||||
_binding = null
|
_binding = null
|
||||||
|
showNormalFabsJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDrawerView(inflater: LayoutInflater) = ComposeView(inflater.context).apply {
|
override fun onCreateDrawerView(inflater: LayoutInflater) = ComposeWithViewLifecycle().apply {
|
||||||
setMD3Content {
|
setMD3Content {
|
||||||
val localFavCount by vm.localFavCount.collectAsState(0)
|
val localFavCount by vm.localFavCount.collectAsState(0)
|
||||||
ElevatedCard {
|
ElevatedCard {
|
||||||
@@ -532,12 +515,20 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
|
|
||||||
private fun showNormalFab() {
|
private fun showNormalFab() {
|
||||||
// Delay showing normal fab to avoid mutation
|
// Delay showing normal fab to avoid mutation
|
||||||
SimpleHandler.removeCallbacks(showNormalFabsRunnable)
|
showNormalFabsJob = viewLifecycleOwner.lifecycleScope.launch {
|
||||||
SimpleHandler.postDelayed(showNormalFabsRunnable, 300)
|
delay(300)
|
||||||
|
updateJumpFab() // index: 0, 2
|
||||||
|
binding.fabLayout.run {
|
||||||
|
setSecondaryFabVisibilityAt(1, true)
|
||||||
|
for (i in 3..6) {
|
||||||
|
setSecondaryFabVisibilityAt(i, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSelectionFab() {
|
private fun showSelectionFab() {
|
||||||
SimpleHandler.removeCallbacks(showNormalFabsRunnable)
|
showNormalFabsJob?.cancel()
|
||||||
binding.fabLayout.run {
|
binding.fabLayout.run {
|
||||||
for (i in 0..2) {
|
for (i in 0..2) {
|
||||||
setSecondaryFabVisibilityAt(i, false)
|
setSecondaryFabVisibilityAt(i, false)
|
||||||
|
|||||||
@@ -74,31 +74,15 @@ class GalleryAdapter(
|
|||||||
layoutManager.setStrategy(STRATEGY_MIN_SIZE)
|
layoutManager.setStrategy(STRATEGY_MIN_SIZE)
|
||||||
if (null != mGirdDecoration) {
|
if (null != mGirdDecoration) {
|
||||||
recyclerView.removeItemDecoration(mGirdDecoration!!)
|
recyclerView.removeItemDecoration(mGirdDecoration!!)
|
||||||
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_h)
|
|
||||||
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_v)
|
|
||||||
recyclerView.setPadding(
|
|
||||||
recyclerView.paddingLeft - paddingH,
|
|
||||||
recyclerView.paddingTop - paddingV,
|
|
||||||
recyclerView.paddingRight - paddingH,
|
|
||||||
recyclerView.paddingBottom - paddingV,
|
|
||||||
)
|
|
||||||
mGirdDecoration = null
|
mGirdDecoration = null
|
||||||
}
|
}
|
||||||
if (null == mListDecoration) {
|
if (null == mListDecoration) {
|
||||||
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
|
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
|
||||||
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)
|
|
||||||
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)
|
|
||||||
mListDecoration = object : ItemDecoration() {
|
mListDecoration = object : ItemDecoration() {
|
||||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
outRect.set(0, interval / 2, 0, interval / 2)
|
outRect.set(0, interval / 2, 0, interval / 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recyclerView.setPadding(
|
|
||||||
recyclerView.paddingLeft + paddingH,
|
|
||||||
recyclerView.paddingTop + paddingV,
|
|
||||||
recyclerView.paddingRight + paddingH,
|
|
||||||
recyclerView.paddingBottom + paddingV,
|
|
||||||
)
|
|
||||||
recyclerView.addItemDecoration(mListDecoration!!)
|
recyclerView.addItemDecoration(mListDecoration!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,27 +93,11 @@ class GalleryAdapter(
|
|||||||
layoutManager.setStrategy(STRATEGY_SUITABLE_SIZE)
|
layoutManager.setStrategy(STRATEGY_SUITABLE_SIZE)
|
||||||
if (null != mListDecoration) {
|
if (null != mListDecoration) {
|
||||||
recyclerView.removeItemDecoration(mListDecoration!!)
|
recyclerView.removeItemDecoration(mListDecoration!!)
|
||||||
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)
|
|
||||||
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)
|
|
||||||
recyclerView.setPadding(
|
|
||||||
recyclerView.paddingLeft - paddingH,
|
|
||||||
recyclerView.paddingTop - paddingV,
|
|
||||||
recyclerView.paddingRight - paddingH,
|
|
||||||
recyclerView.paddingBottom - paddingV,
|
|
||||||
)
|
|
||||||
mListDecoration = null
|
mListDecoration = null
|
||||||
}
|
}
|
||||||
if (null == mGirdDecoration) {
|
if (null == mGirdDecoration) {
|
||||||
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_grid_interval)
|
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_grid_interval)
|
||||||
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_h)
|
|
||||||
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_v)
|
|
||||||
mGirdDecoration = MarginItemDecoration(interval, 0, 0, 0, 0)
|
mGirdDecoration = MarginItemDecoration(interval, 0, 0, 0, 0)
|
||||||
recyclerView.setPadding(
|
|
||||||
recyclerView.paddingLeft + paddingH,
|
|
||||||
recyclerView.paddingTop + paddingV,
|
|
||||||
recyclerView.paddingRight + paddingH,
|
|
||||||
recyclerView.paddingBottom + paddingV,
|
|
||||||
)
|
|
||||||
recyclerView.addItemDecoration(mGirdDecoration!!)
|
recyclerView.addItemDecoration(mGirdDecoration!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,13 +87,15 @@ import com.hippo.ehviewer.util.ReadableTime
|
|||||||
import com.hippo.ehviewer.util.SimpleAnimatorListener
|
import com.hippo.ehviewer.util.SimpleAnimatorListener
|
||||||
import com.hippo.ehviewer.util.TextUrl
|
import com.hippo.ehviewer.util.TextUrl
|
||||||
import com.hippo.ehviewer.util.addTextToClipboard
|
import com.hippo.ehviewer.util.addTextToClipboard
|
||||||
|
import com.hippo.ehviewer.util.applyNavigationBarsPadding
|
||||||
import com.hippo.ehviewer.util.getParcelableCompat
|
import com.hippo.ehviewer.util.getParcelableCompat
|
||||||
import com.hippo.ehviewer.util.toBBCode
|
import com.hippo.ehviewer.util.toBBCode
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import kotlin.math.hypot
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import rikka.core.res.resolveColor
|
import rikka.core.res.resolveColor
|
||||||
import kotlin.math.hypot
|
|
||||||
|
|
||||||
class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefreshListener {
|
class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefreshListener {
|
||||||
private var _binding: SceneGalleryCommentsBinding? = null
|
private var _binding: SceneGalleryCommentsBinding? = null
|
||||||
@@ -256,10 +258,19 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.fab.setOnClickListener(this)
|
binding.fab.setOnClickListener(this)
|
||||||
addAboveSnackView(binding.editPanel)
|
|
||||||
addAboveSnackView(binding.fabLayout)
|
|
||||||
mViewTransition = ViewTransition(binding.refreshLayout, tip)
|
mViewTransition = ViewTransition(binding.refreshLayout, tip)
|
||||||
updateView(false)
|
updateView(false)
|
||||||
|
binding.editPanel.applyInsetter {
|
||||||
|
type(ime = true, navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.recyclerView.applyInsetter {
|
||||||
|
type(ime = true, navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.fabLayout.applyNavigationBarsPadding()
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,8 +293,6 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
callback.remove()
|
callback.remove()
|
||||||
binding.recyclerView.stopScroll()
|
binding.recyclerView.stopScroll()
|
||||||
removeAboveSnackView(binding.editPanel)
|
|
||||||
removeAboveSnackView(binding.fabLayout)
|
|
||||||
mAdapter = null
|
mAdapter = null
|
||||||
mViewTransition = null
|
mViewTransition = null
|
||||||
_binding = null
|
_binding = null
|
||||||
@@ -428,7 +437,7 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
|||||||
return@setItems
|
return@setItems
|
||||||
}
|
}
|
||||||
when (menuId[which]) {
|
when (menuId[which]) {
|
||||||
R.id.copy -> requireActivity().addTextToClipboard(text, false)
|
R.id.copy -> requireActivity().addTextToClipboard(text)
|
||||||
R.id.block_commenter -> showFilterCommenterDialog(comment.user, position)
|
R.id.block_commenter -> showFilterCommenterDialog(comment.user, position)
|
||||||
R.id.vote_up -> voteComment(comment.id, 1)
|
R.id.vote_up -> voteComment(comment.id, 1)
|
||||||
R.id.vote_down -> voteComment(comment.id, -1)
|
R.id.vote_down -> voteComment(comment.id, -1)
|
||||||
|
|||||||
@@ -91,12 +91,11 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.layout.LocalPinnableContainer
|
import androidx.compose.ui.layout.LocalPinnableContainer
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidViewBinding
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.text.parseAsHtml
|
import androidx.core.text.parseAsHtml
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -136,6 +135,7 @@ import com.hippo.ehviewer.databinding.DialogArchiveListBinding
|
|||||||
import com.hippo.ehviewer.databinding.DialogRateBinding
|
import com.hippo.ehviewer.databinding.DialogRateBinding
|
||||||
import com.hippo.ehviewer.databinding.DialogTorrentListBinding
|
import com.hippo.ehviewer.databinding.DialogTorrentListBinding
|
||||||
import com.hippo.ehviewer.databinding.ItemGalleryCommentBinding
|
import com.hippo.ehviewer.databinding.ItemGalleryCommentBinding
|
||||||
|
import com.hippo.ehviewer.download.DownloadManager as EhDownloadManager
|
||||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||||
import com.hippo.ehviewer.spider.SpiderQueen
|
import com.hippo.ehviewer.spider.SpiderQueen
|
||||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.MODE_READ
|
import com.hippo.ehviewer.spider.SpiderQueen.Companion.MODE_READ
|
||||||
@@ -172,6 +172,7 @@ import com.hippo.ehviewer.util.addTextToClipboard
|
|||||||
import com.hippo.ehviewer.util.getParcelableCompat
|
import com.hippo.ehviewer.util.getParcelableCompat
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@@ -179,8 +180,6 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import splitties.systemservices.downloadManager
|
import splitties.systemservices.downloadManager
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import com.hippo.ehviewer.download.DownloadManager as EhDownloadManager
|
|
||||||
|
|
||||||
class GalleryDetailScene : BaseScene() {
|
class GalleryDetailScene : BaseScene() {
|
||||||
private var mDownloadState = 0
|
private var mDownloadState = 0
|
||||||
@@ -416,7 +415,7 @@ class GalleryDetailScene : BaseScene() {
|
|||||||
getDetailError = getString(R.string.error_cannot_find_gallery)
|
getDetailError = getString(R.string.error_cannot_find_gallery)
|
||||||
}
|
}
|
||||||
(requireActivity() as MainActivity).mShareUrl = galleryDetailUrl
|
(requireActivity() as MainActivity).mShareUrl = galleryDetailUrl
|
||||||
return ComposeView(requireContext()).apply {
|
return ComposeWithViewLifecycle().apply {
|
||||||
setMD3Content {
|
setMD3Content {
|
||||||
LaunchedEffect(gid) {
|
LaunchedEffect(gid) {
|
||||||
EhDownloadManager.stateFlow(gid).collect {
|
EhDownloadManager.stateFlow(gid).collect {
|
||||||
@@ -726,7 +725,9 @@ class GalleryDetailScene : BaseScene() {
|
|||||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
||||||
) {
|
) {
|
||||||
val favored by produceState(initialValue = false) {
|
val favored by produceState(initialValue = false) {
|
||||||
@@ -979,7 +980,7 @@ class GalleryDetailScene : BaseScene() {
|
|||||||
context ?: return
|
context ?: return
|
||||||
if (mPage != 0) {
|
if (mPage != 0) {
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
requireActivity().findViewById(R.id.snackbar),
|
requireActivity().findViewById(R.id.fragment_container),
|
||||||
getString(R.string.read_from, mPage),
|
getString(R.string.read_from, mPage),
|
||||||
Snackbar.LENGTH_LONG,
|
Snackbar.LENGTH_LONG,
|
||||||
).setAction(R.string.read) { context?.navToReader(gd.galleryInfo, mPage) }.show()
|
).setAction(R.string.read) { context?.navToReader(gd.galleryInfo, mPage) }.show()
|
||||||
@@ -1014,9 +1015,9 @@ class GalleryDetailScene : BaseScene() {
|
|||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
@Composable
|
@Composable
|
||||||
private fun GalleryDetailComment(commentsList: Array<GalleryComment>?) {
|
private fun GalleryDetailComment(commentsList: Array<GalleryComment>) {
|
||||||
val maxShowCount = 2
|
val maxShowCount = 2
|
||||||
val commentText = if (commentsList.isNullOrEmpty()) {
|
val commentText = if (commentsList.isEmpty()) {
|
||||||
stringResource(R.string.no_comments)
|
stringResource(R.string.no_comments)
|
||||||
} else if (commentsList.size <= maxShowCount) {
|
} else if (commentsList.size <= maxShowCount) {
|
||||||
stringResource(R.string.no_more_comments)
|
stringResource(R.string.no_more_comments)
|
||||||
@@ -1024,25 +1025,24 @@ class GalleryDetailScene : BaseScene() {
|
|||||||
stringResource(R.string.more_comment)
|
stringResource(R.string.more_comment)
|
||||||
}
|
}
|
||||||
CrystalCard {
|
CrystalCard {
|
||||||
if (commentsList != null) {
|
val length = maxShowCount.coerceAtMost(commentsList.size)
|
||||||
val length = maxShowCount.coerceAtMost(commentsList.size)
|
for (i in 0 until length) {
|
||||||
for (i in 0 until length) {
|
val item = commentsList[i]
|
||||||
val comment = commentsList[i]
|
AndroidViewBinding(factory = ItemGalleryCommentBinding::inflate) {
|
||||||
AndroidView(factory = { context ->
|
card.setOnClickListener { onNavigateToCommentScene() }
|
||||||
ItemGalleryCommentBinding.inflate(LayoutInflater.from(context), null, false).apply {
|
user.text = item.user
|
||||||
card.setOnClickListener { onNavigateToCommentScene() }
|
user.setBackgroundColor(Color.TRANSPARENT)
|
||||||
user.text = comment.user
|
time.text = ReadableTime.getTimeAgo(item.time)
|
||||||
user.setBackgroundColor(Color.TRANSPARENT)
|
comment.maxLines = 5
|
||||||
time.text = ReadableTime.getTimeAgo(comment.time)
|
comment.text = item.comment.orEmpty().parseAsHtml(imageGetter = CoilImageGetter(comment))
|
||||||
this.comment.maxLines = 5
|
|
||||||
this.comment.text = comment.comment.orEmpty().parseAsHtml(imageGetter = CoilImageGetter(this.comment))
|
|
||||||
}.root
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = dimensionResource(id = R.dimen.strip_item_padding_v))
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(16.dp)).clickable(onClick = ::onNavigateToCommentScene),
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = dimensionResource(id = R.dimen.strip_item_padding_v))
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(onClick = ::onNavigateToCommentScene),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(commentText)
|
Text(commentText)
|
||||||
@@ -1211,11 +1211,11 @@ class GalleryDetailScene : BaseScene() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.copy -> {
|
R.id.copy -> {
|
||||||
requireActivity().addTextToClipboard(tag, false)
|
requireActivity().addTextToClipboard(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.copy_trans -> {
|
R.id.copy_trans -> {
|
||||||
requireActivity().addTextToClipboard(translated, false)
|
requireActivity().addTextToClipboard(translated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.ui.scene
|
package com.hippo.ehviewer.ui.scene
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -29,14 +30,11 @@ import android.widget.TextView
|
|||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.GravityCompat
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsAnimationCompat
|
import androidx.core.view.WindowInsetsAnimationCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -72,7 +70,6 @@ import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_NORMAL
|
|||||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_SUBSCRIPTION
|
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_SUBSCRIPTION
|
||||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TAG
|
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TAG
|
||||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
|
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
|
||||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_UPLOADER
|
|
||||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT
|
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT
|
||||||
import com.hippo.ehviewer.client.exception.CloudflareBypassException
|
import com.hippo.ehviewer.client.exception.CloudflareBypassException
|
||||||
import com.hippo.ehviewer.client.exception.EhException
|
import com.hippo.ehviewer.client.exception.EhException
|
||||||
@@ -101,19 +98,17 @@ import com.hippo.ehviewer.ui.tools.DialogState
|
|||||||
import com.hippo.ehviewer.updater.AppUpdater
|
import com.hippo.ehviewer.updater.AppUpdater
|
||||||
import com.hippo.ehviewer.util.AnimationUtils
|
import com.hippo.ehviewer.util.AnimationUtils
|
||||||
import com.hippo.ehviewer.util.ExceptionUtils
|
import com.hippo.ehviewer.util.ExceptionUtils
|
||||||
|
import com.hippo.ehviewer.util.SimpleAnimatorListener
|
||||||
|
import com.hippo.ehviewer.util.applyNavigationBarsPadding
|
||||||
import com.hippo.ehviewer.util.getParcelableCompat
|
import com.hippo.ehviewer.util.getParcelableCompat
|
||||||
import com.hippo.ehviewer.util.getValue
|
import com.hippo.ehviewer.util.getValue
|
||||||
import com.hippo.ehviewer.util.lazyMut
|
import com.hippo.ehviewer.util.lazyMut
|
||||||
import com.hippo.ehviewer.util.setValue
|
import com.hippo.ehviewer.util.setValue
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import kotlinx.coroutines.delay
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
|
||||||
import rikka.core.res.resolveColor
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -121,10 +116,16 @@ import java.time.ZoneId
|
|||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
import rikka.core.res.resolveColor
|
||||||
|
|
||||||
class VMStorage1 : ViewModel() {
|
class VMStorage1 : ViewModel() {
|
||||||
var urlBuilder = ListUrlBuilder()
|
var urlBuilder = ListUrlBuilder()
|
||||||
var shouldScrollToTop = false
|
|
||||||
val dataFlow = Pager(PagingConfig(25)) {
|
val dataFlow = Pager(PagingConfig(25)) {
|
||||||
object : PagingSource<String, BaseGalleryInfo>() {
|
object : PagingSource<String, BaseGalleryInfo>() {
|
||||||
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
|
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
|
||||||
@@ -155,7 +156,6 @@ class VMStorage1 : ViewModel() {
|
|||||||
} else {
|
} else {
|
||||||
urlBuilder.setIndex(key, false)
|
urlBuilder.setIndex(key, false)
|
||||||
}
|
}
|
||||||
shouldScrollToTop = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val r = runSuspendCatching {
|
val r = runSuspendCatching {
|
||||||
@@ -201,8 +201,16 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
lateinit var mQuickSearchList: MutableList<QuickSearch>
|
lateinit var mQuickSearchList: MutableList<QuickSearch>
|
||||||
private var mHideActionFabSlop = 0
|
private var mHideActionFabSlop = 0
|
||||||
private var mState = State.NORMAL
|
private var mState = State.NORMAL
|
||||||
|
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
binding.searchFab.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val fabLayout get() = binding.fabLayout
|
override val fabLayout get() = binding.fabLayout
|
||||||
|
override val fastScroller get() = binding.fastScroller
|
||||||
|
override val recyclerView get() = binding.recyclerView
|
||||||
|
override val contentView get() = binding.contentLayout.contentView
|
||||||
|
|
||||||
override fun getMenuResId(): Int {
|
override fun getMenuResId(): Int {
|
||||||
return R.menu.scene_gallery_list_searchbar_menu
|
return R.menu.scene_gallery_list_searchbar_menu
|
||||||
@@ -214,7 +222,7 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
ACTION_HOMEPAGE -> ListUrlBuilder()
|
ACTION_HOMEPAGE -> ListUrlBuilder()
|
||||||
ACTION_SUBSCRIPTION -> ListUrlBuilder(MODE_SUBSCRIPTION)
|
ACTION_SUBSCRIPTION -> ListUrlBuilder(MODE_SUBSCRIPTION)
|
||||||
ACTION_WHATS_HOT -> ListUrlBuilder(MODE_WHATS_HOT)
|
ACTION_WHATS_HOT -> ListUrlBuilder(MODE_WHATS_HOT)
|
||||||
ACTION_TOP_LIST -> ListUrlBuilder(MODE_TOPLIST, mKeyword = "11")
|
ACTION_TOP_LIST -> ListUrlBuilder(MODE_TOPLIST, mKeyword = Settings.recentToplist)
|
||||||
ACTION_LIST_URL_BUILDER -> args?.getParcelableCompat<ListUrlBuilder>(KEY_LIST_URL_BUILDER)?.copy() ?: ListUrlBuilder()
|
ACTION_LIST_URL_BUILDER -> args?.getParcelableCompat<ListUrlBuilder>(KEY_LIST_URL_BUILDER)?.copy() ?: ListUrlBuilder()
|
||||||
else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!")
|
else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!")
|
||||||
}
|
}
|
||||||
@@ -286,14 +294,6 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
title = resources.getString(R.string.search)
|
title = resources.getString(R.string.search)
|
||||||
}
|
}
|
||||||
setSearchBarHint(title)
|
setSearchBarHint(title)
|
||||||
|
|
||||||
when (mode) {
|
|
||||||
MODE_NORMAL, MODE_SUBSCRIPTION -> if (category != EhUtils.NONE || !keyword.isNullOrEmpty()) {
|
|
||||||
mainActivity?.clearNavCheckedItem()
|
|
||||||
}
|
|
||||||
MODE_TAG, MODE_UPLOADER, MODE_IMAGE_SEARCH -> mainActivity?.clearNavCheckedItem()
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val dialogState = DialogState()
|
private val dialogState = DialogState()
|
||||||
@@ -321,7 +321,7 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View {
|
): View {
|
||||||
_binding = SceneGalleryListBinding.inflate(inflater, container!!)
|
_binding = SceneGalleryListBinding.inflate(inflater, container!!)
|
||||||
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
|
container.addView(ComposeWithViewLifecycle().apply { setMD3Content { dialogState.Intercept() } })
|
||||||
checkForUpdates()
|
checkForUpdates()
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(stateBackPressedCallback)
|
requireActivity().onBackPressedDispatcher.addCallback(stateBackPressedCallback)
|
||||||
mHideActionFabSlop = ViewConfiguration.get(requireContext()).scaledTouchSlop
|
mHideActionFabSlop = ViewConfiguration.get(requireContext()).scaledTouchSlop
|
||||||
@@ -331,7 +331,7 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
WindowInsetsAnimationHelper(
|
WindowInsetsAnimationHelper(
|
||||||
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
|
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
|
||||||
binding.fabLayout,
|
binding.fabLayout,
|
||||||
mSearchFab.parent as View,
|
binding.searchFab.parent as View,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
mViewTransition = BringOutTransition(binding.contentLayout.contentView, binding.searchLayout)
|
mViewTransition = BringOutTransition(binding.contentLayout.contentView, binding.searchLayout)
|
||||||
@@ -366,6 +366,7 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||||
binding.tip.setCompoundDrawables(null, drawable, null, null)
|
binding.tip.setCompoundDrawables(null, drawable, null, null)
|
||||||
binding.tip.setOnClickListener { mAdapter?.retry() }
|
binding.tip.setOnClickListener { mAdapter?.retry() }
|
||||||
|
binding.refreshLayout.setProgressViewOffset(true, 0, 64.dpToPx)
|
||||||
binding.refreshLayout.setOnRefreshListener {
|
binding.refreshLayout.setOnRefreshListener {
|
||||||
mUrlBuilder.setIndex(null, true)
|
mUrlBuilder.setIndex(null, true)
|
||||||
mUrlBuilder.mJumpTo = null
|
mUrlBuilder.mJumpTo = null
|
||||||
@@ -379,12 +380,10 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
is LoadState.Loading -> {
|
is LoadState.Loading -> {
|
||||||
showSearchBar()
|
showSearchBar()
|
||||||
if (!binding.refreshLayout.isRefreshing) {
|
if (!binding.refreshLayout.isRefreshing) {
|
||||||
// https://github.com/FooIbar/EhViewer/issues/45
|
transition.showView(1)
|
||||||
// transition.showView(1)
|
|
||||||
transition.showView(0, false)
|
|
||||||
binding.refreshLayout.isRefreshing = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is LoadState.Error -> {
|
is LoadState.Error -> {
|
||||||
binding.refreshLayout.isRefreshing = false
|
binding.refreshLayout.isRefreshing = false
|
||||||
binding.tip.text = ExceptionUtils.getReadableString(state.error)
|
binding.tip.text = ExceptionUtils.getReadableString(state.error)
|
||||||
@@ -400,22 +399,20 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
navAnimated(R.id.webView, bundleOf(WebViewActivity.KEY_URL to EhUrl.host))
|
navAnimated(R.id.webView, bundleOf(WebViewActivity.KEY_URL to EhUrl.host))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is LoadState.NotLoading -> {
|
is LoadState.NotLoading -> {
|
||||||
binding.refreshLayout.isRefreshing = false
|
binding.refreshLayout.isRefreshing = false
|
||||||
if (mAdapter?.itemCount == 0) {
|
if (mAdapter?.itemCount == 0) {
|
||||||
binding.tip.text = empty
|
binding.tip.text = empty
|
||||||
transition.showView(2)
|
transition.showView(2)
|
||||||
} else {
|
} else {
|
||||||
transition.showView(0, false)
|
transition.showView(0)
|
||||||
if (vm.shouldScrollToTop) {
|
|
||||||
vm.shouldScrollToTop = false
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Settings.needSignInFlow.first { !it }
|
||||||
vm.dataFlow.collectLatest {
|
vm.dataFlow.collectLatest {
|
||||||
adapter.submitData(it)
|
adapter.submitData(it)
|
||||||
}
|
}
|
||||||
@@ -463,6 +460,7 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
mUrlBuilder.mJumpTo = null
|
mUrlBuilder.mJumpTo = null
|
||||||
mAdapter?.refresh()
|
mAdapter?.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> { // Last
|
2 -> { // Last
|
||||||
if (mIsTopList) {
|
if (mIsTopList) {
|
||||||
mUrlBuilder.mJumpTo = "${TOPLIST_PAGES - 1}"
|
mUrlBuilder.mJumpTo = "${TOPLIST_PAGES - 1}"
|
||||||
@@ -480,15 +478,24 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
val actionFabDrawable = AddDeleteDrawable(requireContext(), colorID)
|
val actionFabDrawable = AddDeleteDrawable(requireContext(), colorID)
|
||||||
binding.fabLayout.addOnExpandListener {
|
binding.fabLayout.addOnExpandListener {
|
||||||
if (it) {
|
if (it) {
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
|
lockDrawer()
|
||||||
actionFabDrawable.setDelete(ANIMATE_TIME)
|
actionFabDrawable.setDelete(ANIMATE_TIME)
|
||||||
} else {
|
} else {
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
|
unlockDrawer()
|
||||||
actionFabDrawable.setAdd(ANIMATE_TIME)
|
actionFabDrawable.setAdd(ANIMATE_TIME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.fabLayout.primaryFab!!.setImageDrawable(actionFabDrawable)
|
binding.fabLayout.primaryFab!!.setImageDrawable(actionFabDrawable)
|
||||||
addAboveSnackView(binding.fabLayout)
|
binding.searchFab.setOnClickListener { onApplySearch() }
|
||||||
|
(binding.searchFab.parent as View).apply {
|
||||||
|
bringToFront()
|
||||||
|
applyNavigationBarsPadding()
|
||||||
|
}
|
||||||
|
binding.searchLayout.applyInsetter {
|
||||||
|
type(statusBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update list url builder
|
// Update list url builder
|
||||||
onUpdateUrlBuilder()
|
onUpdateUrlBuilder()
|
||||||
@@ -504,7 +511,6 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
stateBackPressedCallback.remove()
|
stateBackPressedCallback.remove()
|
||||||
binding.recyclerView.stopScroll()
|
binding.recyclerView.stopScroll()
|
||||||
removeAboveSnackView(binding.fabLayout)
|
|
||||||
_binding = null
|
_binding = null
|
||||||
fabAnimator = null
|
fabAnimator = null
|
||||||
mAdapter = null
|
mAdapter = null
|
||||||
@@ -705,6 +711,47 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showSearchFab(animation: Boolean, delay: Long = 0) {
|
||||||
|
if (animation) {
|
||||||
|
binding.searchFab.visibility = View.VISIBLE
|
||||||
|
binding.searchFab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
|
||||||
|
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
||||||
|
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
||||||
|
} else {
|
||||||
|
binding.searchFab.visibility = View.VISIBLE
|
||||||
|
binding.searchFab.scaleX = 1.0f
|
||||||
|
binding.searchFab.scaleY = 1.0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideSearchFab(animation: Boolean): Long {
|
||||||
|
return if (View.INVISIBLE == binding.searchFab.visibility) {
|
||||||
|
0L
|
||||||
|
} else {
|
||||||
|
if (animation) {
|
||||||
|
binding.searchFab.animate().scaleX(0.0f).scaleY(0.0f)
|
||||||
|
.setListener(mSearchFabAnimatorListener)
|
||||||
|
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
||||||
|
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
||||||
|
} else {
|
||||||
|
binding.searchFab.visibility = View.INVISIBLE
|
||||||
|
binding.searchFab.scaleX = 0.0f
|
||||||
|
binding.searchFab.scaleY = 0.0f
|
||||||
|
}
|
||||||
|
ANIMATE_TIME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectSearchFab(animation: Boolean) {
|
||||||
|
val delay = hideActionFab(animation)
|
||||||
|
showSearchFab(animation, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectActionFab(animation: Boolean) {
|
||||||
|
val delay = hideSearchFab(animation)
|
||||||
|
showActionFab(animation, delay)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSearchViewExpanded() {
|
override fun onSearchViewExpanded() {
|
||||||
if (mState == State.NORMAL) {
|
if (mState == State.NORMAL) {
|
||||||
hideActionFab(true)
|
hideActionFab(true)
|
||||||
@@ -854,9 +901,9 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
private fun onStateChange(newState: State) {
|
private fun onStateChange(newState: State) {
|
||||||
stateBackPressedCallback.isEnabled = newState != State.NORMAL
|
stateBackPressedCallback.isEnabled = newState != State.NORMAL
|
||||||
if (newState == State.NORMAL || newState == State.SIMPLE_SEARCH) {
|
if (newState == State.NORMAL || newState == State.SIMPLE_SEARCH) {
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
|
unlockDrawer()
|
||||||
} else {
|
} else {
|
||||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
|
lockDrawer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -864,7 +911,8 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
|
|
||||||
private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) :
|
private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) :
|
||||||
RecyclerView.Adapter<QsDrawerHolder>() {
|
RecyclerView.Adapter<QsDrawerHolder>() {
|
||||||
private val keywords = intArrayOf(11, 12, 13, 15)
|
private val toplists = resources.getStringArray(R.array.toplist_entries)
|
||||||
|
private val keywords = resources.getStringArray(R.array.toplist_values)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {
|
||||||
val holder = QsDrawerHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
|
val holder = QsDrawerHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
|
||||||
@@ -874,7 +922,9 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
navAnimated(R.id.galleryListScene, ListUrlBuilder().apply { set(q) }.toStartArgs())
|
navAnimated(R.id.galleryListScene, ListUrlBuilder().apply { set(q) }.toStartArgs())
|
||||||
} else {
|
} else {
|
||||||
if (mIsTopList) {
|
if (mIsTopList) {
|
||||||
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
|
val keyword = keywords[holder.bindingAdapterPosition]
|
||||||
|
Settings.recentToplist = keyword
|
||||||
|
mUrlBuilder.keyword = keyword
|
||||||
mUrlBuilder.mJumpTo = null
|
mUrlBuilder.mJumpTo = null
|
||||||
} else {
|
} else {
|
||||||
val q = mQuickSearchList[holder.bindingAdapterPosition]
|
val q = mQuickSearchList[holder.bindingAdapterPosition]
|
||||||
@@ -894,13 +944,7 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
if (!mIsTopList) {
|
if (!mIsTopList) {
|
||||||
text.text = mQuickSearchList[position].name
|
text.text = mQuickSearchList[position].name
|
||||||
} else {
|
} else {
|
||||||
val toplists = intArrayOf(
|
text.text = toplists[position]
|
||||||
R.string.toplist_alltime,
|
|
||||||
R.string.toplist_pastyear,
|
|
||||||
R.string.toplist_pastmonth,
|
|
||||||
R.string.toplist_yesterday,
|
|
||||||
)
|
|
||||||
text.text = getString(toplists[position])
|
|
||||||
option.visibility = View.GONE
|
option.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.hippo.ehviewer.ui.scene
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -53,6 +54,7 @@ import com.hippo.ehviewer.client.data.GalleryDetail
|
|||||||
import com.hippo.ehviewer.client.data.GalleryPreview
|
import com.hippo.ehviewer.client.data.GalleryPreview
|
||||||
import com.hippo.ehviewer.coil.justDownload
|
import com.hippo.ehviewer.coil.justDownload
|
||||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||||
|
import com.hippo.ehviewer.ui.MainActivity
|
||||||
import com.hippo.ehviewer.ui.legacy.calculateSuitableSpanCount
|
import com.hippo.ehviewer.ui.legacy.calculateSuitableSpanCount
|
||||||
import com.hippo.ehviewer.ui.main.EhPreviewItem
|
import com.hippo.ehviewer.ui.main.EhPreviewItem
|
||||||
import com.hippo.ehviewer.ui.navToReader
|
import com.hippo.ehviewer.ui.navToReader
|
||||||
@@ -60,6 +62,7 @@ import com.hippo.ehviewer.ui.setMD3Content
|
|||||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid
|
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid
|
||||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||||
|
import com.hippo.ehviewer.util.findActivity
|
||||||
import com.hippo.ehviewer.util.getParcelableCompat
|
import com.hippo.ehviewer.util.getParcelableCompat
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -158,6 +161,11 @@ class GalleryPreviewScreen : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
requireContext().findActivity<MainActivity>().recompose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const val KEY_GALLERY_DETAIL = "gallery_detail"
|
const val KEY_GALLERY_DETAIL = "gallery_detail"
|
||||||
|
|||||||
@@ -44,13 +44,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.GravityCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
@@ -61,6 +58,8 @@ import androidx.paging.compose.itemKey
|
|||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings.listThumbSize
|
import com.hippo.ehviewer.Settings.listThumbSize
|
||||||
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
|
import com.hippo.ehviewer.icons.big.History
|
||||||
import com.hippo.ehviewer.ui.doGalleryInfoAction
|
import com.hippo.ehviewer.ui.doGalleryInfoAction
|
||||||
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
|
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
|
||||||
import com.hippo.ehviewer.ui.setMD3Content
|
import com.hippo.ehviewer.ui.setMD3Content
|
||||||
@@ -73,7 +72,10 @@ import eu.kanade.tachiyomi.util.system.pxToDp
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
class HistoryScene : BaseScene() {
|
class HistoryScene : BaseScene() {
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = ComposeView(inflater.context).apply {
|
// Disabled for breaking swipe-to-dismiss
|
||||||
|
override val enableDrawerGestures = false
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = ComposeWithViewLifecycle().apply {
|
||||||
setMD3Content {
|
setMD3Content {
|
||||||
val dialogState = rememberDialogState()
|
val dialogState = rememberDialogState()
|
||||||
dialogState.Intercept()
|
dialogState.Intercept()
|
||||||
@@ -86,7 +88,7 @@ class HistoryScene : BaseScene() {
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(text = stringResource(id = R.string.history)) },
|
title = { Text(text = stringResource(id = R.string.history)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { toggleDrawer(GravityCompat.START) }) {
|
IconButton(onClick = { openDrawer() }) {
|
||||||
Icon(imageVector = Icons.Default.Menu, contentDescription = null)
|
Icon(imageVector = Icons.Default.Menu, contentDescription = null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -159,19 +161,25 @@ class HistoryScene : BaseScene() {
|
|||||||
directions = setOf(DismissDirection.EndToStart),
|
directions = setOf(DismissDirection.EndToStart),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CrystalCard(modifier = Modifier.height(cardHeight).fillMaxWidth()) {}
|
CrystalCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(cardHeight)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Deferred({ delay(200) }) {
|
Deferred({ delay(200) }) {
|
||||||
if (historyData.itemCount == 0) {
|
if (historyData.itemCount == 0) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(paddingValues).fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.big_history),
|
imageVector = EhIcons.Big.Default.History,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
Modifier.padding(16.dp),
|
Modifier.padding(16.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,24 +20,22 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
|
import com.hippo.ehviewer.icons.big.SadAndroid
|
||||||
import com.hippo.ehviewer.ui.setMD3Content
|
import com.hippo.ehviewer.ui.setMD3Content
|
||||||
import com.hippo.ehviewer.util.ExceptionUtils
|
import com.hippo.ehviewer.util.ExceptionUtils
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
|
||||||
class ProgressScene : BaseScene() {
|
class ProgressScene : BaseScene() {
|
||||||
override val showLeftDrawer = false
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return ComposeView(inflater.context).apply {
|
return ComposeWithViewLifecycle().apply {
|
||||||
setMD3Content {
|
setMD3Content {
|
||||||
val action = rememberSaveable { requireArguments().getString(KEY_ACTION, INVALID) }
|
val action = rememberSaveable { requireArguments().getString(KEY_ACTION, INVALID) }
|
||||||
val gid = rememberSaveable { requireArguments().getLong(KEY_GID, -1) }
|
val gid = rememberSaveable { requireArguments().getLong(KEY_GID, -1) }
|
||||||
@@ -79,7 +77,7 @@ class ProgressScene : BaseScene() {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.big_sad_pandroid),
|
imageVector = EhIcons.Big.Default.SadAndroid,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.hippo.ehviewer.ui.scene
|
||||||
|
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* compose view with fragment view lifecycle
|
||||||
|
* use it after [Fragment.onCreateView]
|
||||||
|
* also @see [Fragment.getViewLifecycleOwner]
|
||||||
|
*/
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
fun Fragment.ComposeWithViewLifecycle() = ComposeView(requireContext()).apply {
|
||||||
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner))
|
||||||
|
}
|
||||||
@@ -9,10 +9,9 @@ import android.view.ViewGroup
|
|||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -28,9 +27,8 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.search.SearchView
|
import com.google.android.material.search.SearchView
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
@@ -46,8 +44,10 @@ import com.hippo.ehviewer.ui.legacy.FabLayout
|
|||||||
import com.hippo.ehviewer.ui.setMD3Content
|
import com.hippo.ehviewer.ui.setMD3Content
|
||||||
import com.hippo.ehviewer.util.AnimationUtils
|
import com.hippo.ehviewer.util.AnimationUtils
|
||||||
import com.hippo.ehviewer.util.SimpleAnimatorListener
|
import com.hippo.ehviewer.util.SimpleAnimatorListener
|
||||||
|
import com.hippo.ehviewer.util.applyNavigationBarsPadding
|
||||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||||
import com.jamal.composeprefs3.ui.ifTrueThen
|
import com.jamal.composeprefs3.ui.ifTrueThen
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
@@ -58,22 +58,22 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
|||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private var mSuggestionList by mutableStateOf(emptyList<Suggestion>())
|
private var mSuggestionList by mutableStateOf(emptyList<Suggestion>())
|
||||||
private var mSuggestionProvider: SuggestionProvider? = null
|
private var mSuggestionProvider: SuggestionProvider? = null
|
||||||
var allowEmptySearch = true
|
private var allowEmptySearch = true
|
||||||
private val mSearchDatabase = searchDatabase.searchDao()
|
private val mSearchDatabase = searchDatabase.searchDao()
|
||||||
private var onApplySearch: (String) -> Unit = {}
|
private var onApplySearch: (String) -> Unit = {}
|
||||||
protected val mSearchFab get() = binding.searchFab
|
|
||||||
protected abstract val fabLayout: FabLayout
|
|
||||||
protected var mShowActionFab = true
|
protected var mShowActionFab = true
|
||||||
protected val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
|
protected val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
fabLayout.primaryFab?.visibility = View.INVISIBLE
|
fabLayout.primaryFab?.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
protected abstract val fabLayout: FabLayout
|
||||||
mSearchFab.visibility = View.INVISIBLE
|
protected abstract val fastScroller: View
|
||||||
}
|
protected abstract val recyclerView: View
|
||||||
}
|
protected abstract val contentView: View
|
||||||
|
|
||||||
|
override val enableDrawerGestures = true
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = SceneSearchbarBinding.inflate(inflater, container, false)
|
_binding = SceneSearchbarBinding.inflate(inflater, container, false)
|
||||||
@@ -84,8 +84,9 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
binding.searchBarList.consumeWindowInsets = false
|
binding.searchBarList.consumeWindowInsets = false
|
||||||
|
binding.searchBarList.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner))
|
||||||
binding.searchBarList.setMD3Content {
|
binding.searchBarList.setMD3Content {
|
||||||
LazyColumn(contentPadding = WindowInsets.navigationBars.asPaddingValues()) {
|
LazyColumn(modifier = Modifier.navigationBarsPadding().imePadding()) {
|
||||||
items(mSuggestionList) {
|
items(mSuggestionList) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(text = it.keyword) },
|
headlineContent = { Text(text = it.keyword) },
|
||||||
@@ -117,13 +118,20 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
|||||||
onSearchViewHidden()
|
onSearchViewHidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mSearchFab.setOnClickListener { onApplySearch() }
|
|
||||||
onCreateViewWithToolbar(inflater, binding.root, savedInstanceState)
|
onCreateViewWithToolbar(inflater, binding.root, savedInstanceState)
|
||||||
// This has to be placed after onCreateViewWithToolbar() since
|
// This has to be placed after onCreateViewWithToolbar() since
|
||||||
// callbacks are invoked in the reverse order in which they are added
|
// callbacks are invoked in the reverse order in which they are added
|
||||||
binding.searchview.addTransitionListener(mSearchViewOnBackPressedCallback)
|
binding.searchview.addTransitionListener(mSearchViewOnBackPressedCallback)
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(mSearchViewOnBackPressedCallback)
|
requireActivity().onBackPressedDispatcher.addCallback(mSearchViewOnBackPressedCallback)
|
||||||
binding.appbar.bringToFront()
|
binding.appbar.bringToFront()
|
||||||
|
fabLayout.applyNavigationBarsPadding()
|
||||||
|
fastScroller.applyNavigationBarsPadding()
|
||||||
|
recyclerView.applyNavigationBarsPadding()
|
||||||
|
contentView.applyInsetter {
|
||||||
|
type(statusBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,16 +154,13 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
|||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private var privLockModeStart: Int? = null
|
private var drawerLocked = false
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun onSearchViewExpanded() {
|
open fun onSearchViewExpanded() {
|
||||||
privLockModeStart = getDrawerLockMode(GravityCompat.START)
|
if (!isDrawerLocked) {
|
||||||
privLockModeStart?.let {
|
drawerLocked = true
|
||||||
setDrawerLockMode(
|
lockDrawer()
|
||||||
DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
|
|
||||||
GravityCompat.START,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
updateSuggestions()
|
updateSuggestions()
|
||||||
}
|
}
|
||||||
@@ -163,38 +168,9 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
|||||||
@CallSuper
|
@CallSuper
|
||||||
open fun onSearchViewHidden() {
|
open fun onSearchViewHidden() {
|
||||||
binding.toolbar.setText(binding.searchview.text)
|
binding.toolbar.setText(binding.searchview.text)
|
||||||
privLockModeStart?.let { setDrawerLockMode(it, GravityCompat.START) }
|
if (drawerLocked) {
|
||||||
privLockModeStart = null
|
unlockDrawer()
|
||||||
}
|
drawerLocked = false
|
||||||
|
|
||||||
fun showSearchFab(animation: Boolean, delay: Long = 0) {
|
|
||||||
if (animation) {
|
|
||||||
mSearchFab.visibility = View.VISIBLE
|
|
||||||
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
|
|
||||||
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
|
||||||
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
|
||||||
} else {
|
|
||||||
mSearchFab.visibility = View.VISIBLE
|
|
||||||
mSearchFab.scaleX = 1.0f
|
|
||||||
mSearchFab.scaleY = 1.0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideSearchFab(animation: Boolean): Long {
|
|
||||||
return if (View.INVISIBLE == mSearchFab.visibility) {
|
|
||||||
0L
|
|
||||||
} else {
|
|
||||||
if (animation) {
|
|
||||||
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
|
|
||||||
.setListener(mSearchFabAnimatorListener)
|
|
||||||
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
|
||||||
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
|
||||||
} else {
|
|
||||||
mSearchFab.visibility = View.INVISIBLE
|
|
||||||
mSearchFab.scaleX = 0.0f
|
|
||||||
mSearchFab.scaleY = 0.0f
|
|
||||||
}
|
|
||||||
ANIMATE_TIME
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,16 +214,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectSearchFab(animation: Boolean) {
|
|
||||||
val delay = hideActionFab(animation)
|
|
||||||
showSearchFab(animation, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun selectActionFab(animation: Boolean) {
|
|
||||||
val delay = hideSearchFab(animation)
|
|
||||||
showActionFab(animation, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchBarHint(hint: String?) {
|
fun setSearchBarHint(hint: String?) {
|
||||||
binding.toolbar.hint = hint
|
binding.toolbar.hint = hint
|
||||||
}
|
}
|
||||||
@@ -262,7 +228,7 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationClick() {
|
override fun onNavigationClick() {
|
||||||
toggleDrawer(GravityCompat.START)
|
openDrawer()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMenuResId(): Int {
|
override fun getMenuResId(): Int {
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ import com.hippo.ehviewer.util.AppConfig
|
|||||||
import com.hippo.ehviewer.util.ExceptionUtils
|
import com.hippo.ehviewer.util.ExceptionUtils
|
||||||
import com.hippo.ehviewer.util.installPackage
|
import com.hippo.ehviewer.util.installPackage
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import java.io.File
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}"
|
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}"
|
||||||
private const val RELEASE_URL = "$REPO_URL/releases"
|
private const val RELEASE_URL = "$REPO_URL/releases"
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -31,20 +33,22 @@ import com.hippo.ehviewer.BuildConfig
|
|||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
|
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||||
import com.hippo.ehviewer.ui.LocalNavController
|
import com.hippo.ehviewer.ui.LocalNavController
|
||||||
import com.hippo.ehviewer.ui.tools.observed
|
import com.hippo.ehviewer.ui.tools.observed
|
||||||
|
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.ReadableTime
|
import com.hippo.ehviewer.util.ReadableTime
|
||||||
|
import java.io.File
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import java.io.File
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdvancedScreen() {
|
fun AdvancedScreen() {
|
||||||
@@ -54,6 +58,8 @@ fun AdvancedScreen() {
|
|||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
|
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
|
||||||
|
val dialogState = rememberDialogState()
|
||||||
|
dialogState.Intercept()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -133,6 +139,19 @@ fun AdvancedScreen() {
|
|||||||
title = stringResource(id = R.string.preload_thumb_aggressively),
|
title = stringResource(id = R.string.preload_thumb_aggressively),
|
||||||
value = Settings::preloadThumbAggressively,
|
value = Settings::preloadThumbAggressively,
|
||||||
)
|
)
|
||||||
|
var userAgent by Settings::userAgent.observed
|
||||||
|
val userAgentTitle = stringResource(id = R.string.user_agent)
|
||||||
|
Preference(
|
||||||
|
title = userAgentTitle,
|
||||||
|
summary = userAgent,
|
||||||
|
) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
userAgent = dialogState.awaitInputText(
|
||||||
|
initial = userAgent,
|
||||||
|
title = userAgentTitle,
|
||||||
|
).trim().takeUnless { it.isBlank() } ?: CHROME_USER_AGENT
|
||||||
|
}
|
||||||
|
}
|
||||||
val exportFailed = stringResource(id = R.string.settings_advanced_export_data_failed)
|
val exportFailed = stringResource(id = R.string.settings_advanced_export_data_failed)
|
||||||
LauncherPreference(
|
LauncherPreference(
|
||||||
title = stringResource(id = R.string.settings_advanced_export_data),
|
title = stringResource(id = R.string.settings_advanced_export_data),
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
|
import com.hippo.ehviewer.icons.filled.SadPanda
|
||||||
import com.hippo.ehviewer.ui.ABOUT_SETTINGS_SCREEN
|
import com.hippo.ehviewer.ui.ABOUT_SETTINGS_SCREEN
|
||||||
import com.hippo.ehviewer.ui.ADVANCED_SETTINGS_SCREEN
|
import com.hippo.ehviewer.ui.ADVANCED_SETTINGS_SCREEN
|
||||||
import com.hippo.ehviewer.ui.DOWNLOAD_SETTINGS_SCREEN
|
import com.hippo.ehviewer.ui.DOWNLOAD_SETTINGS_SCREEN
|
||||||
@@ -47,7 +48,7 @@ fun BaseScreen() {
|
|||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(it).nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
Column(modifier = Modifier.padding(it).nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||||
PreferenceHeader(
|
PreferenceHeader(
|
||||||
icon = painterResource(id = R.drawable.v_sad_panda_primary_x24),
|
icon = EhIcons.Default.SadPanda,
|
||||||
title = R.string.settings_eh,
|
title = R.string.settings_eh,
|
||||||
childRouteName = EH_SETTINGS_SCREEN,
|
childRouteName = EH_SETTINGS_SCREEN,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import com.hippo.ehviewer.ui.tools.TimePickerDialog
|
|||||||
import com.hippo.ehviewer.ui.tools.observed
|
import com.hippo.ehviewer.ui.tools.observed
|
||||||
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||||
import com.hippo.ehviewer.util.whisperClipboard
|
import com.hippo.ehviewer.util.copyTextToClipboard
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -111,7 +111,7 @@ fun EhScreen() {
|
|||||||
val spanned = context.getString(R.string.settings_eh_identity_cookies_signed, str).parseAsHtml()
|
val spanned = context.getString(R.string.settings_eh_identity_cookies_signed, str).parseAsHtml()
|
||||||
setMessage(spanned)
|
setMessage(spanned)
|
||||||
setNeutralButton(R.string.settings_eh_identity_cookies_copy) { _, _ ->
|
setNeutralButton(R.string.settings_eh_identity_cookies_copy) { _, _ ->
|
||||||
context whisperClipboard str.replace("<br>", "\n")
|
copyTextToClipboard(str.replace("<br>", "\n"), true)
|
||||||
// Avoid double notify user since system have done that on Tiramisu above
|
// Avoid double notify user since system have done that on Tiramisu above
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) launchSnackBar(copiedToClipboard)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) launchSnackBar(copiedToClipboard)
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ fun EhScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SimpleMenuPreferenceInt(
|
SimpleMenuPreferenceInt(
|
||||||
title = stringResource(id = rikka.core.R.string.dark_theme),
|
title = stringResource(id = R.string.dark_theme),
|
||||||
entry = R.array.night_mode_entries,
|
entry = R.array.night_mode_entries,
|
||||||
entryValueRes = R.array.night_mode_values,
|
entryValueRes = R.array.night_mode_values,
|
||||||
value = Settings::theme.observed,
|
value = Settings::theme.observed,
|
||||||
@@ -277,11 +277,22 @@ fun EhScreen() {
|
|||||||
summary = stringResource(id = R.string.settings_eh_show_gallery_pages_summary),
|
summary = stringResource(id = R.string.settings_eh_show_gallery_pages_summary),
|
||||||
value = Settings::showGalleryPages,
|
value = Settings::showGalleryPages,
|
||||||
)
|
)
|
||||||
|
val showComments = Settings::showComments.observed
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = stringResource(id = R.string.settings_eh_show_gallery_comments),
|
title = stringResource(id = R.string.settings_eh_show_gallery_comments),
|
||||||
summary = stringResource(id = R.string.settings_eh_show_gallery_comments_summary),
|
summary = stringResource(id = R.string.settings_eh_show_gallery_comments_summary),
|
||||||
value = Settings::showComments,
|
value = showComments.rememberedAccessor,
|
||||||
)
|
)
|
||||||
|
AnimatedVisibility(visible = showComments.value) {
|
||||||
|
IntSliderPreference(
|
||||||
|
maxValue = 100,
|
||||||
|
minValue = -101,
|
||||||
|
showTicks = false,
|
||||||
|
title = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold),
|
||||||
|
summary = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold_summary),
|
||||||
|
value = Settings::commentThreshold,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (EhTagDatabase.isTranslatable(context)) {
|
if (EhTagDatabase.isTranslatable(context)) {
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = stringResource(id = R.string.settings_eh_show_tag_translations),
|
title = stringResource(id = R.string.settings_eh_show_tag_translations),
|
||||||
|
|||||||
@@ -49,8 +49,11 @@ import com.hippo.ehviewer.databinding.DialogAddFilterBinding
|
|||||||
import com.hippo.ehviewer.ui.LocalNavController
|
import com.hippo.ehviewer.ui.LocalNavController
|
||||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||||
import com.hippo.ehviewer.ui.tools.Deferred
|
import com.hippo.ehviewer.ui.tools.Deferred
|
||||||
|
import com.hippo.ehviewer.ui.tools.rememberDialogState
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.coroutines.groupByToObserved
|
import moe.tarsin.coroutines.groupByToObserved
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -60,6 +63,8 @@ fun FilterScreen() {
|
|||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
val allFilterMap = remember { scope.async { EhFilter.filters.await().groupByToObserved { it.mode } } }
|
val allFilterMap = remember { scope.async { EhFilter.filters.await().groupByToObserved { it.mode } } }
|
||||||
|
val dialogState = rememberDialogState()
|
||||||
|
dialogState.Intercept()
|
||||||
class AddFilterDialogHelper(private val dialog: AlertDialog) : View.OnClickListener {
|
class AddFilterDialogHelper(private val dialog: AlertDialog) : View.OnClickListener {
|
||||||
private val mArray = dialog.context.resources.getStringArray(R.array.filter_entries)
|
private val mArray = dialog.context.resources.getStringArray(R.array.filter_entries)
|
||||||
private val binding = DialogAddFilterBinding.bind(dialog.findViewById(R.id.base)!!)
|
private val binding = DialogAddFilterBinding.bind(dialog.findViewById(R.id.base)!!)
|
||||||
@@ -160,15 +165,22 @@ fun FilterScreen() {
|
|||||||
checked = filter.enable,
|
checked = filter.enable,
|
||||||
onCheckedChange = { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } },
|
onCheckedChange = { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } },
|
||||||
)
|
)
|
||||||
Text(text = filter.text)
|
Text(text = filter.text, modifier = Modifier.weight(1F))
|
||||||
Spacer(modifier = Modifier.weight(1F))
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
BaseDialogBuilder(context).setMessage(context.getString(R.string.delete_filter, filter.text)).setPositiveButton(R.string.delete) { _, which ->
|
scope.launch {
|
||||||
if (DialogInterface.BUTTON_POSITIVE == which) {
|
dialogState.awaitPermissionOrCancel(
|
||||||
filter.forget { filters.remove(filter) }
|
confirmText = R.string.delete,
|
||||||
|
dismissText = android.R.string.cancel,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.delete_filter, filter.text))
|
||||||
}
|
}
|
||||||
}.setNegativeButton(android.R.string.cancel, null).show()
|
filter.forget {
|
||||||
|
withUIContext {
|
||||||
|
filters.remove(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Default.Delete, contentDescription = null)
|
Icon(imageVector = Icons.Default.Delete, contentDescription = null)
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ import com.jamal.composeprefs3.ui.prefs.SliderPref
|
|||||||
import com.jamal.composeprefs3.ui.prefs.SpannedTextPref
|
import com.jamal.composeprefs3.ui.prefs.SpannedTextPref
|
||||||
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
||||||
import com.jamal.composeprefs3.ui.prefs.TextPref
|
import com.jamal.composeprefs3.ui.prefs.TextPref
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.reflect.KMutableProperty0
|
import kotlin.reflect.KMutableProperty0
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
object PreferenceTokens {
|
object PreferenceTokens {
|
||||||
val PreferenceHeaderHeight = 56.dp
|
val PreferenceHeaderHeight = 56.dp
|
||||||
@@ -95,10 +95,10 @@ fun SwitchPreference(title: String, summary: String? = null, value: KMutableProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, title: String, summary: String? = null, value: KMutableProperty0<Int>, enabled: Boolean = true) {
|
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, showTicks: Boolean = true, title: String, summary: String? = null, value: KMutableProperty0<Int>, enabled: Boolean = true) {
|
||||||
var v by remember { mutableIntStateOf(value.get()) }
|
var v by remember { mutableIntStateOf(value.get()) }
|
||||||
fun set(float: Float) = value.set(float.roundToInt().also { v = it })
|
fun set(float: Float) = value.set(float.roundToInt().also { v = it })
|
||||||
SliderPref(title = title, summary = summary, defaultValue = v.toFloat(), onValueChangeFinished = ::set, valueRange = minValue.toFloat()..maxValue.toFloat(), showValue = true, showInteger = true, steps = step, enabled = enabled)
|
SliderPref(title = title, summary = summary, defaultValue = v.toFloat(), onValueChangeFinished = ::set, valueRange = minValue.toFloat()..maxValue.toFloat(), showValue = true, showInteger = true, steps = step, showTicks = showTicks, enabled = enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> Deferred(block: suspend () -> T, content: @Composable (T) -> Unit) {
|
fun <T> Deferred(block: suspend () -> T, content: @Composable (T) -> Unit) {
|
||||||
var completed by remember(block) { mutableStateOf<T?>(null) }
|
var completed by remember { mutableStateOf<T?>(null) }
|
||||||
LaunchedEffect(key1 = Unit) {
|
LaunchedEffect(key1 = Unit) {
|
||||||
completed = withIOContext { block() }
|
completed = withIOContext { block() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||||
|
import kotlin.coroutines.resume
|
||||||
import kotlinx.coroutines.CancellableContinuation
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
interface DialogScope<R> {
|
interface DialogScope<R> {
|
||||||
var expectedValue: R
|
var expectedValue: R
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ import androidx.compose.ui.util.fastFirstOrNull
|
|||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import androidx.compose.ui.util.fastMaxBy
|
import androidx.compose.ui.util.fastMaxBy
|
||||||
import com.hippo.ehviewer.ui.tools.Scroller.STICKY_HEADER_KEY_PREFIX
|
import com.hippo.ehviewer.ui.tools.Scroller.STICKY_HEADER_KEY_PREFIX
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.sample
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws vertical fast scroller to a lazy list
|
* Draws vertical fast scroller to a lazy list
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import com.hippo.ehviewer.client.execute
|
|||||||
import com.hippo.ehviewer.client.executeAndParseAs
|
import com.hippo.ehviewer.client.executeAndParseAs
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import io.ktor.util.encodeBase64
|
import io.ktor.util.encodeBase64
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.sink
|
import okio.sink
|
||||||
@@ -14,10 +18,6 @@ import tachiyomi.data.release.GithubArtifacts
|
|||||||
import tachiyomi.data.release.GithubCommitComparison
|
import tachiyomi.data.release.GithubCommitComparison
|
||||||
import tachiyomi.data.release.GithubRelease
|
import tachiyomi.data.release.GithubRelease
|
||||||
import tachiyomi.data.release.GithubWorkflowRuns
|
import tachiyomi.data.release.GithubWorkflowRuns
|
||||||
import java.io.File
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.temporal.ChronoUnit
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
|
|
||||||
private const val API_URL = "https://api.github.com/repos/${BuildConfig.REPO_NAME}"
|
private const val API_URL = "https://api.github.com/repos/${BuildConfig.REPO_NAME}"
|
||||||
private const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
|
private const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ package com.hippo.ehviewer.util
|
|||||||
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
import splitties.init.appCtx
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
fun File.ensureDirectory() = if (exists()) isDirectory else mkdirs()
|
fun File.ensureDirectory() = if (exists()) isDirectory else mkdirs()
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ fun Spanned.toBBCode(): String {
|
|||||||
}
|
}
|
||||||
if (next < text.length) processOneSpanTransition(next)
|
if (next < text.length) processOneSpanTransition(next)
|
||||||
}
|
}
|
||||||
return StringBuilder().apply {
|
return buildString {
|
||||||
processOneSpanTransition()
|
processOneSpanTransition()
|
||||||
}.toString()
|
}
|
||||||
}
|
}
|
||||||
@@ -17,12 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.util
|
package com.hippo.ehviewer.util
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipDescription
|
import android.content.ClipDescription
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.textclassifier.TextClassifier
|
import android.view.textclassifier.TextClassifier
|
||||||
import androidx.core.os.persistableBundleOf
|
import androidx.core.os.persistableBundleOf
|
||||||
@@ -31,15 +29,7 @@ import com.hippo.ehviewer.ui.MainActivity
|
|||||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||||
import splitties.systemservices.clipboardManager
|
import splitties.systemservices.clipboardManager
|
||||||
|
|
||||||
infix fun Context.tellClipboardWithToast(text: String?) {
|
fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
|
||||||
addTextToClipboard(text, false, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
infix fun Context.whisperClipboard(text: String?) {
|
|
||||||
addTextToClipboard(text, true, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.addTextToClipboard(text: CharSequence?, isSensitive: Boolean, useToast: Boolean = false) {
|
|
||||||
clipboardManager.apply {
|
clipboardManager.apply {
|
||||||
setPrimaryClip(
|
setPrimaryClip(
|
||||||
ClipData.newPlainText(null, text).apply {
|
ClipData.newPlainText(null, text).apply {
|
||||||
@@ -49,12 +39,14 @@ fun Context.addTextToClipboard(text: CharSequence?, isSensitive: Boolean, useToa
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.addTextToClipboard(text: CharSequence?, useToast: Boolean = false) {
|
||||||
|
copyTextToClipboard(text, false)
|
||||||
// Avoid double notify user since system have done that on Tiramisu above
|
// Avoid double notify user since system have done that on Tiramisu above
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
val activity = findActivity()
|
val activity = findActivity<MainActivity>()
|
||||||
if (activity is MainActivity) {
|
activity.showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT, useToast)
|
||||||
activity.showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT, useToast)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,15 +58,3 @@ fun ClipboardManager.getUrlFromClipboard(context: Context): String? {
|
|||||||
val string = item?.coerceToText(context).toString()
|
val string = item?.coerceToText(context).toString()
|
||||||
return string.ifEmpty { null }
|
return string.ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the closest Activity in a given Context.
|
|
||||||
*/
|
|
||||||
internal fun Context.findActivity(): Activity {
|
|
||||||
var context = this
|
|
||||||
while (context is ContextWrapper) {
|
|
||||||
if (context is Activity) return context
|
|
||||||
context = context.baseContext
|
|
||||||
}
|
|
||||||
throw IllegalStateException("findActivity() should be called in the context of an Activity")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.hippo.ehviewer.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the closest Activity in a given Context.
|
||||||
|
*/
|
||||||
|
inline fun <reified T : Activity> Context.findActivity(): T {
|
||||||
|
var context = this
|
||||||
|
while (context is ContextWrapper) {
|
||||||
|
if (context is Activity) return context as T
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
throw IllegalStateException("findActivity() should be called in the context of an Activity")
|
||||||
|
}
|
||||||
@@ -16,8 +16,6 @@
|
|||||||
package com.hippo.ehviewer.util
|
package com.hippo.ehviewer.util
|
||||||
|
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.client.exception.EhException
|
|
||||||
import splitties.init.appCtx
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.ProtocolException
|
import java.net.ProtocolException
|
||||||
@@ -25,6 +23,7 @@ import java.net.SocketException
|
|||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import javax.net.ssl.SSLException
|
import javax.net.ssl.SSLException
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
object ExceptionUtils {
|
object ExceptionUtils {
|
||||||
fun getReadableString(e: Throwable): String {
|
fun getReadableString(e: Throwable): String {
|
||||||
@@ -43,12 +42,12 @@ object ExceptionUtils {
|
|||||||
is SocketTimeoutException -> appCtx.getString(R.string.error_timeout)
|
is SocketTimeoutException -> appCtx.getString(R.string.error_timeout)
|
||||||
is UnknownHostException -> appCtx.getString(R.string.error_unknown_host)
|
is UnknownHostException -> appCtx.getString(R.string.error_unknown_host)
|
||||||
is StatusCodeException -> {
|
is StatusCodeException -> {
|
||||||
val sb = StringBuilder()
|
buildString {
|
||||||
sb.append(appCtx.getString(R.string.error_bad_status_code, e.responseCode))
|
append(appCtx.getString(R.string.error_bad_status_code, e.responseCode))
|
||||||
if (e.isIdentifiedResponseCode) {
|
if (e.isIdentifiedResponseCode) {
|
||||||
sb.append(", ").append(e.message)
|
append(", ").append(e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sb.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is ProtocolException -> {
|
is ProtocolException -> {
|
||||||
@@ -60,8 +59,7 @@ object ExceptionUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is SocketException, is SSLException -> appCtx.getString(R.string.error_socket)
|
is SocketException, is SSLException -> appCtx.getString(R.string.error_socket)
|
||||||
is EhException -> e.message!!
|
else -> e.message ?: appCtx.getString(R.string.error_unknown)
|
||||||
else -> appCtx.getString(R.string.error_unknown)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.hippo.ehviewer.util
|
package com.hippo.ehviewer.util
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
fun ResponseBody.copyToFile(file: File) {
|
fun ResponseBody.copyToFile(file: File) {
|
||||||
file.outputStream().use { os ->
|
file.outputStream().use { os ->
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import arrow.atomic.AtomicInt
|
import arrow.atomic.AtomicInt
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import rikka.core.util.ContextUtils.requireActivity
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
@@ -58,7 +57,7 @@ private suspend fun <I, O> Context.awaitActivityResult(contract: ActivityResultC
|
|||||||
lifecycle.addObserver(observer)
|
lifecycle.addObserver(observer)
|
||||||
return withUIContext {
|
return withUIContext {
|
||||||
suspendCoroutine { cont -> // No cancellation support here since we cannot cancel a launched Intent
|
suspendCoroutine { cont -> // No cancellation support here since we cannot cancel a launched Intent
|
||||||
val activity = requireActivity<ComponentActivity>(this@awaitActivityResult)
|
val activity = findActivity<ComponentActivity>()
|
||||||
launcher = activity.activityResultRegistry.register(key, contract) {
|
launcher = activity.activityResultRegistry.register(key, contract) {
|
||||||
launcher?.unregister()
|
launcher?.unregister()
|
||||||
lifecycle.removeObserver(observer)
|
lifecycle.removeObserver(observer)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package com.hippo.ehviewer.util
|
|||||||
import kotlin.reflect.KMutableProperty0
|
import kotlin.reflect.KMutableProperty0
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer)
|
||||||
|
|
||||||
interface Lazy<T> {
|
interface Lazy<T> {
|
||||||
var value: T
|
var value: T
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
package com.hippo.ehviewer.util
|
package com.hippo.ehviewer.util
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.FileReader
|
import java.io.FileReader
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
object OSUtils {
|
object OSUtils {
|
||||||
private const val PROCFS_MEMFILE = "/proc/meminfo"
|
private const val PROCFS_MEMFILE = "/proc/meminfo"
|
||||||
|
|||||||
@@ -101,19 +101,19 @@ object ReadableTime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getShortTimeInterval(time: Long): String {
|
fun getShortTimeInterval(time: Long): String {
|
||||||
val sb = StringBuilder()
|
|
||||||
val resources = sResources
|
val resources = sResources
|
||||||
for (i in 0 until SIZE) {
|
return buildString {
|
||||||
val multiple = MULTIPLES[i]
|
for (i in 0 until SIZE) {
|
||||||
val quotient = time / multiple
|
val multiple = MULTIPLES[i]
|
||||||
if (time > multiple * 1.5 || i == SIZE - 1) {
|
val quotient = time / multiple
|
||||||
sb.append(quotient)
|
if (time > multiple * 1.5 || i == SIZE - 1) {
|
||||||
.append(" ")
|
append(quotient)
|
||||||
.append(resources!!.getQuantityString(UNITS[i], quotient.toInt()))
|
.append(" ")
|
||||||
break
|
.append(resources!!.getQuantityString(UNITS[i], quotient.toInt()))
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFilenamableTime(time: Long): String {
|
fun getFilenamableTime(time: Long): String {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.hippo.ehviewer.util
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
import com.hippo.ehviewer.Settings
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
fun WebView.setDefaultSettings() = settings.run {
|
fun WebView.setDefaultSettings() = settings.run {
|
||||||
builtInZoomControls = true
|
builtInZoomControls = true
|
||||||
displayZoomControls = false
|
displayZoomControls = false
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
userAgentString = CHROME_USER_AGENT
|
userAgentString = Settings.userAgent
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.hippo.ehviewer.util
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
|
inline fun buildWindowInsets(builderAction: WindowInsetsCompat.Builder.() -> Unit): WindowInsetsCompat {
|
||||||
|
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
|
||||||
|
val builder = WindowInsetsCompat.Builder()
|
||||||
|
builder.builderAction()
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WindowInsetsCompat.Builder.set(type: Int, insets: WindowInsets): WindowInsetsCompat.Builder {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
|
return setInsets(
|
||||||
|
type,
|
||||||
|
Insets.of(
|
||||||
|
insets.getLeft(density, layoutDirection),
|
||||||
|
insets.getTop(density),
|
||||||
|
insets.getRight(density, layoutDirection),
|
||||||
|
insets.getBottom(density),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.applyNavigationBarsPadding() {
|
||||||
|
applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -30,6 +31,7 @@ fun SliderPref(
|
|||||||
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
|
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
|
||||||
showValue: Boolean = false,
|
showValue: Boolean = false,
|
||||||
showInteger: Boolean = false,
|
showInteger: Boolean = false,
|
||||||
|
showTicks: Boolean = true,
|
||||||
steps: Int = 0,
|
steps: Int = 0,
|
||||||
textColor: Color = MaterialTheme.colorScheme.onBackground,
|
textColor: Color = MaterialTheme.colorScheme.onBackground,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
@@ -61,6 +63,14 @@ fun SliderPref(
|
|||||||
steps = steps,
|
steps = steps,
|
||||||
onValueChangeFinished = { onValueChangeFinished?.invoke(value) },
|
onValueChangeFinished = { onValueChangeFinished?.invoke(value) },
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
colors = if (showTicks) {
|
||||||
|
SliderDefaults.colors()
|
||||||
|
} else {
|
||||||
|
SliderDefaults.colors(
|
||||||
|
activeTickColor = Color.Transparent,
|
||||||
|
inactiveTickColor = Color.Transparent,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if (showValue) {
|
if (showValue) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+6
-10
@@ -8,12 +8,11 @@ import android.webkit.WebViewClient
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
import com.hippo.ehviewer.client.exception.CloudflareBypassException
|
import com.hippo.ehviewer.client.exception.CloudflareBypassException
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
|
|
||||||
class CloudflareInterceptor(context: Context) : WebViewInterceptor(context) {
|
class CloudflareInterceptor(context: Context) : WebViewInterceptor(context) {
|
||||||
private val executor = ContextCompat.getMainExecutor(context)
|
private val executor = ContextCompat.getMainExecutor(context)
|
||||||
@@ -27,14 +26,11 @@ class CloudflareInterceptor(context: Context) : WebViewInterceptor(context) {
|
|||||||
request: Request,
|
request: Request,
|
||||||
response: Response,
|
response: Response,
|
||||||
): Response {
|
): Response {
|
||||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
response.close()
|
||||||
// we don't crash the entire app
|
EhCookieStore.deleteCookie(request.url, COOKIE_NAME)
|
||||||
return runCatching {
|
resolveWithWebView(request)
|
||||||
response.close()
|
|
||||||
EhCookieStore.deleteCookie(request.url, COOKIE_NAME)
|
return chain.proceed(request)
|
||||||
resolveWithWebView(request)
|
|
||||||
chain.proceed(request)
|
|
||||||
}.getOrElse { throw IOException(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveWithWebView(originalRequest: Request) {
|
private fun resolveWithWebView(originalRequest: Request) {
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catches any uncaught exceptions from later in the chain and rethrows as a non-fatal
|
||||||
|
* IOException to avoid catastrophic failure.
|
||||||
|
*
|
||||||
|
* This should be the first interceptor in the client.
|
||||||
|
*
|
||||||
|
* See [Interceptor](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-interceptor/index.html)
|
||||||
|
*/
|
||||||
|
class UncaughtExceptionInterceptor : Interceptor {
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
return try {
|
||||||
|
chain.proceed(chain.request())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is IOException) {
|
||||||
|
throw e
|
||||||
|
} else {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,13 @@ import android.content.Context
|
|||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import com.hippo.ehviewer.util.setDefaultSettings
|
import com.hippo.ehviewer.util.setDefaultSettings
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
abstract class WebViewInterceptor(private val context: Context) : Interceptor {
|
abstract class WebViewInterceptor(private val context: Context) : Interceptor {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import android.webkit.MimeTypeMap
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.ChecksSdkIntAtLeast
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
@@ -59,6 +60,7 @@ import androidx.core.view.WindowCompat
|
|||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import com.hippo.ehviewer.BuildConfig
|
import com.hippo.ehviewer.BuildConfig
|
||||||
@@ -78,9 +80,12 @@ import com.hippo.ehviewer.util.ExceptionUtils
|
|||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.getParcelableCompat
|
import com.hippo.ehviewer.util.getParcelableCompat
|
||||||
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
||||||
|
import com.hippo.ehviewer.util.getValue
|
||||||
import com.hippo.ehviewer.util.isAtLeastO
|
import com.hippo.ehviewer.util.isAtLeastO
|
||||||
import com.hippo.ehviewer.util.isAtLeastP
|
import com.hippo.ehviewer.util.isAtLeastP
|
||||||
|
import com.hippo.ehviewer.util.lazyMut
|
||||||
import com.hippo.ehviewer.util.sendTo
|
import com.hippo.ehviewer.util.sendTo
|
||||||
|
import com.hippo.ehviewer.util.setValue
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
@@ -98,6 +103,12 @@ import eu.kanade.tachiyomi.util.view.copy
|
|||||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
import eu.kanade.tachiyomi.util.view.popupMenu
|
||||||
import eu.kanade.tachiyomi.util.view.setTooltip
|
import eu.kanade.tachiyomi.util.view.setTooltip
|
||||||
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
@@ -108,13 +119,17 @@ import kotlinx.coroutines.flow.merge
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import splitties.init.appCtx
|
||||||
import splitties.systemservices.clipboardManager
|
import splitties.systemservices.clipboardManager
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
class GalleryModel : ViewModel() {
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
var galleryProvider: PageLoader2? = null
|
||||||
import kotlin.coroutines.Continuation
|
override fun onCleared() {
|
||||||
import kotlin.coroutines.resume
|
super.onCleared()
|
||||||
import kotlin.math.abs
|
galleryProvider?.stop()
|
||||||
|
galleryProvider = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ReaderActivity : EhActivity() {
|
class ReaderActivity : EhActivity() {
|
||||||
lateinit var binding: ReaderActivityBinding
|
lateinit var binding: ReaderActivityBinding
|
||||||
@@ -124,6 +139,7 @@ class ReaderActivity : EhActivity() {
|
|||||||
private var mGalleryInfo: BaseGalleryInfo? = null
|
private var mGalleryInfo: BaseGalleryInfo? = null
|
||||||
private var mPage: Int = 0
|
private var mPage: Int = 0
|
||||||
private var mCacheFileName: String? = null
|
private var mCacheFileName: String? = null
|
||||||
|
private val vm: GalleryModel by viewModels()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the menu is currently visible.
|
* Whether the menu is currently visible.
|
||||||
@@ -159,7 +175,7 @@ class ReaderActivity : EhActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var mGalleryProvider: PageLoader2? = null
|
var mGalleryProvider by lazyMut { vm::galleryProvider }
|
||||||
private var mCurrentIndex: Int = 0
|
private var mCurrentIndex: Int = 0
|
||||||
private var mSavingPage = -1
|
private var mSavingPage = -1
|
||||||
private lateinit var builder: EditTextDialogBuilder
|
private lateinit var builder: EditTextDialogBuilder
|
||||||
@@ -190,13 +206,6 @@ class ReaderActivity : EhActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildProvider(replace: Boolean = false) {
|
private fun buildProvider(replace: Boolean = false) {
|
||||||
// TODO: Better solution for this dirty workaround
|
|
||||||
if (setOrientation(ReaderPreferences.defaultOrientationType().get())) {
|
|
||||||
// Orientation changed, we don't know if the activity will recreate, just recreate anyway
|
|
||||||
recreate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mGalleryProvider != null) {
|
if (mGalleryProvider != null) {
|
||||||
if (replace) mGalleryProvider!!.stop() else return
|
if (replace) mGalleryProvider!!.stop() else return
|
||||||
}
|
}
|
||||||
@@ -218,7 +227,7 @@ class ReaderActivity : EhActivity() {
|
|||||||
|
|
||||||
val continuation: AtomicReference<Continuation<String>?> = AtomicReference(null)
|
val continuation: AtomicReference<Continuation<String>?> = AtomicReference(null)
|
||||||
mGalleryProvider = ArchivePageLoader(
|
mGalleryProvider = ArchivePageLoader(
|
||||||
this,
|
appCtx,
|
||||||
mUri!!,
|
mUri!!,
|
||||||
flow {
|
flow {
|
||||||
if (!dialogShown) {
|
if (!dialogShown) {
|
||||||
@@ -271,7 +280,6 @@ class ReaderActivity : EhActivity() {
|
|||||||
|
|
||||||
private fun onInit() {
|
private fun onInit() {
|
||||||
handleIntent(intent)
|
handleIntent(intent)
|
||||||
buildProvider()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRestore(savedInstanceState: Bundle) {
|
private fun onRestore(savedInstanceState: Bundle) {
|
||||||
@@ -281,7 +289,6 @@ class ReaderActivity : EhActivity() {
|
|||||||
mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO)
|
mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO)
|
||||||
mPage = savedInstanceState.getInt(KEY_PAGE, -1)
|
mPage = savedInstanceState.getInt(KEY_PAGE, -1)
|
||||||
mCurrentIndex = savedInstanceState.getInt(KEY_CURRENT_INDEX)
|
mCurrentIndex = savedInstanceState.getInt(KEY_CURRENT_INDEX)
|
||||||
buildProvider()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
@@ -328,6 +335,7 @@ class ReaderActivity : EhActivity() {
|
|||||||
} else {
|
} else {
|
||||||
onRestore(savedInstanceState)
|
onRestore(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
buildProvider()
|
||||||
binding = ReaderActivityBinding.inflate(layoutInflater)
|
binding = ReaderActivityBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
builder = EditTextDialogBuilder(this, null, getString(R.string.archive_passwd))
|
builder = EditTextDialogBuilder(this, null, getString(R.string.archive_passwd))
|
||||||
@@ -338,6 +346,7 @@ class ReaderActivity : EhActivity() {
|
|||||||
|
|
||||||
mGalleryProvider.let {
|
mGalleryProvider.let {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lifecycleScope.launchIO {
|
lifecycleScope.launchIO {
|
||||||
@@ -368,7 +377,6 @@ class ReaderActivity : EhActivity() {
|
|||||||
updateViewerInset(ReaderPreferences.fullscreen().get())
|
updateViewerInset(ReaderPreferences.fullscreen().get())
|
||||||
binding.viewerContainer.removeAllViews()
|
binding.viewerContainer.removeAllViews()
|
||||||
setOrientation(ReaderPreferences.defaultOrientationType().get())
|
setOrientation(ReaderPreferences.defaultOrientationType().get())
|
||||||
updateOrientationShortcut(ReaderPreferences.defaultOrientationType().get())
|
|
||||||
binding.viewerContainer.addView(viewer?.getView())
|
binding.viewerContainer.addView(viewer?.getView())
|
||||||
viewer?.setGalleryProvider(mGalleryProvider!!)
|
viewer?.setGalleryProvider(mGalleryProvider!!)
|
||||||
moveToPageIndex(mCurrentIndex)
|
moveToPageIndex(mCurrentIndex)
|
||||||
@@ -379,10 +387,6 @@ class ReaderActivity : EhActivity() {
|
|||||||
config = null
|
config = null
|
||||||
viewer?.destroy()
|
viewer?.destroy()
|
||||||
viewer = null
|
viewer = null
|
||||||
if (mGalleryProvider != null) {
|
|
||||||
mGalleryProvider!!.stop()
|
|
||||||
mGalleryProvider = null
|
|
||||||
}
|
|
||||||
readerSettingSheetDialog?.dismiss()
|
readerSettingSheetDialog?.dismiss()
|
||||||
readerSettingSheetDialog = null
|
readerSettingSheetDialog = null
|
||||||
}
|
}
|
||||||
@@ -856,15 +860,13 @@ class ReaderActivity : EhActivity() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Forces the user preferred [orientation] on the activity.
|
* Forces the user preferred [orientation] on the activity.
|
||||||
* Returns true if a restart is required.
|
|
||||||
*/
|
*/
|
||||||
private fun setOrientation(orientation: Int): Boolean {
|
private fun setOrientation(orientation: Int) {
|
||||||
val newOrientation = OrientationType.fromPreference(orientation)
|
val newOrientation = OrientationType.fromPreference(orientation)
|
||||||
val restart = newOrientation.flag != requestedOrientation
|
if (newOrientation.flag != requestedOrientation) {
|
||||||
if (restart) {
|
|
||||||
requestedOrientation = newOrientation.flag
|
requestedOrientation = newOrientation.flag
|
||||||
}
|
}
|
||||||
return restart
|
updateOrientationShortcut(ReaderPreferences.defaultOrientationType().get())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
@@ -27,6 +27,7 @@ class WebtoonLayoutManager(activity: ReaderActivity) : LinearLayoutManager(activ
|
|||||||
/**
|
/**
|
||||||
* Returns the custom extra layout space.
|
* Returns the custom extra layout space.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
|
override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
|
||||||
return extraLayoutSpace
|
return extraLayoutSpace
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import androidx.core.graphics.blue
|
|||||||
import androidx.core.graphics.green
|
import androidx.core.graphics.green
|
||||||
import androidx.core.graphics.red
|
import androidx.core.graphics.red
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
|
||||||
import splitties.systemservices.windowManager
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import com.hippo.ehviewer.Settings as AppSettings
|
import com.hippo.ehviewer.Settings as AppSettings
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import splitties.systemservices.windowManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the color for the given attribute.
|
* Returns the color for the given attribute.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package moe.tarsin.coroutines
|
package moe.tarsin.coroutines
|
||||||
|
|
||||||
import arrow.core.memoize
|
import arrow.core.memoize
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlin.contracts.InvocationKind
|
import kotlin.contracts.InvocationKind
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
|
||||||
class NamedMutex<T> {
|
class NamedMutex<T> {
|
||||||
val innerMutexGetter = { _: T -> Mutex() }.memoize()
|
val innerMutexGetter = { _: T -> Mutex() }.memoize()
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package rikka.core.res
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
fun Resources.Theme.resolveColor(@AttrRes attrId: Int): Int {
|
||||||
|
val a = obtainStyledAttributes(intArrayOf(attrId))
|
||||||
|
val res = a.getColor(0, 0)
|
||||||
|
a.recycle()
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Resources.Theme.resolveDrawable(@AttrRes attrId: Int): Drawable? {
|
||||||
|
val a = obtainStyledAttributes(intArrayOf(attrId))
|
||||||
|
val res = a.getDrawable(0)
|
||||||
|
a.recycle()
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright 2016 Hippo Seven
|
|
||||||
~
|
|
||||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
~ you may not use this file except in compliance with the License.
|
|
||||||
~ You may obtain a copy of the License at
|
|
||||||
~
|
|
||||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
~
|
|
||||||
~ Unless required by applicable law or agreed to in writing, software
|
|
||||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
~ See the License for the specific language governing permissions and
|
|
||||||
~ limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="120dp"
|
|
||||||
android:height="120dp"
|
|
||||||
android:tint="?colorTertiary"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="@string/pd_big_history" />
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright 2022 Tarsin Norbin
|
|
||||||
~
|
|
||||||
~ This file is part of EhViewer
|
|
||||||
~
|
|
||||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
|
||||||
~ under the terms of the GNU General Public License as published
|
|
||||||
~ by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License along with EhViewer.
|
|
||||||
~ If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M2,17h2v0.5L3,17.5v1h1v0.5L2,19v1h3v-4L2,16v1zM3,8h1L4,4L2,4v1h1v3zM2,11h1.8L2,13.1v0.9h3v-1L3.2,13L5,10.9L5,10L2,10v1zM7,5v2h14L21,5L7,5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright 2019 Hippo Seven
|
|
||||||
~
|
|
||||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
~ you may not use this file except in compliance with the License.
|
|
||||||
~ You may obtain a copy of the License at
|
|
||||||
~
|
|
||||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
~
|
|
||||||
~ Unless required by applicable law or agreed to in writing, software
|
|
||||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
~ See the License for the specific language governing permissions and
|
|
||||||
~ limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="@string/pd_eh_subscription" />
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright 2022 Tarsin Norbin
|
|
||||||
~
|
|
||||||
~ This file is part of EhViewer
|
|
||||||
~
|
|
||||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
|
||||||
~ under the terms of the GNU General Public License as published
|
|
||||||
~ by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License along with EhViewer.
|
|
||||||
~ If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
~ Copyright 2016 Hippo Seven
|
|
||||||
~
|
|
||||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
~ you may not use this file except in compliance with the License.
|
|
||||||
~ You may obtain a copy of the License at
|
|
||||||
~
|
|
||||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
~
|
|
||||||
~ Unless required by applicable law or agreed to in writing, software
|
|
||||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
~ See the License for the specific language governing permissions and
|
|
||||||
~ limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="@string/pd_fire" />
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user