Compare commits

..
35 Commits
Author SHA1 Message Date
FooIbar 9746f20b60 Release 1.8.9.4
Releases / Build (push) Has been cancelled
2023-08-30 16:41:26 +08:00
FooIbar 501148179b Merge branch '1.8.x.x' into 1.8.9.x 2023-08-30 16:40:48 +08:00
FooIbar d6a3a351f5 Fix UI not updating after download state changed 2023-08-30 16:37:37 +08:00
FooIbar a574bca48e Kotlin 1.9.10 2023-08-30 16:22:18 +08:00
FooIbar 785faf1bf4 Upgrade ndk to r26 RC 1 2023-08-30 16:20:32 +08:00
FooIbar fbcc32b3ee Merge remote-tracking branch 'upstream/1.8.x.x' into 1.8.x.x
# Conflicts:
#	app/build.gradle.kts
2023-08-25 13:28:07 +08:00
renovate[bot] 46b19a9de5 Update room_version to v2.6.0-beta01 2023-08-24 15:49:05 +00:00
renovate[bot] d2f47d51f9 Update plugin com.google.devtools.ksp to v1.9.10-1.0.13 2023-08-24 10:15:46 +00:00
renovate[bot] db380507f5 Update nav_version to v2.7.1 2023-08-24 07:03:27 +00:00
renovate[bot] 84cf67d21a Update dependency composeOptions to v1.5.2 2023-08-24 03:49:47 +00:00
renovate[bot] 8f53591800 Update dependency androidx.fragment:fragment-ktx to v1.7.0-alpha03 2023-08-24 01:47:24 +00:00
renovate[bot] 65ffd78844 Update dependency androidx.activity:activity-compose to v1.8.0-alpha07 2023-08-23 23:18:25 +00:00
renovate[bot] c00b5e6635 Update serialization to v1.6.0 2023-08-22 23:06:02 +00:00
FooIbar 8160991086 Don't respond to touch events when scrolling 2023-08-22 19:27:44 +08:00
FooIbar f0c933d97a Replace deprecated method 2023-08-22 18:55:39 +08:00
renovate[bot] 3fb8d3e73b Update plugin com.android.application to v8.1.1 2023-08-22 06:12:17 +00:00
renovate[bot] d8efff9921 Update accompanist_version to v0.32.0 2023-08-21 14:07:10 +00:00
FooIbar 32170688b6 Support clearing image cache for specific gallery 2023-08-21 18:39:35 +08:00
FooIbar d08491bf7e Release 1.8.9.3
Releases / Build (push) Has been cancelled
2023-08-17 22:54:38 +08:00
FooIbar 12f6f08d81 Merge branch '1.8.x.x' into 1.8.9.x 2023-08-17 22:53:52 +08:00
FooIbar ca74bfe2e4 Merge branch 'Ehviewer-Overhauled:1.8.x.x' into 1.8.x.x 2023-08-17 22:52:55 +08:00
renovate[bot] 41767f0fd7 Update dependency gradle to v8.3 2023-08-17 14:50:21 +00:00
FooIbar c62ca4dd23 Fix DownloadsScene crash
java.lang.LinkageError: Method com.hippo.ehviewer.databinding.ActivityMainBinding com.hippo.ehviewer.ui.scene.DownloadsScene.getBinding() overrides final method in class Lcom/hippo/ehviewer/ui/scene/SearchBarScene
2023-08-17 20:24:34 +08:00
FooIbar 9c5eddb903 Remove embedded Cronet library 2023-08-17 17:51:44 +08:00
FooIbar 4a0df07805 Set User-Agent in CronetEngine.Builder 2023-08-17 00:03:13 +08:00
FooIbar 37a3edd44b Revert "Add quichint"
This reverts commit 4e427f27
2023-08-16 23:50:20 +08:00
FooIbar f117abd483 Revert "Add option for QUIC support"
This reverts commit 861c05db
2023-08-16 23:27:38 +08:00
FooIbar 4593714298 Cleanup DownloadManager 2023-08-16 23:12:18 +08:00
FooIbar a148fd6ec3 Use flow for DownloadsScene UI updates 2023-08-16 23:12:18 +08:00
FooIbar 23a44e2f3b Migrate DownloadAdapter to ListAdapter 2023-08-16 20:23:57 +08:00
FooIbar 01e03ec0f0 Support search in DownloadsScene 2023-08-15 23:03:52 +08:00
FooIbar e2345cc756 Release 1.8.9.2
Releases / Build (push) Has been cancelled
2023-08-15 17:48:26 +08:00
FooIbar 8d0b8534b2 Merge branch '1.8.x.x' into 1.8.9.x 2023-08-15 17:48:11 +08:00
FooIbar 63f9c8de79 Release 1.8.9.1
Releases / Build (push) Has been cancelled
2023-08-14 23:17:22 +08:00
FooIbar f792f2c968 Merge branch '1.8.x.x' into 1.8.9.x 2023-08-14 23:16:13 +08:00
36 changed files with 314 additions and 423 deletions

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10636728" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- 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
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10636728" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- 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
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10636728" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
+10 -13
View File
@@ -21,7 +21,7 @@ plugins {
android {
compileSdk = 34
buildToolsVersion = "34.0.0"
ndkVersion = "26.0.10404224"
ndkVersion = "26.0.10636728"
splits {
abi {
@@ -64,7 +64,7 @@ android {
minSdk = 28
targetSdk = 34
versionCode = 180043
versionName = "1.8.9.2-gms"
versionName = "1.8.9.4"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -159,7 +159,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
kotlinCompilerExtensionVersion = "1.5.3"
}
namespace = "com.hippo.ehviewer"
@@ -167,7 +167,7 @@ android {
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation("androidx.activity:activity-compose:1.8.0-alpha06")
implementation("androidx.activity:activity-compose:1.8.0-alpha07")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
implementation("androidx.browser:browser:1.6.0")
@@ -185,12 +185,12 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha12")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha02")
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha03")
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
// https://developer.android.com/jetpack/androidx/releases/navigation
val nav_version = "2.7.0"
val nav_version = "2.7.1"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
implementation("androidx.navigation:navigation-compose:$nav_version")
@@ -203,7 +203,7 @@ dependencies {
implementation("androidx.recyclerview:recyclerview-selection:1.2.0-alpha01")
// https://developer.android.com/jetpack/androidx/releases/room
val room_version = "2.6.0-alpha03"
val room_version = "2.6.0-beta01"
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-paging:$room_version")
@@ -213,7 +213,7 @@ dependencies {
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
// https://github.com/google/accompanist/releases
val accompanist_version = "0.31.6-rc"
val accompanist_version = "0.32.0"
implementation("com.google.accompanist:accompanist-themeadapter-material3:$accompanist_version")
implementation("com.google.accompanist:accompanist-webview:$accompanist_version")
implementation("com.google.android.material:material:1.11.0-alpha02")
@@ -250,10 +250,7 @@ dependencies {
implementation("io.ktor:ktor-io-jvm:$ktor")
implementation("io.ktor:ktor-utils-jvm:$ktor")
implementation("org.chromium.net:cronet-api:113.5672.61")
implementation("com.google.android.gms:play-services-cronet:18.0.1")
val serialization = "1.5.1"
val serialization = "1.6.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:$serialization")
@@ -299,6 +296,6 @@ tasks.configureEach {
if ((name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders")) {
dependsOn("cargoBuild")
// fix mergeDebugJniLibFolders UP-TO-DATE
inputs.dir(buildDir.resolve("rustJniLibs/android"))
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
}
}
+1
View File
@@ -17,6 +17,7 @@
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
-keep class com.hippo.ehviewer.ui.scene.SearchBarScene { getBinding(); }
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
@@ -25,7 +25,6 @@ import androidx.lifecycle.coroutineScope
import coil.ImageLoaderFactory
import coil.decode.ImageDecoderDecoder
import coil.util.DebugLogger
import com.google.android.gms.net.CronetProviderInstaller
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.data.GalleryDetail
@@ -78,7 +77,6 @@ class EhApplication : Application(), ImageLoaderFactory {
handler?.uncaughtException(t, e)
}
super.onCreate()
CronetProviderInstaller.installProvider(appCtx)
System.loadLibrary("ehviewer")
System.loadLibrary("ehviewer_rust")
ReadableTime.initialize(this)
@@ -62,7 +62,6 @@ object Settings : DataStorePreferences(null) {
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
var downloadOriginImage by boolPref("download_origin_image", false)
var enableQuic by boolPref("enable_quic", true)
var thumbSizeDp by intPref("thumb_size_", 120)
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
var defaultFavSlot by intPref("default_favorite_slot", -2)
@@ -1,9 +1,12 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.coil
import android.net.Uri
import android.os.Build
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresExtension
import coil.ComponentRegistry
import coil.ImageLoader
import coil.decode.DataSource
@@ -27,7 +30,7 @@ class CronetHttpUriFetcher(private val data: String, private val options: Option
val diskCache = requireNotNull(imageLoader.diskCache)
val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run {
val success = cronetRequest(data) {
disableCache()
setCacheDisabled(true)
}.execute {
diskCache.suspendEdit(diskCacheKey) {
RandomAccessFile(data.toFile(), "rw").use {
@@ -1,5 +1,13 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.cronet
import android.net.http.HttpEngine
import android.net.http.HttpException
import android.net.http.UrlRequest
import android.net.http.UrlResponseInfo
import android.os.Build
import androidx.annotation.RequiresExtension
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.client.CHROME_ACCEPT
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
@@ -12,10 +20,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.Path.Companion.toOkioPath
import org.chromium.net.CronetEngine
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
@@ -28,26 +32,23 @@ import kotlin.coroutines.resumeWithException
private const val TAG = "CronetRequest"
val pool = DirectByteBufferPool(32)
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
enableBrotli(true)
val cronetHttpClient: HttpEngine = HttpEngine.Builder(appCtx).apply {
setEnableBrotli(true)
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
addQuicHint("e-hentai.org", 443, 443)
addQuicHint("forums.e-hentai.org", 443, 443)
addQuicHint("exhentai.org", 443, 443)
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
setUserAgent(CHROME_USER_AGENT)
}.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
lateinit var request: UrlRequest
lateinit var onError: (Throwable) -> Unit
lateinit var readerCont: Continuation<Unit>
val callback = object : UrlRequest.Callback() {
val callback = object : UrlRequest.Callback {
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
logcat(tag = TAG) { "Redirected to $url" }
req.followRedirect()
@@ -66,16 +67,19 @@ class CronetRequest {
readerCont.resume(Unit)
}
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: CronetException) {
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: HttpException) {
onError(e)
}
override fun onCanceled(req: UrlRequest, info: UrlResponseInfo?) {
// No-op
}
}
}
inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply {
request = cronetHttpClient.newUrlRequestBuilder(url, cronetHttpClientExecutor, callback).apply {
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
addHeader("User-Agent", CHROME_USER_AGENT)
addHeader("Accept", CHROME_ACCEPT)
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
referer?.let { addHeader("Referer", it) }
@@ -25,7 +25,7 @@ import java.time.Instant
),
],
)
class DownloadEntity(
data class DownloadEntity(
@PrimaryKey
@ColumnInfo(name = "GID")
var gid: Long,
@@ -44,21 +44,23 @@ class DownloadEntity(
@ColumnInfo(name = "POSITION")
override var position: Int = 0,
@Ignore
override var speed: Long = 0,
@Ignore
override var remaining: Long = 0,
@Ignore
override var finished: Int = 0,
@Ignore
override var downloaded: Int = 0,
@Ignore
override var total: Int = 0,
) : AbstractDownloadInfo {
@Ignore
override var speed: Long = 0
@Ignore
override var remaining: Long = 0
@Ignore
override var finished = 0
@Ignore
override var downloaded = 0
@Ignore
override var total = 0
constructor() : this(0)
}
data class DownloadInfo(
@@ -71,7 +71,7 @@ object DownloadManager : OnSpiderListener {
// Store download info wait to start
private val mWaitList = LinkedList<DownloadInfo>()
private val mSpeedReminder = SpeedReminder()
private val mDownloadInfoListeners = mutableListOf<DownloadInfoListener>()
private var mDownloadInfoListener: DownloadInfoListener? = null
private val mNotifyTaskPool = ConcurrentPool<NotifyTask?>(5)
private var mDownloadListener: DownloadListener? = null
private var mCurrentTask: DownloadInfo? = null
@@ -125,14 +125,6 @@ object DownloadManager : OnSpiderListener {
return info?.state ?: DownloadInfo.STATE_INVALID
}
fun addDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
mDownloadInfoListeners.add(downloadInfoListener)
}
fun removeDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
mDownloadInfoListeners.remove(downloadInfoListener)
}
fun setDownloadListener(listener: DownloadListener?) {
mDownloadListener = listener
}
@@ -166,12 +158,7 @@ object DownloadManager : OnSpiderListener {
mDownloadListener!!.onStart(info)
}
// Notify state update
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -192,12 +179,7 @@ object DownloadManager : OnSpiderListener {
// Update in DB
EhDB.putDownloadInfo(info)
// Notify state update
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
// Make sure download is running
ensureDownload()
}
@@ -227,9 +209,7 @@ object DownloadManager : OnSpiderListener {
EhDB.putDownloadInfo(info)
// Notify
for (l in mDownloadInfoListeners) {
l.onAdd(info, list, list.size - 1)
}
mDownloadInfoListener?.onUpdate(info)
// Make sure download is running
ensureDownload()
@@ -259,9 +239,7 @@ object DownloadManager : OnSpiderListener {
}
if (update) {
// Notify Listener
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
mDownloadInfoListener?.onUpdateAll()
// Ensure download
ensureDownload()
}
@@ -271,33 +249,23 @@ object DownloadManager : OnSpiderListener {
private val _stateFlow = callbackFlow {
val listener = object : DownloadInfoListener {
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
trySend(info)
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
trySend(info)
override fun onUpdate(info: DownloadInfo) {
trySend(info.gid)
}
override fun onUpdateAll() {
// No-op
}
override fun onReload() {
// No-op
}
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
trySend(info)
trySend(0)
}
}
addDownloadInfoListener(listener)
mDownloadInfoListener = listener
awaitClose {
removeDownloadInfoListener(listener)
mDownloadInfoListener = null
}
}.shareIn(callbackFlowScope, SharingStarted.Eagerly)
fun stateFlow(gid: Long) = _stateFlow.filter { it.gid == gid }
fun stateFlow(gid: Long) = _stateFlow.filter { it == gid }
fun stateFlow() = _stateFlow
suspend fun startAllDownload() {
var update = false
@@ -315,9 +283,7 @@ object DownloadManager : OnSpiderListener {
}
if (update) {
// Notify Listener
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
mDownloadInfoListener?.onUpdateAll()
// Ensure download
ensureDownload()
}
@@ -353,9 +319,7 @@ object DownloadManager : OnSpiderListener {
// Notify
if (notify) {
for (l in mDownloadInfoListeners) {
l.onReload()
}
mDownloadInfoListener?.onUpdateAll()
}
}
@@ -386,9 +350,7 @@ object DownloadManager : OnSpiderListener {
// Notify
withUIContext {
for (l in mDownloadInfoListeners) {
l.onAdd(info, list, list.size - 1)
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -396,12 +358,7 @@ object DownloadManager : OnSpiderListener {
val info = stopDownloadInternal(gid)
if (info != null) {
// Update listener
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
// Ensure download
ensureDownload()
}
@@ -411,12 +368,7 @@ object DownloadManager : OnSpiderListener {
val info = stopCurrentDownloadInternal()
if (info != null) {
// Update listener
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
// Ensure download
ensureDownload()
}
@@ -426,9 +378,7 @@ object DownloadManager : OnSpiderListener {
stopRangeDownloadInternal(gidList)
// Update listener
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
mDownloadInfoListener?.onUpdateAll()
// Ensure download
ensureDownload()
@@ -447,9 +397,7 @@ object DownloadManager : OnSpiderListener {
stopCurrentDownloadInternal()
// Notify mDownloadInfoListener
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
mDownloadInfoListener?.onUpdateAll()
}
suspend fun deleteDownload(gid: Long, deleteFiles: Boolean = false) {
@@ -470,9 +418,7 @@ object DownloadManager : OnSpiderListener {
if (index >= 0) {
list.remove(info)
// Update listener
for (l in mDownloadInfoListeners) {
l.onRemove(info, list, index)
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -495,15 +441,13 @@ object DownloadManager : OnSpiderListener {
allInfoList.removeAll(list.toSet())
// Update listener
for (l in mDownloadInfoListeners) {
l.onReload()
}
mDownloadInfoListener?.onUpdateAll()
// Ensure download
ensureDownload()
}
fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
private fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
val info = allInfoList.removeAt(fromPosition)
allInfoList.add(toPosition, info)
val range = if (fromPosition < toPosition) fromPosition..toPosition else toPosition..fromPosition
@@ -515,12 +459,11 @@ object DownloadManager : OnSpiderListener {
return list
}
fun moveDownload(label: String?, fromPosition: Int, toPosition: Int) =
getInfoListForLabel(label)!!.let {
val absFromPosition = allInfoList.indexOf(it[fromPosition])
val absToPosition = allInfoList.indexOf(it[toPosition])
moveDownload(absFromPosition, absToPosition)
}
fun moveDownload(fromItem: DownloadInfo, toItem: DownloadInfo): List<DownloadInfo> {
val fromPosition = allInfoList.indexOf(fromItem)
val toPosition = allInfoList.indexOf(toItem)
return moveDownload(fromPosition, toPosition)
}
suspend fun resetAllReadingProgress() = coroutineScope {
allInfoList.forEach { info ->
@@ -658,9 +601,7 @@ object DownloadManager : OnSpiderListener {
// Save to DB
EhDB.putDownloadInfo(info)
}
for (l in mDownloadInfoListeners) {
l.onReload()
}
mDownloadInfoListener?.onUpdateAll()
}
suspend fun addLabel(label: String?) {
@@ -810,30 +751,15 @@ object DownloadManager : OnSpiderListener {
}
interface DownloadInfoListener {
/**
* Add the special info to the special position
*/
fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int)
/**
* The special info is changed
*/
fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>)
fun onUpdate(info: DownloadInfo)
/**
* Maybe all data is changed, but size is the same
*/
fun onUpdateAll()
/**
* Maybe all data is changed, maybe list is changed
*/
fun onReload()
/**
* Remove the special info from the special position
*/
fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int)
}
interface DownloadListener {
@@ -940,12 +866,7 @@ object DownloadManager : OnSpiderListener {
Log.e(TAG, "Current task is null, but it should not be")
} else {
info.total = mPages
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -974,12 +895,7 @@ object DownloadManager : OnSpiderListener {
if (mDownloadListener != null) {
mDownloadListener!!.onGetPage(info)
}
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -992,12 +908,7 @@ object DownloadManager : OnSpiderListener {
info.finished = mFinished
info.downloaded = mDownloaded
info.total = mTotal
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -1036,12 +947,7 @@ object DownloadManager : OnSpiderListener {
if (mDownloadListener != null) {
mDownloadListener!!.onFinish(info)
}
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
// Start next download
ensureDownload()
}
@@ -1127,12 +1033,7 @@ object DownloadManager : OnSpiderListener {
if (mDownloadListener != null) {
mDownloadListener!!.onDownload(info)
}
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
mBytesRead = 0
if (!mStop) {
@@ -135,9 +135,9 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
): Boolean {
return if (isCronetSupported) {
cronetRequest(url, referer) {
disableCache()
setCacheDisabled(true)
}.execute { info ->
val headers = info.allHeaders
val headers = info.headers.asMap
val type = headers["Content-Type"]?.first()?.toMediaType()?.subtype ?: "jpg"
val length = headers["Content-Length"]!!.first().toLong()
saveResponseMeta(index, type, length) { file ->
@@ -26,7 +26,10 @@ 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
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.android.material.sidesheet.SideSheetDialog
import com.hippo.ehviewer.ui.MainActivity
@@ -155,8 +158,10 @@ abstract class BaseScene : Fragment() {
createDrawerView(savedInstanceState)?.let {
sideSheetDialog = SideSheetDialog(requireContext()).apply {
window?.decorView?.apply {
setViewTreeLifecycleOwner(viewLifecycleOwner)
setViewTreeSavedStateRegistryOwner(this@BaseScene)
val owner = viewLifecycleOwner
setViewTreeLifecycleOwner(owner)
setViewTreeViewModelStoreOwner(owner as ViewModelStoreOwner)
setViewTreeSavedStateRegistryOwner(owner as SavedStateRegistryOwner)
}
setContentView(it)
}
@@ -55,9 +55,11 @@ 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
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import arrow.core.partially1
@@ -79,7 +81,6 @@ 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.DownloadInfoListener
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.DownloadService.Companion.clear
import com.hippo.ehviewer.download.downloadDir
@@ -102,6 +103,7 @@ import com.hippo.ehviewer.ui.tools.CropDefaults
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.LongList
import com.hippo.ehviewer.util.containsIgnoreCase
import com.hippo.ehviewer.util.sendTo
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.launchIO
@@ -110,20 +112,21 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.pxToDp
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 :
BaseToolbarScene(),
DownloadInfoListener,
SearchBarScene(),
OnClickFabListener {
/*---------------
Whole life cycle
---------------*/
private var mKeyword: String? = null
private var mLabel: String? = null
private var mList: List<DownloadInfo>? = null
private val mList get() = mAdapter?.currentList
/*---------------
View life cycle
@@ -134,20 +137,23 @@ class DownloadsScene :
private var mAdapter: DownloadAdapter? = null
private var mItemTouchHelper: ItemTouchHelper? = null
private val tracker get() = mAdapter!!.tracker!!
private var mInitPosition = -1
private var mGid = -1L
private var mLabelAdapter: DownloadLabelAdapter? = null
private lateinit var mLabels: MutableList<String>
private var mType = -1
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
private fun initLabels() {
context ?: return
val listLabel = downloadManager.labelList
mLabels = ArrayList(listLabel.size + LABEL_OFFSET)
// Add "All" and "Default" label names
mLabels.add(getString(R.string.download_all))
mLabels.add(getString(R.string.default_download_label_name))
mLabels = arrayListOf(
getString(R.string.download_all),
getString(R.string.default_download_label_name),
)
listLabel.forEach {
mLabels.add(it.label)
}
@@ -165,18 +171,7 @@ class DownloadsScene :
val info = DownloadManager.getDownloadInfo(gid)
if (null != info) {
mLabel = info.label
updateForLabel()
updateView()
// Get position
if (null != mList) {
val position = mList!!.indexOf(info)
if (position >= 0 && null != _binding) {
binding.recyclerView.scrollToPosition(position)
} else {
mInitPosition = position
}
}
mGid = gid
return true
}
}
@@ -185,7 +180,6 @@ class DownloadsScene :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DownloadManager.addDownloadInfoListener(this)
if (savedInstanceState == null) {
onInit()
} else {
@@ -193,50 +187,54 @@ class DownloadsScene :
}
}
override fun onDestroy() {
super.onDestroy()
mList = null
DownloadManager.removeDownloadInfoListener(this)
private fun filter(info: DownloadInfo) = info.run {
(mType == -1 || state == mType) &&
mKeyword?.let {
title.containsIgnoreCase(it) || titleJpn.containsIgnoreCase(it) ||
uploader.containsIgnoreCase(it)
} ?: true
}
private fun updateInfoList() {
mAdapter?.run {
val list = when (mLabel) {
null -> DownloadManager.allInfoList
getString(R.string.default_download_label_name) -> DownloadManager.defaultInfoList
else -> DownloadManager.getLabelDownloadInfoList(mLabel)
?: DownloadManager.allInfoList.also { mLabel = null }
}.mapNotNull { it.takeIf(::filter)?.copy(downloadInfo = it.downloadInfo.copy()) }
submitList(list) {
if (mGid != -1L) {
val position = list.indexOfFirst { it.gid == mGid }
if (position != -1) {
binding.recyclerView.scrollToPosition(position)
}
mGid = -1L
}
updateView()
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun updateForLabel() {
val list = when (mLabel) {
null -> DownloadManager.allInfoList
getString(R.string.default_download_label_name) -> DownloadManager.defaultInfoList
else -> DownloadManager.getLabelDownloadInfoList(mLabel)
?: DownloadManager.allInfoList.also { mLabel = null }
}
mList = if (mType != -1) {
list.filter { it.state == mType }
} else {
list
}
if (mAdapter != null) {
mAdapter!!.notifyDataSetChanged()
}
updateInfoList()
Settings.recentDownloadLabel = mLabel
}
private fun updateTitle() {
setTitle(
getString(
R.string.scene_download_title,
if (mLabel != null) mLabel else getString(R.string.download_all),
),
)
val title = getString(R.string.scene_download_title, mLabel ?: getString(R.string.download_all))
setSearchBarHint(title)
setEditTextHint(getString(R.string.search_bar_hint, title))
}
private fun onInit() {
if (!handleArguments(arguments)) {
mLabel = Settings.recentDownloadLabel
updateForLabel()
}
}
private fun onRestore(savedInstanceState: Bundle) {
mLabel = savedInstanceState.getString(KEY_LABEL)
updateForLabel()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -300,10 +298,6 @@ class DownloadsScene :
}
}
recyclerView.addItemDecoration(decoration)
if (mInitPosition >= 0) {
recyclerView.scrollToPosition(mInitPosition)
mInitPosition = -1
}
mItemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
@@ -325,12 +319,10 @@ class DownloadsScene :
if (fromPosition == toPosition) {
return false
}
val list = when (mLabel) {
null -> DownloadManager.moveDownload(fromPosition, toPosition)
getString(R.string.default_download_label_name) -> DownloadManager.moveDownload(null, fromPosition, toPosition)
else -> DownloadManager.moveDownload(mLabel, fromPosition, toPosition)
}
mAdapter!!.notifyItemMoved(fromPosition, toPosition)
val fromItem = mList!![fromPosition]
val toItem = mList!![toPosition]
val list = DownloadManager.moveDownload(fromItem, toItem)
updateInfoList()
lifecycleScope.launchIO {
EhDB.updateDownloadInfo(list)
}
@@ -343,7 +335,6 @@ class DownloadsScene :
dialogState.confirmRemoveDownload(info) {
mAdapter!!.notifyItemChanged(position)
}
mAdapter!!.notifyItemRemoved(position)
}
}
})
@@ -358,15 +349,29 @@ class DownloadsScene :
fabLayout.setAutoCancel(false)
fabLayout.setOnClickFabListener(this@DownloadsScene)
addAboveSnackView(fabLayout)
updateView()
updateForLabel()
}
viewLifecycleOwner.lifecycleScope.launch {
DownloadManager.stateFlow().collectLatest {
updateInfoList()
}
}
setOnApplySearch {
mKeyword = it.takeUnless { it.isEmpty() }
updateInfoList()
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateTitle()
setNavigationIcon(R.drawable.ic_baseline_menu_24)
override fun onSearchViewExpanded() {
super.onSearchViewExpanded()
if (tracker.isInCustomChoice) tracker.clearSelection()
showSearchFab(true)
}
override fun onSearchViewHidden() {
super.onSearchViewHidden()
hideSearchFab(true)
}
override fun onDestroyView() {
@@ -402,8 +407,7 @@ class DownloadsScene :
mType + 1,
) { dialog: DialogInterface, which: Int ->
mType = which - 1
updateForLabel()
updateView()
updateInfoList()
dialog.dismiss()
}
.show()
@@ -456,7 +460,7 @@ class DownloadsScene :
}
}
fun updateView() {
private fun updateView() {
if (mViewTransition != null) {
if (mList.isNullOrEmpty()) {
mViewTransition!!.showView(1)
@@ -604,61 +608,6 @@ class DownloadsScene :
}
}
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
if (mList !== list) {
return
}
lifecycleScope.launchUI {
if (mAdapter != null) {
mAdapter!!.notifyItemInserted(position)
}
updateView()
}
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
if (mLabel != null && mList !== list) {
return
}
val index = mList!!.indexOf(info)
lifecycleScope.launchUI {
if (index >= 0 && mAdapter != null) {
mAdapter!!.notifyItemChanged(index, PAYLOAD_STATE)
}
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onUpdateAll() {
lifecycleScope.launchUI {
if (mAdapter != null) {
mAdapter!!.notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onReload() {
lifecycleScope.launchUI {
if (mAdapter != null) {
mAdapter!!.notifyDataSetChanged()
}
updateView()
}
}
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
if (mList !== list) {
return
}
lifecycleScope.launchUI {
if (mAdapter != null) {
mAdapter!!.notifyItemRemoved(position)
}
updateView()
}
}
private class DownloadLabelHolder(val binding: ItemDrawerListBinding) :
RecyclerView.ViewHolder(binding.root)
@@ -669,17 +618,14 @@ class DownloadsScene :
DownloadLabelHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
holder.itemView.setOnClickListener {
val position = holder.bindingAdapterPosition
val label1: String? = if (position == 0) {
null
} else {
mLabels[position]
}
if (mLabel != label1) {
mLabel = label1
val label = mLabels[position].takeUnless { position == 0 }
if (mLabel != label) {
mKeyword = null
setSearchBarText(null)
mLabel = label
updateForLabel()
updateView()
closeSideSheet()
}
closeSideSheet()
}
holder.binding.edit.setOnClickListener {
val context = context
@@ -717,7 +663,7 @@ class DownloadsScene :
}
holder.binding.run {
if (list != null) {
text.text = label + " [" + list.size + "]"
text.text = "$label [${list.size}]"
} else {
text.text = label
}
@@ -961,17 +907,23 @@ class DownloadsScene :
}
}
private inner class DownloadAdapter : RecyclerView.Adapter<DownloadHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<DownloadInfo>() {
override fun areItemsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
override fun areContentsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem == newItem
override fun getChangePayload(oldItem: DownloadInfo, newItem: DownloadInfo): Any? {
return if (oldItem.downloadInfo != newItem.downloadInfo) {
PAYLOAD_DOWNLOAD_INFO
} else {
super.getChangePayload(oldItem, newItem)
}
}
}
private inner class DownloadAdapter : ListAdapter<DownloadInfo, DownloadHolder>(diffCallback) {
private val mInflater: LayoutInflater = layoutInflater
var tracker: GallerySelectionTracker<DownloadInfo>? = null
override fun getItemId(position: Int): Long {
return if (mList == null || position < 0 || position >= mList!!.size) {
0
} else {
mList!![position].gid
}
}
override fun getItemId(position: Int) = getItem(position).gid
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
val holder = DownloadHolder(ItemDownloadBinding.inflate(mInflater, parent, false))
@@ -980,10 +932,8 @@ class DownloadsScene :
}
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
mList?.let {
val info = it[position]
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
}
val info = getItem(position)
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
}
override fun onBindViewHolder(
@@ -993,18 +943,15 @@ class DownloadsScene :
) {
payloads.forEach { payload ->
when (payload) {
PAYLOAD_STATE -> {
mList?.let { holder.bindForState(it[position]) }
PAYLOAD_DOWNLOAD_INFO -> {
val info = getItem(position)
holder.bindForState(info)
return
}
}
}
super.onBindViewHolder(holder, position, payloads)
}
override fun getItemCount(): Int {
return if (mList == null) 0 else mList!!.size
}
}
private inner class RenameLabelDialogHelper(
@@ -1039,7 +986,6 @@ class DownloadsScene :
if (mLabel == mOriginalLabel) {
mLabel = text
updateForLabel()
updateView()
}
}
}
@@ -1128,7 +1074,6 @@ class DownloadsScene :
withUIContext {
mLabel = null
updateForLabel()
updateView()
mLabelAdapter!!.notifyItemRemoved(position)
}
}
@@ -1146,7 +1091,7 @@ class DownloadsScene :
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
private const val KEY_LABEL = "label"
private const val LABEL_OFFSET = 2
private const val PAYLOAD_STATE = 0
private const val PAYLOAD_DOWNLOAD_INFO = 0
}
}
@@ -87,12 +87,11 @@ import com.hippo.ehviewer.util.getValue
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.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import rikka.core.res.resolveColor
import java.time.Instant
@@ -203,7 +202,7 @@ class FavoritesScene : SearchBarScene() {
} else {
setSearchBarHint(getString(R.string.favorites_title_2, favCatName, keyword))
}
setEditTextHint(getString(R.string.favorites_search_bar_hint, favCatName))
setEditTextHint(getString(R.string.search_bar_hint, favCatName))
Settings.recentFavCat = urlBuilder.favCat
}
@@ -337,9 +336,8 @@ class FavoritesScene : SearchBarScene() {
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
adapter.addLoadStateListener {
viewLifecycleOwner.lifecycleScope.launchUI {
ensureActive()
viewLifecycleOwner.lifecycleScope.launch {
adapter.loadStateFlow.collectLatest {
when (val state = it.refresh) {
is LoadState.Loading -> {
showSearchBar()
@@ -422,7 +420,6 @@ class FavoritesScene : SearchBarScene() {
}
})
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
allowEmptySearch = false
return binding.root
}
@@ -105,6 +105,7 @@ import arrow.core.partially1
import coil.imageLoader
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -122,6 +123,7 @@ import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.NoHAtHClientException
import com.hippo.ehviewer.client.getImageKey
import com.hippo.ehviewer.client.parser.ArchiveParser
import com.hippo.ehviewer.client.parser.HomeParser
import com.hippo.ehviewer.client.parser.TorrentResult
@@ -341,6 +343,24 @@ class GalleryDetailScene : BaseScene() {
}
}
private fun actionClearCache() {
val gd = composeBindingGD ?: return
lifecycleScope.launchIO {
dialogState.awaitPermissionOrCancel(
confirmText = R.string.clear_all,
dismissText = android.R.string.cancel,
title = R.string.clear_image_cache,
) {
Text(text = stringResource(id = R.string.clear_image_cache_confirm))
}
(0..<gd.pages).forEach {
val key = getImageKey(gd.gid, it)
imageCache.remove(key)
}
showTip(R.string.image_cache_cleared, LENGTH_SHORT)
}
}
private fun actionOpenInOtherApp() {
val url = galleryDetailUrl ?: return
val activity: Activity = mainActivity ?: return
@@ -466,6 +486,13 @@ class GalleryDetailScene : BaseScene() {
actionRefresh()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.clear_image_cache)) },
onClick = {
dropdown = false
actionClearCache()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.open_in_other_app)) },
onClick = {
@@ -104,11 +104,9 @@ import com.hippo.ehviewer.util.getValue
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.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@@ -365,9 +363,8 @@ class GalleryListScene : SearchBarScene() {
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
adapter.addLoadStateListener {
viewLifecycleOwner.lifecycleScope.launchUI {
ensureActive()
launch {
adapter.loadStateFlow.collectLatest {
when (val state = it.refresh) {
is LoadState.Loading -> {
showSearchBar()
@@ -164,52 +164,68 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
privLockModeStart = null
}
fun selectSearchFab(animation: Boolean) {
_binding ?: return
mShowActionFab = false
fun showSearchFab(animation: Boolean, delay: Long = 0) {
if (animation) {
val fab: View? = fabLayout.primaryFab
val delay: Long
if (View.INVISIBLE == fab!!.visibility) {
delay = 0L
} else {
delay = ANIMATE_TIME
fabLayout.setExpanded(expanded = false, animation = true)
fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
}
mSearchFab.visibility = View.VISIBLE
mSearchFab.rotation = -45.0f
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
fabLayout.setExpanded(expanded = false, animation = false)
val fab = fabLayout.primaryFab!!
fab.visibility = View.INVISIBLE
fab.scaleX = 0.0f
fab.scaleY = 0.0f
mSearchFab.visibility = View.VISIBLE
mSearchFab.scaleX = 1.0f
mSearchFab.scaleY = 1.0f
}
}
fun selectActionFab(animation: Boolean) {
_binding ?: return
mShowActionFab = true
if (animation) {
val delay: Long
if (View.INVISIBLE == mSearchFab.visibility) {
delay = 0L
} else {
delay = ANIMATE_TIME
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
}
}
fun selectSearchFab(animation: Boolean) {
_binding ?: return
mShowActionFab = false
val delay = if (animation) {
val fab = fabLayout.primaryFab!!
if (View.INVISIBLE == fab.visibility) {
0L
} else {
fabLayout.setExpanded(expanded = false, animation = true)
fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
ANIMATE_TIME
}
} else {
fabLayout.setExpanded(expanded = false, animation = false)
val fab = fabLayout.primaryFab!!
fab.visibility = View.INVISIBLE
fab.scaleX = 0.0f
fab.scaleY = 0.0f
0L
}
showSearchFab(animation, delay)
}
fun selectActionFab(animation: Boolean) {
_binding ?: return
mShowActionFab = true
val delay = hideSearchFab(animation)
if (animation) {
val fab = fabLayout.primaryFab!!
fab.visibility = View.VISIBLE
fab.rotation = -45.0f
@@ -222,9 +238,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
fab.visibility = View.VISIBLE
fab.scaleX = 1.0f
fab.scaleY = 1.0f
mSearchFab.visibility = View.INVISIBLE
mSearchFab.scaleX = 0.0f
mSearchFab.scaleY = 0.0f
}
}
@@ -126,11 +126,6 @@ fun AdvancedScreen() {
entryValueRes = R.array.app_language_entry_values,
value = Settings::language,
)
SwitchPreference(
title = stringResource(id = R.string.settings_advanced_enable_quic),
summary = stringResource(id = R.string.settings_advanced_enable_quic_summary),
value = Settings::enableQuic,
)
SwitchPreference(
title = stringResource(id = R.string.preload_thumb_aggressively),
value = Settings::preloadThumbAggressively,
@@ -1,7 +1,8 @@
package com.hippo.ehviewer.util
import com.google.android.gms.net.CronetProviderInstaller
import com.hippo.ehviewer.Settings
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
@get:ChecksSdkIntAtLeast(Build.VERSION_CODES.S, extension = 7)
val isCronetSupported: Boolean
get() = Settings.enableQuic && isAtLeastQ && CronetProviderInstaller.isInstalled()
get() = isAtLeastSExtension7
@@ -1,5 +1,8 @@
package com.hippo.ehviewer.util
import android.os.Build
import android.os.ext.SdkExtensions
val isAtLeastQ = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
val isAtLeastR = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
val isAtLeastSExtension7 = isAtLeastR && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 7
@@ -21,3 +21,5 @@ fun String.toLongOrDefault(defaultValue: Long): Long {
fun String.toFloatOrDefault(defaultValue: Float): Float {
return toFloatOrNull() ?: defaultValue
}
fun String?.containsIgnoreCase(other: String) = this?.contains(other, true) ?: false
@@ -57,7 +57,9 @@ class WebtoonRecyclerView @JvmOverloads constructor(
}
override fun onTouchEvent(e: MotionEvent): Boolean {
detector.onTouchEvent(e)
if (scrollState == SCROLL_STATE_IDLE) {
detector.onTouchEvent(e)
}
return super.onTouchEvent(e)
}
+7 -10
View File
@@ -17,29 +17,26 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_filter"
android:icon="@drawable/v_filter_dark_x24"
android:title="@string/download_filter"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_open_side_sheet"
android:icon="@drawable/ic_outline_label_24"
android:title="@string/download_labels"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_filter"
android:title="@string/download_filter"
app:showAsAction="never" />
<item
android:id="@+id/action_start_all"
android:icon="@drawable/v_play_x24"
android:title="@string/download_start_all"
app:showAsAction="ifRoom" />
app:showAsAction="never" />
<item
android:id="@+id/action_stop_all"
android:icon="@drawable/v_pause_x24"
android:title="@string/download_stop_all"
app:showAsAction="ifRoom" />
app:showAsAction="never" />
<item
android:id="@+id/action_reset_reading_progress"
+1 -1
View File
@@ -294,7 +294,7 @@
<string name="settings_advanced_dump_logcat_to">Protokolldatei wurde auf %s gespeichert.</string>
<string name="cloud_favorites">Cloud-Favoriten</string>
<string name="local_favorites">Lokale Favoriten</string>
<string name="favorites_search_bar_hint">%s durchsuchen</string>
<string name="search_bar_hint">%s durchsuchen</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Aus Favoriten löschen</string>
+1 -1
View File
@@ -354,7 +354,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Favoritos en la nube</string>
<string name="local_favorites">Favoritos locales</string>
<string name="favorites_search_bar_hint">Buscar %s</string>
<string name="search_bar_hint">Buscar %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Eliminar de favoritos</string>
+1 -1
View File
@@ -393,7 +393,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Favoris de cloud</string>
<string name="local_favorites">Favoris locales</string>
<string name="favorites_search_bar_hint">Chercher dans %s</string>
<string name="search_bar_hint">Chercher dans %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Effacer de favoris</string>
+1 -1
View File
@@ -390,7 +390,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Cloudのお気に入り</string>
<string name="local_favorites">ローカルのお気に入り</string>
<string name="favorites_search_bar_hint">%sを検索</string>
<string name="search_bar_hint">%sを検索</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">お気に入りから削除</string>
+1 -1
View File
@@ -364,7 +364,7 @@
<!-- Favorites -->
<string name="cloud_favorites">클라우드 즐겨찾기</string>
<string name="local_favorites">로컬 즐겨찾기</string>
<string name="favorites_search_bar_hint">%s 검색</string>
<string name="search_bar_hint">%s 검색</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">즐겨찾기에서 삭제</string>
+1 -1
View File
@@ -379,7 +379,7 @@
<!-- Favorites -->
<string name="cloud_favorites">รายการโปรดบนเว็บ</string>
<string name="local_favorites">รายการโปรดบนเครื่อง</string>
<string name="favorites_search_bar_hint">ค้นหา %s</string>
<string name="search_bar_hint">ค้นหา %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">ลบออกจากรายการโปรด</string>
+4 -3
View File
@@ -149,6 +149,9 @@
<string name="refresh">刷新</string>
<string name="refresh_original">刷新(原图)</string>
<string name="open_in_other_app">在其他应用中打开</string>
<string name="clear_image_cache">清除图片缓存</string>
<string name="clear_image_cache_confirm">清除该画廊的所有图片缓存?</string>
<string name="image_cache_cleared">已清除图片缓存</string>
<string name="rate_successfully">评分成功</string>
<string name="rate_failed">评分失败</string>
<string name="no_torrents">没有种子</string>
@@ -376,8 +379,6 @@
<string name="settings_advanced_dump_logcat_to">已保存日志至 %s</string>
<string name="settings_advanced_read_cache_size">阅读缓存大小</string>
<string name="settings_advanced_app_language_title">App 界面语言</string>
<string name="settings_advanced_enable_quic">[实验性] QUIC 支持</string>
<string name="settings_advanced_enable_quic_summary">启用 QUIC 协议支持(需要重启应用)</string>
<string name="settings_advanced_export_data">导出数据</string>
<string name="settings_advanced_export_data_summary">保存数据至外置存储器,例如下载列表,快速搜索列表</string>
<string name="settings_advanced_export_data_to">已导出数据至 %s</string>
@@ -409,7 +410,7 @@
<!-- Favorites -->
<string name="cloud_favorites">云端收藏</string>
<string name="local_favorites">本地收藏</string>
<string name="favorites_search_bar_hint">搜索 %s</string>
<string name="search_bar_hint">搜索 %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">删除收藏</string>
+1 -1
View File
@@ -419,7 +419,7 @@
<!-- Favorites -->
<string name="cloud_favorites">雲端收藏</string>
<string name="local_favorites">本地收藏</string>
<string name="favorites_search_bar_hint">搜索 %s</string>
<string name="search_bar_hint">搜索 %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">刪除收藏</string>
+1 -1
View File
@@ -400,7 +400,7 @@
<!-- Favorites -->
<string name="cloud_favorites">雲端收藏</string>
<string name="local_favorites">本機收藏</string>
<string name="favorites_search_bar_hint">搜尋 %s</string>
<string name="search_bar_hint">搜尋 %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">移除收藏</string>
+4 -3
View File
@@ -191,6 +191,9 @@
<string name="refresh">Refresh</string>
<string name="refresh_original">Refresh (Original image)</string>
<string name="open_in_other_app">Open with other app</string>
<string name="clear_image_cache">Clear image cache</string>
<string name="clear_image_cache_confirm">Clear all cached images for this gallery?</string>
<string name="image_cache_cleared">Image cache cleared</string>
<string name="rate_successfully">Rate successfully</string>
<string name="rate_failed">Rate failed</string>
<string name="no_torrents">No torrents</string>
@@ -420,8 +423,6 @@
<string name="settings_advanced_dump_logcat_to">Logcat dumped to %s</string>
<string name="settings_advanced_read_cache_size">Read cache size</string>
<string name="settings_advanced_app_language_title">App language</string>
<string name="settings_advanced_enable_quic">[Experimental] QUIC support</string>
<string name="settings_advanced_enable_quic_summary">Enable QUIC protocol support (App restart required)</string>
<string name="settings_advanced_export_data">Export data</string>
<string name="settings_advanced_export_data_summary">Save data like download list, quick search list, to external storage</string>
<string name="settings_advanced_export_data_to">Exported data to %s</string>
@@ -467,7 +468,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Cloud Favorites</string>
<string name="local_favorites">Local Favorites</string>
<string name="favorites_search_bar_hint">Search %s</string>
<string name="search_bar_hint">Search %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Delete from favorites</string>
+5 -5
View File
@@ -1,8 +1,8 @@
plugins {
id("com.android.application") version "8.1.0" apply false
kotlin("android") version "1.9.0" apply false
kotlin("plugin.serialization") version "1.9.0" apply false
id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
id("com.android.application") version "8.1.1" apply false
kotlin("android") version "1.9.10" apply false
kotlin("plugin.serialization") version "1.9.10" apply false
id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false
id("com.mikepenz.aboutlibraries.plugin") version "10.8.3" apply false
id("org.mozilla.rust-android-gradle.rust-android") version "0.9.3" apply false
id("dev.shreyaspatil.compose-compiler-report-generator") version "1.1.0" apply false
@@ -13,7 +13,7 @@ allprojects {
}
tasks.register("Delete", Delete::class) {
delete(rootProject.buildDir)
delete(rootProject.layout.buildDirectory)
}
buildscript {
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME