Compare commits

...
101 Commits
Author SHA1 Message Date
FooIbar 7e90555bed Release 1.8.11.1
Releases / Build (push) Has been cancelled
2023-11-01 23:32:31 +08:00
FooIbar d36c189430 Update xz to v5.4.5 2023-11-01 23:29:48 +08:00
FooIbar 61e2ace5c8 Fix insets on API < 29 2023-11-01 23:17:46 +08:00
Lokesh Gidra d6a3f21754 Fix a weird NPE 2023-11-01 22:12:07 +08:00
Lokesh Gidra 4e54d7f66f Refine FavouriteStatusRouter observable stateFlow 2023-11-01 22:12:07 +08:00
Lokesh Gidra f2dff038cd Global, CompositionLocal DialogState
Signed-off-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-01 19:37:22 +08:00
Lokesh Gidra a7b30640e0 Drop DrawerView
Signed-off-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-01 19:37:22 +08:00
FooIbar 45048b1f46 [Marshmallow] Fix crash when saving images 2023-11-01 16:26:06 +08:00
FooIbar 498129314a Refine storage permission request 2023-11-01 15:42:55 +08:00
Officer 3ee4b4a61e Cleanup ReaderActivity & Fix activity not finish when no archive passwd available 2023-11-01 15:41:23 +08:00
FooIbar ee59c0f22e Convert ReaderActivity dialog to use a DialogState 2023-11-01 12:30:36 +08:00
FooIbar c56134cd92 Fix API results parsing error on certain galleries 2023-11-01 12:17:48 +08:00
renovate[bot] b3d1b66a06 Update dependency com.google.devtools.ksp to v1.9.20-1.0.13 2023-11-01 03:49:48 +00:00
renovate[bot] c9bb81bf63 Update kotlin to v1.9.20 2023-11-01 02:41:28 +00:00
FooIbar d176810dae Update compose compiler for kotlin 1.9.20 2023-11-01 10:38:36 +08:00
FooIbar 17bdca71b1 Reduce drawer and history item gesture sensitivity 2023-11-01 09:50:25 +08:00
FooIbar 22fd3730df Sync with upstream changes and cleanup code 2023-11-01 02:37:34 +08:00
FooIbar 23d1271949 Prepare for next iteration 2023-10-31 13:30:29 +08:00
FooIbar bf6aa3e8ae Release 1.8.11.0
Releases / Build (push) Has been cancelled
2023-10-31 12:49:53 +08:00
FooIbar 75fcfde892 Always enable gestures if drawer is open 2023-10-31 11:58:13 +08:00
FooIbar 89be27995e Fix #154 regression (#158)
* Revert "Fix crash when deleting filters"

This reverts commit 717d3717ba.

* Revert "Update dependency dev.chrisbanes.compose:compose-bom to v2023.11.00-alpha02"

This reverts commit 38c18149

Reason for revert: https://issuetracker.google.com/issues/295745063#comment57
2023-10-31 03:45:25 +00:00
FooIbar 6738dc6cdc Partially enable drawer gestures for now 2023-10-31 11:34:14 +08:00
renovate[bot] 9ff0decb98 Update dependency io.coil-kt:coil-bom to v2.5.0 2023-10-30 23:02:14 +00:00
FooIbar 717d3717ba Fix crash when deleting filters 2023-10-31 03:25:25 +08:00
FooIbar fdbca869a8 Allow hiding comments below custom threshold 2023-10-31 00:44:32 +08:00
Astesia aafa750ca3 change runnable to coroutine job, and fix fragment compose memory leak (#152) 2023-10-30 22:39:29 +08:00
Astesia afe2bac672 Fix fragment compose view memory leak (#151)
* fix fragment compose view memory leak

* chang wildcard import
2023-10-30 20:07:37 +08:00
FooIbar b6f5d72db5 Fix NPE when evicting cache but Image was recycled
But why was it recycled?
2023-10-30 18:00:37 +08:00
FooIbar f0021a7601 Fix tag filters not working in thumbnail mode 2023-10-30 17:22:31 +08:00
FooIbar ea5bfbb346 Fix ViewModel memory leak 2023-10-30 12:32:11 +08:00
FooIbar 38fbcf4457 Update issue template 2023-10-30 00:48:24 +08:00
FooIbar 7c43e8f349 Avoid uncaught exceptions crashing entire app 2023-10-30 00:30:01 +08:00
FooIbar 93df4e3dd9 Revert "Scroll to top after refreshing" (#139)
* Revert "Fix progress not showing after clicking error tip"

This reverts commit bc914566dd.

* Revert "Try to fix recyclerview disappearing after refresh (#61)"

This reverts commit f9a3c3d266.

* Revert "Scroll to top after refreshing"

This reverts commit b0896ea465.
2023-10-29 14:44:42 +00:00
fricyo f7d45a52a7 Fix a strange word 2023-10-29 21:01:21 +08:00
FooIbar 328f96d115 Fix GalleryPreviewScreen insets 2023-10-29 20:57:01 +08:00
fricyo 22338a72d8 Complete the missing translations (#131)
* Complete the missing translations

* Remove the duplicate

* Update ""settings_eh_force_eh_thumb_summary""
2023-10-29 17:18:03 +08:00
FooIbar 256dedcb7c Add note for forceEhThumb 2023-10-29 16:36:49 +08:00
FooIbar 95d2438b84 Fix state recalculation after recompose 2023-10-29 16:19:59 +08:00
FooIbar f8d27e165e Update UI state on main thread and fix weight 2023-10-29 16:19:59 +08:00
FooIbar e2f024ae29 Fix reader not loading under certain circumstances (#128)
* Revert "Fix reader bug on certain orientation settings"

This reverts commit 6e59583dea.

* Revert "Skip `buildProvider` if the activity will restart"

This reverts commit a880f801fb.

* Preserve gallery provider in view model
2023-10-29 03:24:24 +08:00
FooIbar 9c28fbeaf8 Allow custom user agent string 2023-10-28 22:34:00 +08:00
FooIbar 6458eb090a Add missing color filter modes 2023-10-28 20:39:16 +08:00
FooIbar f1ca2b6958 Add animations for category chips 2023-10-28 19:34:32 +08:00
FooIbar 60f031e7e3 Fix crash when navController is not initialized 2023-10-28 13:43:32 +08:00
FooIbar e6a44ee0e1 Clarify system requirement for Marshmallow flavor 2023-10-28 02:49:27 +08:00
FooIbar 7a3a641a07 Fix crash when copying cookies 2023-10-28 02:10:13 +08:00
FooIbar 1021efb239 Fix NPE caused by coroutine cancellation 2023-10-27 23:35:23 +08:00
FooIbar edbe6509e1 Remember last viewed toplist 2023-10-27 20:37:20 +08:00
FooIbar 44a6253bfc Sync with upstream API changes 2023-10-27 19:40:41 +08:00
FooIbar 3d6e5df71e Leave 56 dp minimum edge gap for navigation drawer 2023-10-27 13:09:11 +08:00
FooIbar 2733d2e58c Convert more icons to ImageVector 2023-10-27 11:43:47 +08:00
FooIbar 0c78673870 Fix unexpected touch scale behavior in PageViewer 2023-10-27 03:11:00 +08:00
FooIbar d618948429 Set min width and max items in each row (#99)
* Set category chip to fixed width

* Set min width and max items in each row
2023-10-26 17:54:56 +00:00
FooIbar 7584704d22 Avoid race condition when updating tag database
`updateInternal` and `issueUpdateInMemoryData` will try to create tag
database directory at the same time on first launch and one of them will
fail, causing app crash
2023-10-27 00:10:59 +08:00
FooIbar 4a4658cc54 Dispatch and apply system bars insets separately 2023-10-26 22:43:43 +08:00
FooIbar e9875f1a24 Use lazy initialization instead 2023-10-26 21:01:01 +08:00
FooIbar cc11257d80 Suppress lint 2023-10-26 20:17:28 +08:00
FooIbar 4fd64601ef Use ImageVector icons 2023-10-26 19:45:29 +08:00
FooIbar 7eea490adf Drop RikkaX 2023-10-26 19:29:57 +08:00
FooIbar b2b12f7b93 Migrate from RikkaX Insets to Insetter 2023-10-26 13:22:23 +08:00
renovate[bot] 66d026cffa Update kotlin to v1.9.20-RC2 2023-10-26 04:12:31 +00:00
FooIbar 5a15a20dd6 Update compose compiler for kotlin 1.9.20-RC2 2023-10-26 12:09:43 +08:00
renovate[bot] 70450a9725 Update dependency com.google.devtools.ksp to v1.9.20-RC2-1.0.13 2023-10-26 00:43:46 +00:00
FooIbar 470a3c6a09 Move search fab to GalleryListScene 2023-10-26 00:03:04 +08:00
FooIbar 15f8b2f15d Cleanup code 2023-10-26 00:02:20 +08:00
FooIbar 717cbe7630 Request recomposition after view gets created 2023-10-25 23:51:09 +08:00
FooIbar d34075d29d Apply ime padding to suggestion list 2023-10-25 23:51:09 +08:00
FooIbar 80d4d66fb5 Dispatch insets from compose to android view 2023-10-25 23:51:09 +08:00
FooIbar 6b20b46761 Revert "Set bar style correctly"
This reverts 216a043094.

Reason for revert: Incorrect status bar color when app's night mode
setting is different from system's
2023-10-25 21:29:26 +08:00
FooIbar cd764a4a3c Fix spotless 2023-10-25 18:29:44 +08:00
FooIbar 5422b3d8bd Disable edge to edge temporarily 2023-10-25 17:42:32 +08:00
FooIbar 0273390d79 Flatten layout 2023-10-25 15:36:30 +08:00
FooIbar 8f7242b939 Bring search fab in front of search layout 2023-10-25 15:26:43 +08:00
FooIbar 246dee63b4 Move Context.findActivity to separated file 2023-10-25 13:24:28 +08:00
FooIbar 697918a73a Move navItems to top level 2023-10-25 13:11:21 +08:00
FooIbar 67e480220e Refine navigation drawer 2023-10-25 12:58:44 +08:00
FooIbar 5784eb008e Migrate navigation drawer to compose (#80)
* Drop AboveSnackerLayout

* Migrate navigation drawer to compose

* Suspend until logged in before loading

* Use AndroidViewBinding

* fixup! Migrate navigation drawer to compose

* Cleanup AboveSnackView usage

* Cleanup res

* Cleanup dependencies
2023-10-25 01:53:44 +08:00
FooIbar 82d66c4d57 Prepare for next iteration 2023-10-25 01:53:13 +08:00
FooIbar ab8671cc3b Revert "Exclude some libraries from aboutLibraries"
This reverts commit bc34f25a3c.

Reason for revert: Licenses should be included as per Apache 2.0
2023-10-24 23:53:25 +08:00
FooIbar 2e52dda208 Downgrade AGP to stable version but use newer lint (#81)
* Downgrade AGP to stable version but use newer lint

* Downgrade Java to 17 and use newer r8
2023-10-24 13:08:23 +00:00
renovate[bot] 1ab08d0ae1 Update Rust crate quick-xml to 0.31 2023-10-23 20:22:16 +00:00
FooIbar 702be0ed43 Use eh API as well if forceEhThumb is enabled 2023-10-23 22:37:55 +08:00
FooIbar 4067543701 Cleanup code 2023-10-23 16:03:01 +08:00
FooIbar 074689c60e Fall back if failed to retrieve data from API 2023-10-23 15:10:18 +08:00
FooIbar 2beb715df3 Cleanup code 2023-10-23 14:36:28 +08:00
FooIbar 7ea2597899 Fix large torrent list parsing 2023-10-23 02:25:17 +08:00
FooIbar 34a8f92865 Show meaningful error message if it's available 2023-10-23 02:25:17 +08:00
FooIbar 6e2a2de13f Reformat imports alphabetically
https://developer.android.com/kotlin/style-guide#import_statements
2023-10-22 22:13:45 +08:00
FooIbar e073232352 Fix cronet support detection 2023-10-22 17:52:16 +08:00
FooIbar 6f49b4d23e Fix deprecated 2023-10-21 18:22:45 +08:00
FooIbar 97cad20ab1 It's affected by AGP and NDK versions as well 2023-10-21 17:38:29 +08:00
FooIbar 9300a3d9ce Don't write cmake cache every time 2023-10-21 16:26:14 +08:00
FooIbar 28b7f80a8f Fix content behind navigation bar 2023-10-21 16:13:27 +08:00
FooIbar a06d23753a Lock navi drawer on where doesn't have navi icon 2023-10-21 13:10:46 +08:00
FooIbar 98d66b3e6c Release 1.8.10.1
Releases / Build (push) Has been cancelled
2023-10-21 00:14:03 +08:00
FooIbar 6e59583dea Fix reader bug on certain orientation settings
Resolve #68
2023-10-21 00:12:52 +08:00
FooIbar 2f0571e384 Implement splash screen 2023-10-20 17:36:59 +08:00
renovate[bot] 64a6074e02 Update dependency org.jsoup:jsoup to v1.16.2 2023-10-20 06:11:31 +00:00
renovate[bot] 0b7cb4cf41 Update dependency com.android.application to v8.3.0-alpha09 2023-10-20 01:48:36 +00:00
renovate[bot] e620832e33 Update dependency com.google.android.material:material to v1.11.0-beta01 2023-10-19 23:03:41 +00:00
renovate[bot] f82e4af0e3 Update dependency com.android.application to v8.3.0-alpha08 2023-10-19 20:36:34 +00:00
154 changed files with 1730 additions and 2099 deletions

No files matched your search

+1
View File
@@ -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
+10 -1
View File
@@ -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:
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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 |
+4 -15
View File
@@ -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.0" versionName = "1.8.11.1"
resourceConfigurations.addAll( resourceConfigurations.addAll(
listOf( listOf(
"zh", "zh",
@@ -209,6 +209,7 @@ dependencies {
implementation(libs.bundles.compose) implementation(libs.bundles.compose)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.constraintlayout.compose) implementation(libs.androidx.constraintlayout.compose)
implementation(libs.androidx.coordinatorlayout) implementation(libs.androidx.coordinatorlayout)
@@ -230,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
@@ -248,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)
@@ -275,7 +274,7 @@ dependencies {
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(17)
} }
ksp { ksp {
@@ -286,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
View File
@@ -44,6 +44,7 @@
android:configChanges="screenSize" android:configChanges="screenSize"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/AppTheme.Splash"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"> android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
+1 -1
View File
@@ -24,7 +24,7 @@ set(BUILD_TESTING 0)
FetchContent_Declare(liblzma FetchContent_Declare(liblzma
GIT_REPOSITORY https://github.com/FooIbar/xz.git GIT_REPOSITORY https://github.com/FooIbar/xz.git
GIT_TAG v5.4.4 GIT_TAG v5.4.5
) )
FetchContent_MakeAvailable(liblzma) FetchContent_MakeAvailable(liblzma)
@@ -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
} }
@@ -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
} }
@@ -34,7 +34,6 @@ import com.hippo.ehviewer.client.parser.GalleryApiParser
import com.hippo.ehviewer.client.parser.GalleryDetailParser import com.hippo.ehviewer.client.parser.GalleryDetailParser
import com.hippo.ehviewer.client.parser.GalleryListParser import com.hippo.ehviewer.client.parser.GalleryListParser
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
import com.hippo.ehviewer.client.parser.GalleryPageApiParser
import com.hippo.ehviewer.client.parser.GalleryPageParser import com.hippo.ehviewer.client.parser.GalleryPageParser
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
import com.hippo.ehviewer.client.parser.HomeParser import com.hippo.ehviewer.client.parser.HomeParser
@@ -54,6 +53,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 +71,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 +116,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)
} }
} }
} }
@@ -371,7 +365,7 @@ object EhEngine {
pToken: String, pToken: String,
showKey: String?, showKey: String?,
previousPToken: String?, previousPToken: String?,
): GalleryPageApiParser.Result { ): GalleryPageParser.Result {
val referer = if (index > 0 && previousPToken != null) EhUrl.getPageUrl(gid, index - 1, previousPToken) else null val referer = if (index > 0 && previousPToken != null) EhUrl.getPageUrl(gid, index - 1, previousPToken) else null
return ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) { return ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
jsonBody { jsonBody {
@@ -381,7 +375,7 @@ object EhEngine {
put("imgkey", pToken) put("imgkey", pToken)
put("showkey", showKey) put("showkey", showKey)
} }
}.executeAndParsingWith(GalleryPageApiParser::parse) }.executeAndParsingWith { GalleryPageParser.parse(filterNot { it == '\\' }) }
} }
suspend fun rateGallery( suspend fun rateGallery(
@@ -490,7 +484,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")
@@ -33,7 +33,7 @@ object GalleryApiParser {
titleJpn = item.titleJpn.unescapeXml() titleJpn = item.titleJpn.unescapeXml()
category = getCategory(item.category) category = getCategory(item.category)
thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!) thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!)
uploader = item.uploader.unescapeXml() uploader = item.uploader?.unescapeXml()
posted = ParserUtils.formatDate(item.posted * 1000) posted = ParserUtils.formatDate(item.posted * 1000)
rating = item.rating rating = item.rating
simpleTags = item.tags simpleTags = item.tags
@@ -54,7 +54,8 @@ object GalleryApiParser {
val titleJpn: String, val titleJpn: String,
val category: String, val category: String,
val thumb: String, val thumb: String,
val uploader: String, // Null in some old galleries
val uploader: String?,
val posted: Long, val posted: Long,
@SerialName("filecount") @SerialName("filecount")
val pages: Int, val pages: Int,
@@ -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
} }
} }
@@ -1,58 +0,0 @@
/*
* 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.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.unescapeXml
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object GalleryPageApiParser {
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
fun parse(body: String) = runCatching {
val res = body.parseAs<JsonResult>()
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
groupValues[1].trim().unescapeXml()
}
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKey)?.run {
groupValues[1].trim().unescapeXml()
}
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.originImageUrl)?.run {
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
}
check(!imageUrl.isNullOrEmpty())
Result(imageUrl, skipHathKey, originImageUrl)
}.getOrElse { e ->
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error) }
throw ParseException("Parse image url and skip hath key error", e)
}
@Serializable
private data class JsonResult(
@SerialName("i3")
val imageUrl: String,
@SerialName("i6")
val skipHathKey: String,
@SerialName("i7")
val originImageUrl: String,
)
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
}
@@ -17,46 +17,32 @@ package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml import com.hippo.ehviewer.util.unescapeXml
import java.util.regex.Pattern
object GalleryPageParser { object GalleryPageParser {
private val PATTERN_IMAGE_URL = Pattern.compile("<img[^>]*src=\"([^\"]+)\" style") private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^)]+)'\\)") private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([\\d-]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL = private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+/fullimg/[^\"]+)\">")
Pattern.compile("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
// TODO Not sure about the size of show keys // TODO Not sure about the size of show keys
private val PATTERN_SHOW_KEY = Pattern.compile("var showkey=\"([0-9a-z]+)\";") private val PATTERN_SHOW_KEY = Regex("var showkey=\"([0-9a-z]+)\";")
fun parse(body: String): Result { fun parse(body: String): Result {
var m = PATTERN_IMAGE_URL.matcher(body) val imageUrl = PATTERN_IMAGE_URL.find(body)?.run {
val imageUrl = if (m.find()) { groupValues[1].unescapeXml()
m.group(1)!!.trim().unescapeXml()
} else {
null
} }
m = PATTERN_SKIP_HATH_KEY.matcher(body) val skipHathKey = PATTERN_SKIP_HATH_KEY.find(body)?.run {
val skipHathKey = if (m.find()) { groupValues[1]
m.group(1)!!.trim().unescapeXml()
} else {
null
} }
m = PATTERN_ORIGIN_IMAGE_URL.matcher(body) val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(body)?.run {
val originImageUrl = if (m.find()) { groupValues[1].unescapeXml()
m.group(1)!!.unescapeXml() + "fullimg.php" + m.group(2)!!.unescapeXml()
} else {
null
} }
m = PATTERN_SHOW_KEY.matcher(body) val showKey = PATTERN_SHOW_KEY.find(body)?.run {
val showKey = if (m.find()) { groupValues[1]
m.group(1)
} else {
null
} }
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) { return if (!imageUrl.isNullOrEmpty()) {
Result(imageUrl, skipHathKey, originImageUrl, showKey) Result(imageUrl, skipHathKey, originImageUrl, showKey)
} else { } else {
throw ParseException("Parse image url and show error") throw ParseException("Parse image url error")
} }
} }
@@ -64,6 +50,6 @@ object GalleryPageParser {
val imageUrl: String, val imageUrl: String,
val skipHathKey: String?, val skipHathKey: String?,
val originImageUrl: String?, val originImageUrl: String?,
val showKey: String, val showKey: 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
@@ -42,15 +43,16 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore 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 { typealias PasswdInvalidator = (String) -> Boolean
typealias PasswdProvider = suspend (PasswdInvalidator) -> String
class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider: PasswdProvider) : PageLoader2(), CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job() override val coroutineContext = Dispatchers.IO + Job()
private lateinit var pfd: ParcelFileDescriptor private lateinit var pfd: ParcelFileDescriptor
private val hostJob = launch(start = CoroutineStart.LAZY) { private val hostJob = launch(start = CoroutineStart.LAZY) {
@@ -65,12 +67,8 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdFlow: Flow
it ?: return@forEach it ?: return@forEach
if (providePassword(it)) return@launch if (providePassword(it)) return@launch
} }
passwdFlow.collect { val toAdd = passwdProvider { providePassword(it) }
if (providePassword(it)) { archivePasswds = archivePasswds?.toMutableSet()?.apply { add(toAdd) } ?: setOf(toAdd)
archivePasswds = archivePasswds?.toMutableSet()?.apply { add(it) } ?: setOf(it)
currentCoroutineContext().cancel()
}
}
} }
} }
@@ -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,58 +25,112 @@ 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.customview.widget.Openable import androidx.compose.foundation.Image
import androidx.drawerlayout.widget.DrawerLayout 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.view.ViewCompat
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.ui.tools.LocalTouchSlopProvider
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 {
@@ -96,17 +149,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()
} }
@@ -160,57 +211,108 @@ 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()
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 LocalTouchSlopProvider(3f) {
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) {
@@ -218,7 +320,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) {
@@ -280,7 +385,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,
) )
@@ -296,25 +401,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) {
@@ -339,7 +438,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,
) )
@@ -353,42 +452,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) {
@@ -399,17 +464,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
@@ -417,20 +484,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) {}
}
} }
@@ -3,9 +3,31 @@ package com.hippo.ehviewer.ui
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import com.google.accompanist.themeadapter.material3.Mdc3Theme import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LocalDialogState
// To make sure compose theme and legacy view theme are in sync, we only use Mdc3Theme // To make sure compose theme and legacy view theme are in sync, we only use Mdc3Theme
fun ComposeView.setMD3Content(content: @Composable () -> Unit) = setContent { Mdc3Theme(content = content) }
fun ComponentActivity.setMD3Content(content: @Composable () -> Unit) = setContent { Mdc3Theme(content = content) } fun ComposeView.setMD3Content(content: @Composable () -> Unit) = setContent {
Mdc3Theme {
val dialogState = remember { DialogState() }
dialogState.Intercept()
CompositionLocalProvider(LocalDialogState provides dialogState) {
content()
}
}
}
fun ComponentActivity.setMD3Content(content: @Composable () -> Unit) = setContent {
Mdc3Theme {
val dialogState = remember { DialogState() }
dialogState.Intercept()
CompositionLocalProvider(LocalDialogState provides dialogState) {
content()
}
}
}
@@ -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()
}
}
}
}
}
@@ -1,53 +0,0 @@
/*
* 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.
*/
package com.hippo.ehviewer.ui.legacy
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
class DrawerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : FrameLayout(context, attrs) {
private var mMaxWidth = 0
init {
val a = context.obtainStyledAttributes(attrs, SIZE_ATTRS)
mMaxWidth = a.getDimensionPixelOffset(0, 0)
a.recycle()
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
var widthSpec = widthSpec
when (MeasureSpec.getMode(widthSpec)) {
MeasureSpec.EXACTLY -> {}
MeasureSpec.AT_MOST -> widthSpec = MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(widthSpec).coerceAtMost(mMaxWidth),
MeasureSpec.EXACTLY,
)
MeasureSpec.UNSPECIFIED ->
widthSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY)
}
// Let super sort out the height
super.onMeasure(widthSpec, heightSpec)
}
companion object {
private val SIZE_ATTRS = intArrayOf(android.R.attr.maxWidth)
}
}
@@ -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
},
) )
} }
} }
@@ -59,15 +59,15 @@ import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.FINISH_ROUTE_NAME import com.hippo.ehviewer.ui.FINISH_ROUTE_NAME
import com.hippo.ehviewer.ui.LocalNavController import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.SELECT_SITE_ROUTE_NAME import com.hippo.ehviewer.ui.SELECT_SITE_ROUTE_NAME
import com.hippo.ehviewer.ui.tools.rememberDialogState import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.util.ExceptionUtils 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) {
@@ -87,8 +87,7 @@ fun CookieSignInScene(windowSizeClass: WindowSizeClass) {
var signInJob by remember { mutableStateOf<Job?>(null) } var signInJob by remember { mutableStateOf<Job?>(null) }
val dialogState = rememberDialogState() val dialogState = LocalDialogState.current
dialogState.Intercept()
val noCookies = stringResource(R.string.from_clipboard_error) val noCookies = stringResource(R.string.from_clipboard_error)
@@ -101,7 +100,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
@@ -68,7 +68,7 @@ import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.SELECT_SITE_ROUTE_NAME import com.hippo.ehviewer.ui.SELECT_SITE_ROUTE_NAME
import com.hippo.ehviewer.ui.WEBVIEW_SIGN_IN_ROUTE_NAME import com.hippo.ehviewer.ui.WEBVIEW_SIGN_IN_ROUTE_NAME
import com.hippo.ehviewer.ui.openBrowser import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.tools.rememberDialogState import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.util.ExceptionUtils 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
@@ -88,9 +88,7 @@ fun SignInScreen(windowSizeClass: WindowSizeClass) {
var passwordHidden by rememberSaveable { mutableStateOf(true) } var passwordHidden by rememberSaveable { mutableStateOf(true) }
val context = LocalContext.current val context = LocalContext.current
var signInJob by remember { mutableStateOf<Job?>(null) } var signInJob by remember { mutableStateOf<Job?>(null) }
val dialogState = LocalDialogState.current
val dialogState = rememberDialogState()
dialogState.Intercept()
BackHandler { BackHandler {
(context as Activity).moveTaskToBack(true) (context as Activity).moveTaskToBack(true)
@@ -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),
) )
@@ -67,11 +67,8 @@ fun GalleryInfoListItem(
modifier = Modifier.aspectRatio(DEFAULT_ASPECT).fillMaxSize(), modifier = Modifier.aspectRatio(DEFAULT_ASPECT).fillMaxSize(),
) )
} }
val showFav by produceState(false, info.gid) { val showFav by FavouriteStatusRouter.collectAsState(info) {
value = info.favoriteSlot != NOT_FAVORITED && !isInFavScene it != NOT_FAVORITED && !isInFavScene
FavouriteStatusRouter.stateFlow(info.gid).collect {
value = it != NOT_FAVORITED && !isInFavScene
}
} }
ConstraintLayout(modifier = Modifier.padding(8.dp, 4.dp).fillMaxSize()) { ConstraintLayout(modifier = Modifier.padding(8.dp, 4.dp).fillMaxSize()) {
val (titleRef, uploaderRef, ratingRef, categoryRef, postedRef, favRef, iconsRef) = createRefs() val (titleRef, uploaderRef, ratingRef, categoryRef, postedRef, favRef, iconsRef) = createRefs()
@@ -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)
@@ -21,10 +21,8 @@ import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.app.DownloadManager import android.app.DownloadManager
import android.content.DialogInterface import android.content.DialogInterface
import android.content.pm.PackageManager
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -32,7 +30,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
@@ -82,7 +79,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -91,13 +87,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.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@@ -136,6 +130,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
@@ -170,8 +165,12 @@ import com.hippo.ehviewer.util.IntList
import com.hippo.ehviewer.util.ReadableTime import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.addTextToClipboard import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.getParcelableCompat import com.hippo.ehviewer.util.getParcelableCompat
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.requestPermission
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
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 +178,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
@@ -198,20 +195,6 @@ class GalleryDetailScene : BaseScene() {
private var getDetailError by mutableStateOf("") private var getDetailError by mutableStateOf("")
private var mTorrentList: TorrentResult? = null private var mTorrentList: TorrentResult? = null
private var requestStoragePermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { result: Boolean ->
if (result && composeBindingGD != null) {
val helper = TorrentListDialogHelper()
val binding = DialogTorrentListBinding.inflate(layoutInflater)
val dialog: Dialog = BaseDialogBuilder(requireActivity())
.setTitle(R.string.torrents)
.setView(binding.root)
.setOnDismissListener(helper)
.show()
helper.setDialog(dialog, binding, composeBindingGD!!.torrentUrl)
}
}
private var mArchiveFormParamOr: String? = null private var mArchiveFormParamOr: String? = null
private var mArchiveList: List<ArchiveParser.Archive>? = null private var mArchiveList: List<ArchiveParser.Archive>? = null
private var mCurrentFunds: HomeParser.Funds? = null private var mCurrentFunds: HomeParser.Funds? = null
@@ -416,7 +399,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,12 +709,13 @@ 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 FavouriteStatusRouter.collectAsState(galleryDetail) {
value = galleryDetail.favoriteSlot != NOT_FAVORITED it != NOT_FAVORITED
FavouriteStatusRouter.stateFlow(galleryDetail.gid).collect { value = it != NOT_FAVORITED }
} }
val favButtonText = if (favored) { val favButtonText = if (favored) {
galleryDetail.favoriteName ?: stringResource(id = R.string.local_favorites) galleryDetail.favoriteName ?: stringResource(id = R.string.local_favorites)
@@ -979,7 +963,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 +998,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 +1008,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)
@@ -1113,21 +1096,20 @@ class GalleryDetailScene : BaseScene() {
private fun showTorrentDialog() { private fun showTorrentDialog() {
val galleryDetail = composeBindingGD ?: return val galleryDetail = composeBindingGD ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission( viewLifecycleOwner.lifecycleScope.launchUI {
requireActivity(), val granted = isAtLeastQ || requireContext().requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
Manifest.permission.WRITE_EXTERNAL_STORAGE, if (granted) {
) != PackageManager.PERMISSION_GRANTED val helper = TorrentListDialogHelper()
) { val binding = DialogTorrentListBinding.inflate(layoutInflater)
requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) val dialog: Dialog = BaseDialogBuilder(requireContext())
} else { .setTitle(R.string.torrents)
val helper = TorrentListDialogHelper() .setView(binding.root)
val binding = DialogTorrentListBinding.inflate(layoutInflater) .setOnDismissListener(helper)
val dialog: Dialog = BaseDialogBuilder(requireContext()) .show()
.setTitle(R.string.torrents) helper.setDialog(dialog, binding, galleryDetail.torrentUrl)
.setView(binding.root) } else {
.setOnDismissListener(helper) showTip(getString(R.string.permission_denied), LENGTH_SHORT)
.show() }
helper.setDialog(dialog, binding, galleryDetail.torrentUrl)
} }
} }
@@ -1211,11 +1193,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,13 +54,14 @@ 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
import com.hippo.ehviewer.ui.setMD3Content 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.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
@@ -75,8 +77,6 @@ class GalleryPreviewScreen : Fragment() {
val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior() val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior()
val columnCount = calculateSuitableSpanCount() val columnCount = calculateSuitableSpanCount()
val state = rememberLazyGridState() val state = rememberLazyGridState()
val dialogState = rememberDialogState()
dialogState.Intercept()
val coroutineScope = rememberCoroutineScope { Dispatchers.IO } val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val pages = galleryDetail.pages val pages = galleryDetail.pages
val pgSize = galleryDetail.previewList.size val pgSize = galleryDetail.previewList.size
@@ -158,6 +158,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,22 +58,27 @@ 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
import com.hippo.ehviewer.ui.tools.CrystalCard import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.Deferred import com.hippo.ehviewer.ui.tools.Deferred
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
import com.hippo.ehviewer.ui.tools.rememberDialogState import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.pxToDp 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 = LocalDialogState.current
dialogState.Intercept()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val historyData = remember { Pager(PagingConfig(20, jumpThreshold = 40)) { EhDB.historyLazyList }.flow.cachedIn(lifecycleScope) }.collectAsLazyPagingItems() val historyData = remember { Pager(PagingConfig(20, jumpThreshold = 40)) { EhDB.historyLazyList }.flow.cachedIn(lifecycleScope) }.collectAsLazyPagingItems()
@@ -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)
} }
}, },
@@ -131,47 +133,55 @@ class HistoryScene : BaseScene() {
true true
}, },
) )
SwipeToDismiss( LocalTouchSlopProvider(3f) {
state = dismissState, SwipeToDismiss(
background = {}, state = dismissState,
dismissContent = { background = {},
// TODO: item delete & add animation dismissContent = {
// Bug tracker: https://issuetracker.google.com/issues/150812265 // TODO: item delete & add animation
GalleryInfoListItem( // Bug tracker: https://issuetracker.google.com/issues/150812265
onClick = { GalleryInfoListItem(
navAnimated( onClick = {
R.id.galleryDetailScene, navAnimated(
bundleOf( R.id.galleryDetailScene,
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO, bundleOf(
GalleryDetailScene.KEY_GALLERY_INFO to info, GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
), GalleryDetailScene.KEY_GALLERY_INFO to info,
) ),
}, )
onLongClick = { },
coroutineScope.launchIO { onLongClick = {
dialogState.doGalleryInfoAction(info, context) coroutineScope.launchIO {
} dialogState.doGalleryInfoAction(info, context)
}, }
info = info, },
modifier = Modifier.height(cardHeight), info = info,
) modifier = Modifier.height(cardHeight),
}, )
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 {
@@ -34,8 +34,8 @@ import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.ui.LICENSE_SCREEN import com.hippo.ehviewer.ui.LICENSE_SCREEN
import com.hippo.ehviewer.ui.LocalNavController import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.tools.DialogState import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.observed import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.ui.tools.rememberDialogState
import com.hippo.ehviewer.ui.tools.toAnnotatedString import com.hippo.ehviewer.ui.tools.toAnnotatedString
import com.hippo.ehviewer.updater.AppUpdater import com.hippo.ehviewer.updater.AppUpdater
import com.hippo.ehviewer.updater.Release import com.hippo.ehviewer.updater.Release
@@ -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"
@@ -66,8 +66,7 @@ fun AboutScreen() {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO } val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val dialogState = rememberDialogState() val dialogState = LocalDialogState.current
dialogState.Intercept()
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) } fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
Scaffold( Scaffold(
topBar = { topBar = {
@@ -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.LocalDialogState
import com.hippo.ehviewer.ui.tools.observed import com.hippo.ehviewer.ui.tools.observed
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,7 @@ 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 = LocalDialogState.current
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -133,6 +138,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,
) )
@@ -49,11 +49,11 @@ import com.hippo.ehviewer.ui.MYTAGS_SCREEN
import com.hippo.ehviewer.ui.SIGN_IN_ROUTE_NAME import com.hippo.ehviewer.ui.SIGN_IN_ROUTE_NAME
import com.hippo.ehviewer.ui.UCONFIG_SCREEN import com.hippo.ehviewer.ui.UCONFIG_SCREEN
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.TimePickerDialog 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.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
@@ -70,8 +70,7 @@ fun EhScreen() {
val context = LocalContext.current val context = LocalContext.current
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) } fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
val signin = EhCookieStore.hasSignedIn() val signin = EhCookieStore.hasSignedIn()
val dialogState = rememberDialogState() val dialogState = LocalDialogState.current
dialogState.Intercept()
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -111,7 +110,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 +212,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 +276,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.LocalDialogState
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,7 @@ 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 = LocalDialogState.current
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 +164,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
@@ -24,8 +24,8 @@ import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.ui.LocalNavController import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.isAuthenticationSupported import com.hippo.ehviewer.ui.isAuthenticationSupported
import com.hippo.ehviewer.ui.tools.LocalDialogState
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.rememberedAccessor import com.hippo.ehviewer.ui.tools.rememberedAccessor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -37,8 +37,7 @@ fun PrivacyScreen() {
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() val dialogState = LocalDialogState.current
dialogState.Intercept()
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -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() }
} }
@@ -31,9 +31,11 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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
@@ -47,9 +49,10 @@ 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 kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
interface DialogScope<R> { interface DialogScope<R> {
var expectedValue: R var expectedValue: R
@@ -106,17 +109,24 @@ class DialogState {
title: String? = null, title: String? = null,
hint: String? = null, hint: String? = null,
isNumber: Boolean = false, isNumber: Boolean = false,
invalidator: ((String) -> String?)? = null, invalidator: (suspend (String) -> String?)? = null,
): String { ): String {
return dialog { cont -> return dialog { cont ->
val coroutineScope = rememberCoroutineScope()
var state by remember(cont) { mutableStateOf(initial) } var state by remember(cont) { mutableStateOf(initial) }
var error by remember(cont) { mutableStateOf<String?>(null) } var error by remember(cont) { mutableStateOf<String?>(null) }
AlertDialog( AlertDialog(
onDismissRequest = { cont.cancel() }, onDismissRequest = { cont.cancel() },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
error = invalidator?.invoke(state) if (invalidator == null) {
error ?: cont.resume(state) cont.resume(state)
} else {
coroutineScope.launch {
error = invalidator.invoke(state)
error ?: cont.resume(state)
}
}
}) { }) {
Text(text = stringResource(id = android.R.string.ok)) Text(text = stringResource(id = android.R.string.ok))
} }
@@ -309,7 +319,4 @@ class DialogState {
private val IconWithTextCorner = RoundedCornerShape(8.dp) private val IconWithTextCorner = RoundedCornerShape(8.dp)
@Composable val LocalDialogState = compositionLocalOf<DialogState> { error("CompositionLocal LocalDialogState not present!") }
fun rememberDialogState(): DialogState {
return remember { DialogState() }
}
@@ -0,0 +1,19 @@
package com.hippo.ehviewer.ui.tools
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
// TODO: https://issuetracker.google.com/issues/269627294
@Composable
fun LocalTouchSlopProvider(factor: Float, content: @Composable () -> Unit) {
val current = LocalViewConfiguration.current
CompositionLocalProvider(
value = LocalViewConfiguration provides object : ViewConfiguration by current {
override val touchSlop: Float
get() = current.touchSlop * factor
},
content = content,
)
}
@@ -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)
} }
} }
@@ -15,11 +15,17 @@
*/ */
package com.hippo.ehviewer.util package com.hippo.ehviewer.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -40,5 +46,11 @@ object FavouriteStatusRouter {
val globalFlow = _globalFlow.asSharedFlow() val globalFlow = _globalFlow.asSharedFlow()
fun stateFlow(targetGid: Long) = globalFlow.transform { (gid, slot) -> if (targetGid == gid) emit(slot) } @Composable
inline fun <R> collectAsState(initial: GalleryInfo, crossinline transform: @DisallowComposableCalls (Int) -> R) = remember(initial) {
globalFlow.transform { (gid, slot) -> if (initial.gid == gid) emit(slot) }
.map { transform(it) }
// We cannot use .map(transform) otherwise we will get a strange NPE
// TODO: Report to kotlin compiler issue-tracker
}.collectAsState(transform(initial.favoriteSlot))
} }
@@ -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,57 @@
package com.hippo.ehviewer.util
import android.graphics.Rect
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 = if (isAtLeastQ) {
WindowInsetsCompat.Builder()
} else {
// TODO: Report to google issue tracker
// On API 20-28, WindowInsetsCompat.Builder() will copy the WindowInsets.CONSUMED static field,
// which isConsumed() returns true, resulting in the insets not being dispatched
runCatching {
val constructor = android.view.WindowInsets::class.java.getConstructor(Rect::class.java)
val platformInsets = constructor.newInstance(Rect())
WindowInsetsCompat.Builder(WindowInsetsCompat.toWindowInsetsCompat(platformInsets))
}.getOrElse {
it.printStackTrace()
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(
@@ -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) {
@@ -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)
}
}
}
}
Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff. Show more