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