Compare commits

..
29 Commits
Author SHA1 Message Date
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 4594ac592c Merge branch 'Ehviewer-Overhauled:1.8.x.x' into 1.8.x.x 2023-08-15 17:44:38 +08:00
FooIbar eee0f2dded Fix FavoritesScene crash 2023-08-15 17:24:21 +08:00
FooIbar b4080302c3 Refine GalleryListParser 2023-08-15 16:22:55 +08:00
renovate[bot] 337e210b5b Update dependency com.google.android.material:material to v1.11.0-alpha02 2023-08-15 05:08:26 +00: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
FooIbar a05b9174d4 Tweak EhScreen layout 2023-08-14 23:12:39 +08:00
FooIbar aa8adfb6f7 Use LazyColumn in showSelectItem() 2023-08-14 23:10:32 +08:00
FooIbar 1034fcd7b0 Fix favorite button color 2023-08-14 21:11:55 +08:00
FooIbar 69a26aeedb Implement swipe-to-remove in Downloads and Favorites 2023-08-14 21:11:55 +08:00
FooIbar dd607b41e9 Show search fab in FavoritesScene 2023-08-13 19:13:08 +08:00
FooIbar fd45319715 Fix toplist 2023-08-13 00:14:44 +08:00
FooIbar c34ba56896 Start a new fragment if search was performed from Popular or Toplist 2023-08-12 22:47:20 +08:00
FooIbar 6e00a4133c Fix nav checked item 2023-08-12 17:51:52 +08:00
38 changed files with 521 additions and 536 deletions

No files matched your search

+2 -5
View File
@@ -64,7 +64,7 @@ android {
minSdk = 28
targetSdk = 34
versionCode = 180043
versionName = "1.8.9.0-gms"
versionName = "1.8.9.3"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -216,7 +216,7 @@ dependencies {
val accompanist_version = "0.31.6-rc"
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-alpha01")
implementation("com.google.android.material:material:1.11.0-alpha02")
val splitties = "3.0.0"
implementation("com.louiscad.splitties:splitties-appctx:$splitties")
@@ -250,9 +250,6 @@ 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"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
+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
@@ -11,7 +11,6 @@ import androidx.navigation.NavOptions
@SuppressLint("PrivateResource")
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
@@ -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)
@@ -70,8 +70,8 @@ data class ListUrlBuilder(
mPrev = index?.takeUnless { isNext }
}
fun setJumpTo(jumpTo: String?) {
mJumpTo = jumpTo
fun setJumpTo(jumpTo: Int) {
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
}
var keyword: String?
@@ -1,9 +1,12 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.ParseException
object GalleryListParser {
suspend fun parse(body: String) = runCatching {
@@ -15,13 +18,21 @@ object GalleryListParser {
it.generateSLang()
}
}
}.getOrDefault(GalleryListResult())
}.getOrElse {
if (body.contains("<p>You do not have any watched tags")) {
throw EhException(R.string.gallery_list_empty_hit_subscription)
}
if (body.contains("No hits found</p>")) {
throw EhException(R.string.gallery_list_empty_hit)
}
throw ParseException("Can't parse gallery list", body, it)
}
}
class GalleryListResult(
val prev: String? = null,
val next: String? = null,
val galleryInfoList: ArrayList<BaseGalleryInfo> = arrayListOf(),
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
)
private external fun parseGalleryInfoList(e: String): GalleryListResult
@@ -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,
@@ -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 ->
@@ -20,6 +20,8 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
@@ -28,7 +30,12 @@ import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.HeartBroken
import androidx.compose.material.icons.filled.MenuBook
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.EhDB
@@ -55,6 +62,7 @@ import com.hippo.ehviewer.util.requestPermission
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.sync.Mutex
@@ -312,7 +320,6 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
1 -> withUIContext {
if (downloaded) {
confirmRemoveDownload(info)
DownloadManager.deleteDownload(info.gid)
} else {
CommonOperations.startDownload(this@with, info, false)
}
@@ -342,12 +349,28 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
private const val MAX_FAVNOTE_CHAR = 200
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo) = awaitPermissionOrCancel(
confirmText = android.R.string.ok,
dismissText = android.R.string.cancel,
title = R.string.download_remove_dialog_title,
text = { Text(text = stringResource(id = R.string.download_remove_dialog_message, info.title.orEmpty())) },
)
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo, onDismiss: () -> Unit = {}) {
var checked by mutableStateOf(Settings.removeImageFiles)
awaitPermissionOrCancel(
confirmText = android.R.string.ok,
dismissText = android.R.string.cancel,
title = R.string.download_remove_dialog_title,
onDismiss = onDismiss,
text = {
Column {
Text(text = stringResource(id = R.string.download_remove_dialog_message, info.title.orEmpty()))
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = checked, onCheckedChange = { checked = it })
Text(text = stringResource(id = R.string.download_remove_dialog_check_text))
}
}
},
)
Settings.removeImageFiles = checked
withIOContext {
DownloadManager.deleteDownload(info.gid, checked)
}
}
suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) {
val defaultLabel = appCtx.getString(R.string.default_download_label_name)
@@ -197,7 +197,7 @@ class MainActivity : EhActivity() {
// Trick: Tweak NavigationUI to disable multiple backstack
binding.navView.setNavigationItemSelectedListener {
val navigationView = binding.navView
val handled = onNavDestinationSelected2(it, navController)
val handled = navigationView.checkedItem?.itemId == it.itemId || onNavDestinationSelected2(it, navController)
if (handled) {
val parent = navigationView.parent
if (parent is Openable) {
@@ -398,6 +398,10 @@ class MainActivity : EhActivity() {
}
}
fun clearNavCheckedItem() {
binding.navView.setCheckedItem(R.id.nav_stub)
}
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
showTip(getString(id), length, useToast)
}
@@ -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)
}
@@ -47,6 +47,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -54,11 +55,12 @@ 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.SimpleItemAnimator
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import arrow.core.partially1
import coil.compose.AsyncImage
@@ -79,11 +81,11 @@ 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
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.confirmRemoveDownload
import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.CheckBoxDialogBuilder
@@ -98,8 +100,10 @@ import com.hippo.ehviewer.ui.main.requestOf
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.setMD3Content
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
@@ -108,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
@@ -132,18 +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)
}
@@ -161,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
}
}
@@ -181,7 +180,6 @@ class DownloadsScene :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DownloadManager.addDownloadInfoListener(this)
if (savedInstanceState == null) {
onInit()
} else {
@@ -189,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 }
}.filter(::filter)
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) {
@@ -247,6 +249,7 @@ class DownloadsScene :
savedInstanceState: Bundle?,
): View {
_binding = SceneDownloadBinding.inflate(inflater, container!!)
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
binding.run {
setLiftOnScrollTargetView(recyclerView)
mViewTransition = ViewTransition(content, tip)
@@ -288,11 +291,6 @@ class DownloadsScene :
recyclerView.layoutManager = layoutManager
recyclerView.clipToPadding = false
recyclerView.clipChildren = false
// Cancel change animation
val itemAnimator = recyclerView.itemAnimator
if (itemAnimator is SimpleItemAnimator) {
itemAnimator.supportsChangeAnimations = false
}
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
val decoration = object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
@@ -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)
@@ -313,7 +307,7 @@ class DownloadsScene :
}
override fun isLongPressDragEnabled() = false
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT)
}
override fun onMove(
recyclerView: RecyclerView,
@@ -325,18 +319,24 @@ 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)
}
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val info = mList!![position]
lifecycleScope.launchUI {
dialogState.confirmRemoveDownload(info) {
mAdapter!!.notifyItemChanged(position)
}
}
}
})
mItemTouchHelper!!.attachToRecyclerView(recyclerView)
fastScroller.attachToRecyclerView(recyclerView)
@@ -349,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() {
@@ -393,8 +407,7 @@ class DownloadsScene :
mType + 1,
) { dialog: DialogInterface, which: Int ->
mType = which - 1
updateForLabel()
updateView()
updateInfoList()
dialog.dismiss()
}
.show()
@@ -447,7 +460,7 @@ class DownloadsScene :
}
}
fun updateView() {
private fun updateView() {
if (mViewTransition != null) {
if (mList.isNullOrEmpty()) {
mViewTransition!!.showView(1)
@@ -595,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)
@@ -660,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
@@ -708,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
}
@@ -952,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))
@@ -971,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(
@@ -984,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(
@@ -1030,7 +986,6 @@ class DownloadsScene :
if (mLabel == mOriginalLabel) {
mLabel = text
updateForLabel()
updateView()
}
}
}
@@ -1119,7 +1074,6 @@ class DownloadsScene :
withUIContext {
mLabel = null
updateForLabel()
updateView()
mLabelAdapter!!.notifyItemRemoved(position)
}
}
@@ -1137,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
}
}
@@ -51,6 +51,8 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.CalendarConstraints.DateValidator
import com.google.android.material.datepicker.CompositeDateValidator
@@ -78,17 +80,18 @@ import com.hippo.ehviewer.ui.legacy.HandlerDrawable
import com.hippo.ehviewer.ui.legacy.ViewTransition
import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.SimpleHandler
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.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import rikka.core.res.resolveColor
import java.time.Instant
@@ -160,6 +163,10 @@ class FavoritesScene : SearchBarScene() {
}
}
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
private fun onItemClick(position: Int) {
// Skip if in search mode
if (!tracker.isInCustomChoice) {
@@ -191,10 +198,11 @@ class FavoritesScene : SearchBarScene() {
}
if (keyword.isNullOrEmpty()) {
setSearchBarHint(getString(R.string.favorites_title, favCatName))
setSearchBarText(null)
} 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
}
@@ -222,6 +230,7 @@ class FavoritesScene : SearchBarScene() {
savedInstanceState: Bundle?,
): View {
_binding = SceneFavoritesBinding.inflate(inflater, container!!)
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
setOnApplySearch {
if (!tracker.isInCustomChoice) {
switchFav(urlBuilder.favCat, it)
@@ -327,8 +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 {
lifecycleScope.launchUI {
viewLifecycleOwner.lifecycleScope.launch {
adapter.loadStateFlow.collectLatest {
when (val state = it.refresh) {
is LoadState.Loading -> {
showSearchBar()
@@ -381,7 +390,36 @@ class FavoritesScene : SearchBarScene() {
}
switchFav(Settings.recentFavCat)
}
allowEmptySearch = false
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeMovementFlags(0, ItemTouchHelper.LEFT)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
) = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val info = mAdapter!!.peek(position)!!
lifecycleScope.launchIO {
dialogState.awaitPermissionOrCancel(
confirmText = R.string.delete,
dismissText = android.R.string.cancel,
title = R.string.delete_favorites_dialog_title,
onDismiss = { mAdapter!!.notifyItemChanged(position) },
) {
Text(text = stringResource(id = R.string.delete_favorites_dialog_message, 1))
}
if (urlBuilder.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
EhDB.removeLocalFavorites(info)
} else {
EhEngine.modifyFavorites(info.gid, info.token)
}
}
}
})
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
return binding.root
}
@@ -439,6 +477,16 @@ class FavoritesScene : SearchBarScene() {
}
}
override fun onSearchViewExpanded() {
super.onSearchViewExpanded()
selectSearchFab(true)
}
override fun onSearchViewHidden() {
super.onSearchViewHidden()
selectActionFab(true)
}
private fun showGoToDialog() {
context ?: return
val local = LocalDateTime.of(2007, 3, 21, 0, 0)
@@ -528,5 +576,3 @@ class FavoritesScene : SearchBarScene() {
}
}
}
private const val ANIMATE_TIME = 300L
@@ -68,7 +68,6 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
@@ -159,6 +158,7 @@ import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconButton
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconToggleButton
import com.hippo.ehviewer.ui.tools.GalleryDetailRating
import com.hippo.ehviewer.util.AppHelper
import com.hippo.ehviewer.util.ExceptionUtils
@@ -712,7 +712,7 @@ class GalleryDetailScene : BaseScene() {
stringResource(id = R.string.not_favorited)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
FilledTonalIconToggleButton(
FilledTertiaryIconToggleButton(
checked = favored,
onCheckedChange = { modifyFavourite() },
) {
@@ -15,7 +15,6 @@
*/
package com.hippo.ehviewer.ui.scene
import android.animation.Animator
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
@@ -66,10 +65,12 @@ import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_IMAGE_SEARCH
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
@@ -98,13 +99,11 @@ 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.getParcelableCompat
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
@@ -130,10 +129,10 @@ class VMStorage1 : ViewModel() {
if (urlBuilder.mode == MODE_TOPLIST) {
// TODO: Since we know total pages, let pager support jump
val key = (params.key ?: urlBuilder.mJumpTo ?: "0").toInt()
val prev = if (key != 0) key - 1 else null
val next = if (key != 199) key + 1 else null
val prev = (key - 1).takeIf { it > 0 }
val next = (key + 1).takeIf { it < TOPLIST_PAGES }
runSuspendCatching {
urlBuilder.setJumpTo(key.toString())
urlBuilder.setJumpTo(key)
EhEngine.getGalleryList(urlBuilder.build())
}.onFailure {
return@withIOContext LoadResult.Error(it)
@@ -156,7 +155,7 @@ class VMStorage1 : ViewModel() {
}
}
val r = runSuspendCatching {
if (ListUrlBuilder.MODE_IMAGE_SEARCH == urlBuilder.mode) {
if (MODE_IMAGE_SEARCH == urlBuilder.mode) {
EhEngine.imageSearch(
File(urlBuilder.imagePath!!),
urlBuilder.isUseSimilarityScan,
@@ -192,23 +191,15 @@ class GalleryListScene : SearchBarScene() {
}
private var _binding: SceneGalleryListBinding? = null
private val binding get() = _binding!!
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mSearchFab.visibility = View.INVISIBLE
}
}
private val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
binding.fabLayout.primaryFab?.visibility = View.INVISIBLE
}
}
private var fabAnimator: ViewPropertyAnimator? = null
private var mViewTransition: ViewTransition? = null
private var mAdapter: GalleryAdapter? = null
lateinit var mQuickSearchList: MutableList<QuickSearch>
private var mHideActionFabSlop = 0
private var mShowActionFab = true
private var mState = State.NORMAL
override val fabLayout get() = binding.fabLayout
override fun getMenuResId(): Int {
return R.menu.scene_gallery_list_searchbar_menu
}
@@ -223,7 +214,6 @@ class GalleryListScene : SearchBarScene() {
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!")
}
args?.getString(KEY_GOTO)?.let { mUrlBuilder.mNext = it }
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -266,10 +256,7 @@ class GalleryListScene : SearchBarScene() {
val category = mUrlBuilder.category
val mode = mUrlBuilder.mode
val isPopular = mode == MODE_WHATS_HOT
val isTopList = mode == MODE_TOPLIST
if (isTopList != mIsTopList) {
mIsTopList = isTopList
}
mIsTopList = mode == MODE_TOPLIST
// Update fab visibility
binding.fabLayout.setSecondaryFabVisibilityAt(0, !isPopular)
@@ -295,6 +282,14 @@ class GalleryListScene : SearchBarScene() {
title = resources.getString(R.string.search)
}
setSearchBarHint(title)
when (mode) {
MODE_NORMAL -> if (category != EhUtils.NONE || !keyword.isNullOrEmpty()) {
mainActivity?.clearNavCheckedItem()
}
MODE_TAG, MODE_UPLOADER, MODE_IMAGE_SEARCH -> mainActivity?.clearNavCheckedItem()
else -> Unit
}
}
private val dialogState = DialogState()
@@ -368,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)
val noWatch = getString(R.string.gallery_list_empty_hit_subscription)
adapter.addLoadStateListener {
lifecycleScope.launchUI {
launch {
adapter.loadStateFlow.collectLatest {
when (val state = it.refresh) {
is LoadState.Loading -> {
showSearchBar()
@@ -397,11 +391,7 @@ class GalleryListScene : SearchBarScene() {
delay(500)
binding.refreshLayout.isRefreshing = false
if (mAdapter?.itemCount == 0) {
if (mUrlBuilder.mode == MODE_SUBSCRIPTION) {
binding.tip.text = noWatch
} else {
binding.tip.text = empty
}
binding.tip.text = empty
transition.showView(2)
} else {
transition.showView(0)
@@ -452,12 +442,14 @@ class GalleryListScene : SearchBarScene() {
override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {
when (position) {
0 -> showGoToDialog()
1 -> {
1 -> { // First
mUrlBuilder.setIndex(null, true)
mUrlBuilder.mJumpTo = null
mAdapter?.refresh()
}
2 -> {
2 -> { // Last
if (mIsTopList) {
mUrlBuilder.mJumpTo = "${TOPLIST_PAGES - 1}"
mAdapter?.refresh()
} else {
mUrlBuilder.setIndex("1", false)
@@ -481,7 +473,6 @@ class GalleryListScene : SearchBarScene() {
}
binding.fabLayout.primaryFab!!.setImageDrawable(actionFabDrawable)
addAboveSnackView(binding.fabLayout)
mSearchFab.setOnClickListener { onApplySearch() }
// Update list url builder
onUpdateUrlBuilder()
@@ -520,7 +511,7 @@ class GalleryListScene : SearchBarScene() {
val context = context ?: return
// Can't add image search as quick search
if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.mode) {
if (MODE_IMAGE_SEARCH == mUrlBuilder.mode) {
showTip(R.string.image_search_not_quick_search, LENGTH_LONG)
return
}
@@ -644,9 +635,9 @@ class GalleryListScene : SearchBarScene() {
private fun showGoToDialog() {
mAdapter ?: return
if (mIsTopList) {
val pages = 200
val page = mUrlBuilder.mJumpTo?.toIntOrNull() ?: 0
val title = getString(R.string.go_to)
val hint = getString(R.string.go_to_hint, 1, 200)
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
lifecycleScope.launch {
val text = dialogState.awaitInputText(
title = title,
@@ -659,13 +650,13 @@ class GalleryListScene : SearchBarScene() {
}.onFailure {
return@awaitInputText getString(R.string.error_invalid_number)
}.getOrThrow()
if (goTo < 0 || goTo >= pages) {
if (goTo !in 0..<TOPLIST_PAGES) {
getString(R.string.error_out_of_range)
} else {
null
}
}.trim().toInt() - 1
mUrlBuilder.setJumpTo(text.toString())
mUrlBuilder.setJumpTo(text)
mAdapter?.refresh()
}
} else {
@@ -738,70 +729,6 @@ class GalleryListScene : SearchBarScene() {
}
}
private fun selectSearchFab(animation: Boolean) {
_binding ?: return
mShowActionFab = false
if (animation) {
val fab: View? = binding.fabLayout.primaryFab
val delay: Long
if (View.INVISIBLE == fab!!.visibility) {
delay = 0L
} else {
delay = ANIMATE_TIME
binding.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 {
binding.fabLayout.setExpanded(expanded = false, animation = false)
val fab: View? = binding.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
}
}
private fun selectActionFab(animation: Boolean) {
_binding ?: return
mShowActionFab = true
if (animation) {
val delay: Long
if (View.INVISIBLE == mSearchFab.visibility) {
delay = 0L
} else {
delay = ANIMATE_TIME
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
.setListener(mSearchFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
}
val fab: View? = binding.fabLayout.primaryFab
fab!!.visibility = View.VISIBLE
fab.rotation = -45.0f
fab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
binding.fabLayout.setExpanded(expanded = false, animation = false)
val fab: View? = binding.fabLayout.primaryFab
fab!!.visibility = View.VISIBLE
fab.scaleX = 1.0f
fab.scaleY = 1.0f
mSearchFab.visibility = View.INVISIBLE
mSearchFab.scaleX = 0.0f
mSearchFab.scaleY = 0.0f
}
}
private fun setState(state: State) {
setState(state, true)
}
@@ -863,24 +790,35 @@ class GalleryListScene : SearchBarScene() {
private fun onApplySearch(query: String?) {
_binding ?: return
lifecycleScope.launchIO {
val builder = ListUrlBuilder()
val oldMode = mUrlBuilder.mode
if (mState == State.SEARCH || mState == State.SEARCH_SHOW_LIST) {
try {
binding.searchLayout.formatListUrlBuilder(mUrlBuilder, query)
binding.searchLayout.formatListUrlBuilder(builder, query)
} catch (e: EhException) {
showTip(e.message, LENGTH_LONG)
return@launchIO
}
} else {
val oldMode = mUrlBuilder.mode
// If it's MODE_SUBSCRIPTION, keep it
val newMode = if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else MODE_NORMAL
mUrlBuilder.reset()
mUrlBuilder.mode = newMode
mUrlBuilder.keyword = query
builder.mode = newMode
builder.keyword = query
}
withUIContext {
onUpdateUrlBuilder()
mAdapter?.refresh()
when (oldMode) {
MODE_TOPLIST, MODE_WHATS_HOT -> {
// Wait for search view to hide
delay(300)
navAnimated(R.id.galleryListScene, builder.toStartArgs())
}
else -> {
mUrlBuilder = builder
onUpdateUrlBuilder()
mAdapter?.refresh()
}
}
setState(State.NORMAL)
}
}
@@ -899,26 +837,23 @@ class GalleryListScene : SearchBarScene() {
private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) :
RecyclerView.Adapter<QsDrawerHolder>() {
private val keywords = intArrayOf(11, 12, 13, 15)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {
val holder = QsDrawerHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
if (!mIsTopList) {
holder.itemView.setOnClickListener {
holder.itemView.setOnClickListener {
if (!mIsTopList) {
val q = mQuickSearchList[holder.bindingAdapterPosition]
mUrlBuilder.set(q)
onUpdateUrlBuilder()
mAdapter?.refresh()
setState(State.NORMAL)
closeSideSheet()
}
} else {
val keywords = intArrayOf(11, 12, 13, 15)
holder.itemView.setOnClickListener {
// Navigate to galleryListScene to clear nav checked item
navAnimated(R.id.galleryListScene, ListUrlBuilder().apply { set(q) }.toStartArgs())
} else {
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
mUrlBuilder.mJumpTo = null
onUpdateUrlBuilder()
mAdapter?.refresh()
setState(State.NORMAL)
closeSideSheet()
}
setState(State.NORMAL)
closeSideSheet()
}
return holder
}
@@ -1048,7 +983,6 @@ class GalleryListScene : SearchBarScene() {
companion object {
const val KEY_ACTION = "action"
const val KEY_GOTO = "goto"
const val ACTION_HOMEPAGE = "action_homepage"
const val ACTION_SUBSCRIPTION = "action_subscription"
const val ACTION_WHATS_HOT = "action_whats_hot"
@@ -1056,7 +990,6 @@ class GalleryListScene : SearchBarScene() {
const val ACTION_LIST_URL_BUILDER = "action_list_url_builder"
const val KEY_LIST_URL_BUILDER = "list_url_builder"
const val KEY_STATE = "state"
private const val ANIMATE_TIME = 300L
fun ListUrlBuilder.toStartArgs() = bundleOf(
KEY_ACTION to ACTION_LIST_URL_BUILDER,
KEY_LIST_URL_BUILDER to this,
@@ -1064,6 +997,8 @@ class GalleryListScene : SearchBarScene() {
}
}
private const val TOPLIST_PAGES = 200
@Parcelize
enum class State : Parcelable {
NORMAL, SIMPLE_SEARCH, SEARCH, SEARCH_SHOW_LIST
@@ -1,5 +1,6 @@
package com.hippo.ehviewer.ui.scene
import android.animation.Animator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
@@ -40,7 +41,10 @@ import com.hippo.ehviewer.dao.Search
import com.hippo.ehviewer.dao.SearchDao
import com.hippo.ehviewer.databinding.SceneSearchbarBinding
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
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.jamal.composeprefs3.ui.ifNotNullThen
import com.jamal.composeprefs3.ui.ifTrueThen
import eu.kanade.tachiyomi.util.lang.launchIO
@@ -57,6 +61,18 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
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
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = SceneSearchbarBinding.inflate(inflater, container, false)
@@ -99,6 +115,7 @@ 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
@@ -147,6 +164,83 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
privLockModeStart = null
}
fun showSearchFab(animation: Boolean, delay: Long = 0) {
if (animation) {
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 {
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
}
}
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
fab.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.VISIBLE
fab.scaleX = 1.0f
fab.scaleY = 1.0f
}
}
fun setSearchBarHint(hint: String?) {
binding.toolbar.hint = hint
}
@@ -329,3 +423,5 @@ fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
}
}
}
const val ANIMATE_TIME = 300L
@@ -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,
@@ -193,6 +193,27 @@ fun EhScreen() {
summary = stringResource(id = R.string.settings_my_tags_summary),
) { navController.navigate(MYTAGS_SCREEN) }
}
var defaultFavSlot by Settings::defaultFavSlot.observed
val disabled = stringResource(id = R.string.disabled_nav)
val localFav = stringResource(id = R.string.local_favorites)
val summary = when (defaultFavSlot) {
-1 -> localFav
in 0..9 -> Settings.favCat[defaultFavSlot]
else -> stringResource(id = R.string.default_favorites_warning)
}
val items = if (signin) {
arrayOf(disabled, localFav, *Settings.favCat)
} else {
arrayOf(disabled, localFav)
}
Preference(
title = stringResource(id = R.string.default_favorites_collection),
summary = summary,
) {
coroutineScope.launch {
defaultFavSlot = dialogState.showSelectItem(*items, title = R.string.default_favorites_collection) - 2
}
}
SimpleMenuPreferenceInt(
title = stringResource(id = rikka.core.R.string.dark_theme),
entry = R.array.night_mode_entries,
@@ -244,27 +265,6 @@ fun EhScreen() {
entryValueRes = R.array.thumb_resolution_entry_values,
value = thumbResolution,
)
var defaultFavSlot by Settings::defaultFavSlot.observed
val disabled = stringResource(id = R.string.disabled_nav)
val localFav = stringResource(id = R.string.local_favorites)
val summary = when (defaultFavSlot) {
-1 -> localFav
in 0..9 -> Settings.favCat[defaultFavSlot]
else -> stringResource(id = R.string.default_favorites_warning)
}
val items = if (signin) {
arrayOf(disabled, localFav, *Settings.favCat)
} else {
arrayOf(disabled, localFav)
}
Preference(
title = stringResource(id = R.string.default_favorites_collection),
summary = summary,
) {
coroutineScope.launch {
defaultFavSlot = dialogState.showSelectItem(*items, title = R.string.default_favorites_collection) - 2
}
}
SwitchPreference(
title = stringResource(id = R.string.settings_eh_force_eh_thumb),
summary = stringResource(id = R.string.settings_eh_force_eh_thumb_summary),
@@ -3,12 +3,10 @@ package com.hippo.ehviewer.ui.tools
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@@ -148,18 +146,25 @@ class DialogState {
@StringRes confirmText: Int? = null,
@StringRes dismissText: Int? = null,
@StringRes title: Int? = null,
onDismiss: () -> Unit = {},
text: (@Composable () -> Unit)? = null,
) {
return dialog { cont ->
AlertDialog(
onDismissRequest = { cont.cancel() },
onDismissRequest = {
onDismiss()
cont.cancel()
},
confirmButton = {
TextButton(onClick = { cont.resume(Unit) }) {
Text(text = stringResource(id = confirmText ?: android.R.string.ok))
}
},
dismissButton = dismissText.ifNotNullThen {
TextButton(onClick = { cont.cancel() }) {
TextButton(onClick = {
onDismiss()
cont.cancel()
}) {
Text(text = stringResource(id = dismissText!!))
}
},
@@ -195,12 +200,22 @@ class DialogState {
vararg items: String,
@StringRes title: Int,
): Int = showNoButton {
Column(modifier = Modifier.padding(24.dp)) {
Text(text = stringResource(id = title), style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.size(16.dp))
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
items.forEachIndexed { index, text ->
Text(text = text, modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth().padding(vertical = 8.dp), style = MaterialTheme.typography.titleMedium)
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(id = title),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
style = MaterialTheme.typography.headlineSmall,
)
LazyColumn {
itemsIndexed(items) { index, text ->
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
Text(
text = text,
modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
style = MaterialTheme.typography.titleMedium,
)
}
}
}
}
@@ -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
+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>
+1 -3
View File
@@ -376,8 +376,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 +407,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>
+1 -3
View File
@@ -420,8 +420,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 +465,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>
+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