Compare commits

..
3 Commits
Author SHA1 Message Date
FooIbar 10c7019aeb Release 1.8.10.2
Releases / Build (push) Has been cancelled
2023-10-27 19:47:48 +08:00
FooIbar 76aa401800 Sync with upstream API changes
(cherry picked from commit 44a6253bfc)
2023-10-27 19:46:50 +08:00
FooIbar dab5878f74 Fix unexpected touch scale behavior in PageViewer
(cherry picked from commit 0c78673870)
2023-10-27 19:46:42 +08:00
118 changed files with 1776 additions and 1450 deletions

No files matched your search

+1 -10
View File
@@ -64,7 +64,7 @@ body:
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
placeholder: 06-15 17:44:53.704 23382 23382 E ...
validations:
required: true
required: false
- type: textarea
id: more
@@ -79,15 +79,6 @@ body:
validations:
required: false
- type: input
id: site
attributes:
label: 浏览站点 / Browsing site
description: E-Hentai / ExHentai
placeholder: E-Hentai
validations:
required: true
- type: input
id: version
attributes:
+1 -1
View File
@@ -76,7 +76,7 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
| Flavor | Feature |
|-|-|
| Default | Android 9+, full support |
| Marshmallow | Android 6.0-8.1, limited support* |
| Marshmallow | Android 6.0+, limited support* |
| OSS | FOSS dependencies** |
| GMS | Proprietary GMS component |
+4 -2
View File
@@ -64,8 +64,8 @@ android {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
targetSdk = 34
versionCode = 180045
versionName = "1.8.11.1"
versionCode = 180044
versionName = "1.8.10.2"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -231,6 +231,7 @@ dependencies {
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.work.runtime)
implementation(libs.drawer)
implementation(libs.photoview) // Dead Dependency
implementation(libs.directionalviewpager) // Dead Dependency
// https://github.com/google/accompanist/releases
@@ -248,6 +249,7 @@ dependencies {
implementation(libs.aboutlibraries.core)
implementation(libs.insetter) // Dead Dependency
implementation(libs.bundles.rikkax)
implementation(platform(libs.arrow.stack))
implementation(libs.arrow.fx.coroutines)
@@ -1,8 +1,8 @@
package com.hippo.ehviewer.cronet
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.CHROME_ACCEPT
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
import com.hippo.ehviewer.client.CHROME_USER_AGENT
import com.hippo.ehviewer.client.EhCookieStore
import java.nio.ByteBuffer
import kotlin.contracts.InvocationKind
@@ -26,7 +26,7 @@ val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
setUserAgent(Settings.userAgent)
setUserAgent(CHROME_USER_AGENT)
}.build()
class CronetRequest {
+1 -1
View File
@@ -24,7 +24,7 @@ set(BUILD_TESTING 0)
FetchContent_Declare(liblzma
GIT_REPOSITORY https://github.com/FooIbar/xz.git
GIT_TAG v5.4.5
GIT_TAG v5.4.4
)
FetchContent_MakeAvailable(liblzma)
@@ -1,6 +1,7 @@
package androidx.navigation.ui
import android.annotation.SuppressLint
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.navigation.ActivityNavigator
import androidx.navigation.NavController
@@ -9,10 +10,9 @@ import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavOptions
@SuppressLint("PrivateResource")
fun onNavDestinationSelected2(@IdRes id: Int, navController: NavController): Boolean {
if (navController.currentDestination?.matchDestination(id) == true) return true
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (navController.currentDestination!!.parent!!.findNode(id) is ActivityNavigator.Destination) {
if (navController.currentDestination!!.parent!!.findNode(item.itemId) is ActivityNavigator.Destination) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
@@ -26,9 +26,9 @@ fun onNavDestinationSelected2(@IdRes id: Int, navController: NavController): Boo
val options = builder.build()
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(id, null, options)
navController.navigate(item.itemId, null, options)
// Return true only if the destination we've navigated to matches the MenuItem
navController.currentDestination?.matchDestination(id) == true
navController.currentDestination?.matchDestination(item.itemId) == true
} catch (e: IllegalArgumentException) {
false
}
@@ -51,7 +51,6 @@ import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.isCronetSupported
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.launch
import moe.tarsin.kt.unreachable
@@ -92,7 +91,7 @@ class EhApplication : Application(), ImageLoaderFactory {
migrateCookies()
}
launchIO {
EhTagDatabase
EhTagDatabase.update()
}
launchIO {
EhDB
@@ -171,7 +170,6 @@ class EhApplication : Application(), ImageLoaderFactory {
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
}
chunker { alwaysReadResponseBody(false) }
addInterceptor(UncaughtExceptionInterceptor())
addInterceptor(CloudflareInterceptor(appCtx))
}
}
@@ -2,7 +2,6 @@
package com.hippo.ehviewer
import com.hippo.ehviewer.client.CHROME_USER_AGENT
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import java.util.Locale
@@ -25,7 +24,6 @@ object Settings : DataStorePreferences(null) {
var favCount by intArrayPref("fav_count", 10).emitTo(_favFlow)
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
val needSignInFlow: Flow<Boolean>
var downloadScheme by stringOrNullPref("image_scheme", null)
var downloadAuthority by stringOrNullPref("image_authority", null)
var downloadPath by stringOrNullPref("image_path", null)
@@ -43,7 +41,6 @@ object Settings : DataStorePreferences(null) {
var thumbResolution by intPref("thumb_resolution_2", 0)
var readCacheSize by intPref("read_cache_size_2", 640)
var launchPage by intPref("launch_page_2", 0)
var commentThreshold by intPref("comment_threshold", -101)
var forceEhThumb by boolPref("force_eh_thumb", false)
var showComments by boolPref("show_gallery_comments", true)
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
@@ -62,7 +59,7 @@ object Settings : DataStorePreferences(null) {
var saveCrashLog by boolPref("save_crash_log", true)
var security by boolPref("require_unlock", false)
var removeImageFiles by boolPref("include_pic", true)
var needSignIn by boolPref("need_sign_in", true).also { needSignInFlow = it.valueFlow() }
var needSignIn by boolPref("need_sign_in", true)
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
@@ -78,8 +75,6 @@ object Settings : DataStorePreferences(null) {
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
var updateIntervalDays by intPref("update_interval_days", 0)
var recentToplist by stringPref("recent_toplist", "11")
var userAgent by stringPref("user_agent", CHROME_USER_AGENT)
var recentDownloadLabel by stringOrNullPref("recent_download_label", null)
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
var displayName by stringOrNullPref("display_name", null)
@@ -34,6 +34,7 @@ import com.hippo.ehviewer.client.parser.GalleryApiParser
import com.hippo.ehviewer.client.parser.GalleryDetailParser
import com.hippo.ehviewer.client.parser.GalleryListParser
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
import com.hippo.ehviewer.client.parser.GalleryPageApiParser
import com.hippo.ehviewer.client.parser.GalleryPageParser
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
import com.hippo.ehviewer.client.parser.HomeParser
@@ -365,7 +366,7 @@ object EhEngine {
pToken: String,
showKey: String?,
previousPToken: String?,
): GalleryPageParser.Result {
): GalleryPageApiParser.Result {
val referer = if (index > 0 && previousPToken != null) EhUrl.getPageUrl(gid, index - 1, previousPToken) else null
return ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
jsonBody {
@@ -375,7 +376,7 @@ object EhEngine {
put("imgkey", pToken)
put("showkey", showKey)
}
}.executeAndParsingWith { GalleryPageParser.parse(filterNot { it == '\\' }) }
}.executeAndParsingWith(GalleryPageApiParser::parse)
}
suspend fun rateGallery(
@@ -484,7 +485,7 @@ object EhEngine {
var hasPages = false
var hasRated = false
for (gi in list) {
if (!gi.simpleTags.isNullOrEmpty()) {
if (gi.simpleTags != null) {
hasTags = true
}
if (gi.pages != 0) {
@@ -17,17 +17,17 @@ object EhFilter : CoroutineScope {
val filters = async { EhDB.getAllFilter() as MutableList }
private suspend inline fun anyActive(mode: FilterMode, predicate: (Filter) -> Boolean) = filters.await().any { it.mode == mode && it.enable && predicate(it) }
private fun <R> Filter.launchOps(
callback: (suspend (R) -> Unit)? = null,
callback: ((R) -> Unit)? = null,
ops: suspend Filter.() -> R,
) = launch { ops().let { callback?.invoke(it) } }
fun Filter.remember(callback: (suspend (Boolean) -> Unit)? = null) = launchOps(callback) {
fun Filter.remember(callback: ((Boolean) -> Unit)? = null) = launchOps(callback) {
EhDB.addFilter(this).also { if (it) filters.await().add(this) }
}
fun Filter.trigger(callback: (suspend (Unit) -> Unit)? = null) = launchOps(callback) {
fun Filter.trigger(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
enable = !enable
EhDB.updateFilter(this)
}
fun Filter.forget(callback: (suspend (Unit) -> Unit)? = null) = launchOps(callback) {
fun Filter.forget(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
EhDB.deleteFilter(this)
filters.await().remove(this)
}
@@ -19,7 +19,6 @@ import android.util.Log
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.Settings
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.resume
@@ -45,7 +44,7 @@ import okhttp3.executeAsync
import okio.BufferedSource
inline fun ehRequest(url: String, referer: String? = null, origin: String? = null, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply {
addHeader("User-Agent", Settings.userAgent)
addHeader("User-Agent", CHROME_USER_AGENT)
addHeader("Accept", CHROME_ACCEPT)
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
referer?.let { addHeader("Referer", it) }
@@ -55,6 +55,7 @@ object EhTagDatabase : CoroutineScope {
)
private lateinit var tagGroups: Map<String, Map<String, String>>
private val updateLock = Mutex()
private val dbLock = Mutex()
override val coroutineContext = Dispatchers.IO + Job()
var initialized by mutableStateOf(false)
@@ -201,42 +202,42 @@ object EhTagDatabase : CoroutineScope {
}
}
private fun issueUpdateInMemoryData(file: File? = null) {
val dataFile = file ?: getMetadata(appCtx)?.let { metadata ->
val dataName = metadata[2]
val dir = AppConfig.getFilesDir("tag-translations")
File(dir, dataName).takeIf { it.exists() }
} ?: return
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
initialized = true
}
init {
launch {
updateLock.withLock {
private suspend fun issueUpdateInMemoryData() {
dbLock.withLock {
getMetadata(appCtx)?.let { urls ->
val dataName = urls[2]
val dir = AppConfig.getFilesDir("tag-translations")
val dataFile = File(dir, dataName).takeIf { it.exists() } ?: return
runSuspendCatching {
issueUpdateInMemoryData()
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
initialized = true
}.onFailure {
it.printStackTrace()
}
updateInternal()
}
}
}
init {
launch {
issueUpdateInMemoryData()
}
}
private suspend fun updateInternal() {
getMetadata(appCtx)?.let { metadata ->
val sha1Name = metadata[0]
val sha1Url = metadata[1]
val dataName = metadata[2]
val dataUrl = metadata[3]
val urls = getMetadata(appCtx)
urls?.let {
val sha1Name = urls[0]
val sha1Url = urls[1]
val dataName = urls[2]
val dataUrl = urls[3]
val dir = AppConfig.getFilesDir("tag-translations")
checkNotNull(dir)
val sha1File = File(dir, sha1Name)
val dataFile = File(dir, dataName)
runSuspendCatching {
runCatching {
// Check current sha1 and current data
val sha1 = getFileContent(sha1File)
if (!checkData(sha1, dataFile)) {
@@ -253,7 +254,7 @@ object EhTagDatabase : CoroutineScope {
if (tempSha1 == sha1) {
// The data is the same
FileUtils.delete(tempSha1File)
return
return@runCatching
}
// Save new data
@@ -264,7 +265,7 @@ object EhTagDatabase : CoroutineScope {
if (!checkData(tempSha1, tempDataFile)) {
FileUtils.delete(tempSha1File)
FileUtils.delete(tempDataFile)
return
return@runCatching
}
// Replace current sha1 and current data with new sha1 and new data
@@ -274,7 +275,7 @@ object EhTagDatabase : CoroutineScope {
tempDataFile.renameTo(dataFile)
// Read new EhTagDatabase
issueUpdateInMemoryData(dataFile)
issueUpdateInMemoryData()
}.onFailure {
it.printStackTrace()
}
@@ -146,6 +146,10 @@ object EhUtils {
Settings.needSignIn = true
}
fun needSignedIn(): Boolean {
return Settings.needSignIn
}
fun getSuitableTitle(gi: GalleryInfo): String {
return if (Settings.showJpnTitle) {
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
@@ -43,24 +43,63 @@ interface GalleryInfo {
?: title?.let { generateSLangFromTitle(it) }
}
private fun generateSLangFromTags(simpleTags: List<String>): String? {
for (tag in simpleTags) {
for (i in S_LANGS.indices) {
if (S_LANG_TAGS[i] == tag) {
return S_LANGS[i]
}
}
}
return null
}
private fun generateSLangFromTitle(title: String): String? {
for (i in S_LANGS.indices) {
if (S_LANG_PATTERNS[i].matcher(title).find()) {
return S_LANGS[i]
}
}
return null
}
companion object {
/**
* ISO 639-1
*/
const val S_LANG_JA = "JA"
const val S_LANG_EN = "EN"
const val S_LANG_ZH = "ZH"
const val S_LANG_NL = "NL"
const val S_LANG_FR = "FR"
const val S_LANG_DE = "DE"
const val S_LANG_HU = "HU"
const val S_LANG_IT = "IT"
const val S_LANG_KO = "KO"
const val S_LANG_PL = "PL"
const val S_LANG_PT = "PT"
const val S_LANG_RU = "RU"
const val S_LANG_ES = "ES"
const val S_LANG_TH = "TH"
const val S_LANG_VI = "VI"
val S_LANGS = arrayOf(
"EN",
"ZH",
"ES",
"KO",
"RU",
"FR",
"PT",
"TH",
"DE",
"IT",
"VI",
"PL",
"HU",
"NL",
S_LANG_EN,
S_LANG_ZH,
S_LANG_ES,
S_LANG_KO,
S_LANG_RU,
S_LANG_FR,
S_LANG_PT,
S_LANG_TH,
S_LANG_DE,
S_LANG_IT,
S_LANG_VI,
S_LANG_PL,
S_LANG_HU,
S_LANG_NL,
)
private val S_LANG_PATTERNS = arrayOf(
val S_LANG_PATTERNS = arrayOf(
Pattern.compile(
"[(\\[]eng(?:lish)?[)\\]]|英訳",
Pattern.CASE_INSENSITIVE,
@@ -92,7 +131,7 @@ interface GalleryInfo {
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
)
private val S_LANG_TAGS = arrayOf(
val S_LANG_TAGS = arrayOf(
"language:english",
"language:chinese",
"language:spanish",
@@ -109,26 +148,6 @@ interface GalleryInfo {
"language:dutch",
)
private fun generateSLangFromTags(simpleTags: List<String>): String? {
for (tag in simpleTags) {
for (i in S_LANGS.indices) {
if (S_LANG_TAGS[i] == tag) {
return S_LANGS[i]
}
}
}
return null
}
private fun generateSLangFromTitle(title: String): String? {
for (i in S_LANGS.indices) {
if (S_LANG_PATTERNS[i].matcher(title).find()) {
return S_LANGS[i]
}
}
return null
}
const val NOT_FAVORITED = -2
const val LOCAL_FAVORITED = -1
}
@@ -33,7 +33,7 @@ object GalleryApiParser {
titleJpn = item.titleJpn.unescapeXml()
category = getCategory(item.category)
thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!)
uploader = item.uploader?.unescapeXml()
uploader = item.uploader.unescapeXml()
posted = ParserUtils.formatDate(item.posted * 1000)
rating = item.rating
simpleTags = item.tags
@@ -54,8 +54,7 @@ object GalleryApiParser {
val titleJpn: String,
val category: String,
val thumb: String,
// Null in some old galleries
val uploader: String?,
val uploader: String,
val posted: Long,
@SerialName("filecount")
val pages: Int,
@@ -17,7 +17,6 @@ package com.hippo.ehviewer.client.parser
import arrow.core.unzip
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.EhUtils.getCategory
@@ -395,7 +394,7 @@ object GalleryDetailParser {
// filter comment
if (!comment.uploader) {
val sEhFilter = EhFilter
if (comment.score <= Settings.commentThreshold || sEhFilter.filterCommenter(comment.user!!) || sEhFilter.filterComment(comment.comment!!)) {
if (sEhFilter.filterCommenter(comment.user!!) || sEhFilter.filterComment(comment.comment!!)) {
return null
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.unescapeXml
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object GalleryPageApiParser {
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
fun parse(body: String) = runCatching {
val res = body.parseAs<JsonResult>()
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
groupValues[1].trim().unescapeXml()
}
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKeyAndOriginImageUrl)?.run {
groupValues[1].trim().unescapeXml()
}
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.skipHathKeyAndOriginImageUrl)?.run {
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
}
check(!imageUrl.isNullOrEmpty())
Result(imageUrl, skipHathKey, originImageUrl)
}.getOrElse { e ->
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error) }
throw ParseException("Parse image url and skip hath key error", e)
}
@Serializable
private data class JsonResult(
@SerialName("i3")
val imageUrl: String,
@SerialName("i6")
val skipHathKeyAndOriginImageUrl: String,
)
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
}
@@ -17,32 +17,46 @@ package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml
import java.util.regex.Pattern
object GalleryPageParser {
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([\\d-]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+/fullimg/[^\"]+)\">")
private val PATTERN_IMAGE_URL = Pattern.compile("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^)]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL =
Pattern.compile("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
// TODO Not sure about the size of show keys
private val PATTERN_SHOW_KEY = Regex("var showkey=\"([0-9a-z]+)\";")
private val PATTERN_SHOW_KEY = Pattern.compile("var showkey=\"([0-9a-z]+)\";")
fun parse(body: String): Result {
val imageUrl = PATTERN_IMAGE_URL.find(body)?.run {
groupValues[1].unescapeXml()
var m = PATTERN_IMAGE_URL.matcher(body)
val imageUrl = if (m.find()) {
m.group(1)!!.trim().unescapeXml()
} else {
null
}
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(body)?.run {
groupValues[1]
m = PATTERN_SKIP_HATH_KEY.matcher(body)
val skipHathKey = if (m.find()) {
m.group(1)!!.trim().unescapeXml()
} else {
null
}
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(body)?.run {
groupValues[1].unescapeXml()
m = PATTERN_ORIGIN_IMAGE_URL.matcher(body)
val originImageUrl = if (m.find()) {
m.group(1)!!.unescapeXml() + "fullimg.php" + m.group(2)!!.unescapeXml()
} else {
null
}
val showKey = PATTERN_SHOW_KEY.find(body)?.run {
groupValues[1]
m = PATTERN_SHOW_KEY.matcher(body)
val showKey = if (m.find()) {
m.group(1)
} else {
null
}
return if (!imageUrl.isNullOrEmpty()) {
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {
Result(imageUrl, skipHathKey, originImageUrl, showKey)
} else {
throw ParseException("Parse image url error")
throw ParseException("Parse image url and show error")
}
}
@@ -50,6 +64,6 @@ object GalleryPageParser {
val imageUrl: String,
val skipHathKey: String?,
val originImageUrl: String?,
val showKey: String?,
val showKey: String,
)
}
@@ -43,16 +43,14 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import moe.tarsin.coroutines.NamedMutex
import moe.tarsin.coroutines.withLock
typealias PasswdInvalidator = (String) -> Boolean
typealias PasswdProvider = suspend (PasswdInvalidator) -> String
class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider: PasswdProvider) : PageLoader2(), CoroutineScope {
class ArchivePageLoader(context: Context, private val uri: Uri, passwdFlow: Flow<String>) : PageLoader2(), CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
private lateinit var pfd: ParcelFileDescriptor
private val hostJob = launch(start = CoroutineStart.LAZY) {
@@ -67,8 +65,12 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider:
it ?: return@forEach
if (providePassword(it)) return@launch
}
val toAdd = passwdProvider { providePassword(it) }
archivePasswds = archivePasswds?.toMutableSet()?.apply { add(toAdd) } ?: setOf(toAdd)
passwdFlow.collect {
if (providePassword(it)) {
archivePasswds = archivePasswds?.toMutableSet()?.apply { add(it) } ?: setOf(it)
currentCoroutineContext().cancel()
}
}
}
}
@@ -28,10 +28,8 @@ import java.util.Locale
class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpiderListener {
private lateinit var mSpiderQueen: SpiderQueen
override fun start() {
if (!::mSpiderQueen.isInitialized) {
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
mSpiderQueen.addOnSpiderListener(this)
}
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
mSpiderQueen.addOnSpiderListener(this)
}
override fun stop() {
@@ -1,32 +0,0 @@
@file:Suppress("ktlint:standard:property-naming")
package com.hippo.ehviewer.icons
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
object EhIcons {
object Filled
object Big {
object Filled
val Default = Filled
}
val Default = Filled
}
inline fun bigIcon(
name: String,
block: ImageVector.Builder.() -> ImageVector.Builder,
): ImageVector = ImageVector.Builder(
name = name,
defaultWidth = BigIconDimension.dp,
defaultHeight = BigIconDimension.dp,
viewportWidth = MaterialIconDimension,
viewportHeight = MaterialIconDimension,
).block().build()
const val BigIconDimension = 120f
const val MaterialIconDimension = 24f
@@ -1,51 +0,0 @@
package com.hippo.ehviewer.icons.big
import androidx.compose.material.icons.materialPath
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.bigIcon
import com.hippo.ehviewer.util.unsafeLazy
val EhIcons.Big.Filled.History by unsafeLazy {
bigIcon(name = "History") {
materialPath {
moveTo(5.5F, 2.0F)
horizontalLineTo(22.0F)
verticalLineTo(0.0F)
horizontalLineTo(5.0F)
curveTo(3.3F, 0.0F, 2.0F, 1.3F, 2.0F, 3.0F)
verticalLineTo(21.0F)
curveTo(2.0F, 22.7F, 3.3F, 24.0F, 5.0F, 24.0F)
horizontalLineTo(22.0F)
verticalLineTo(4.0F)
horizontalLineTo(5.5F)
curveTo(4.1F, 4.0F, 4.1F, 2.0F, 5.5F, 2.0F)
close()
moveTo(20.0F, 6.0F)
verticalLineTo(22.0F)
horizontalLineTo(6.0F)
verticalLineTo(6.0F)
horizontalLineTo(20.0F)
moveTo(12.5F, 11.8F)
verticalLineTo(14.4F)
lineTo(14.9F, 15.8F)
lineTo(15.3F, 15.2F)
lineTo(13.3F, 14.0F)
verticalLineTo(11.9F)
moveTo(13.3F, 9.3F)
curveTo(11.5F, 9.3F, 10.0F, 10.3F, 9.2F, 11.7F)
lineTo(8.0F, 10.5F)
verticalLineTo(13.8F)
horizontalLineTo(11.3F)
lineTo(9.9F, 12.4F)
curveTo(10.5F, 11.1F, 11.8F, 10.3F, 13.3F, 10.3F)
curveTo(15.4F, 10.3F, 17.1F, 12.0F, 17.1F, 14.1F)
reflectiveCurveTo(15.4F, 17.9F, 13.3F, 17.9F)
curveTo(11.7F, 17.9F, 10.3F, 16.9F, 9.8F, 15.4F)
horizontalLineTo(8.7F)
curveTo(9.3F, 17.4F, 11.1F, 18.9F, 13.3F, 18.9F)
curveTo(15.9F, 18.9F, 18.1F, 16.8F, 18.1F, 14.1F)
curveTo(18.0F, 11.4F, 15.9F, 9.3F, 13.3F, 9.3F)
close()
}
}
}
@@ -1,53 +0,0 @@
package com.hippo.ehviewer.icons.big
import androidx.compose.material.icons.materialPath
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.bigIcon
import com.hippo.ehviewer.util.unsafeLazy
val EhIcons.Big.Filled.SadAndroid by unsafeLazy {
bigIcon(name = "SadAndroid") {
materialPath {
moveTo(16.5F, 10.8F)
curveToRelative(-0.61F, 0.0F, -1.1F, 0.49F, -1.1F, 1.1F)
curveToRelative(0.0F, 0.61F, 0.49F, 1.1F, 1.1F, 1.1F)
reflectiveCurveToRelative(1.1F, -0.49F, 1.1F, -1.1F)
curveTo(17.6F, 11.29F, 17.11F, 10.8F, 16.5F, 10.8F)
close()
moveTo(8.2F, 10.1F)
curveToRelative(-0.61F, 0.0F, -1.1F, 0.49F, -1.1F, 1.1F)
reflectiveCurveToRelative(0.49F, 1.1F, 1.1F, 1.1F)
reflectiveCurveToRelative(1.1F, -0.49F, 1.1F, -1.1F)
reflectiveCurveTo(8.81F, 10.1F, 8.2F, 10.1F)
close()
moveTo(22.1F, 16.4F)
curveToRelative(-0.46F, -3.33F, -2.24F, -5.93F, -4.81F, -7.45F)
lineTo(19.5F, 6.3F)
curveToRelative(0.1F, -0.1F, 0.1F, -0.3F, 0.0F, -0.4F)
curveToRelative(-0.1F, -0.1F, -0.3F, -0.1F, -0.4F, 0.0F)
lineToRelative(-2.27F, 2.79F)
curveToRelative(-1.26F, -0.66F, -2.69F, -1.07F, -4.23F, -1.19F)
curveToRelative(-1.4F, -0.15F, -2.77F, -0.01F, -4.05F, 0.37F)
lineTo(6.7F, 4.7F)
curveTo(6.6F, 4.6F, 6.4F, 4.5F, 6.3F, 4.6F)
curveTo(6.2F, 4.7F, 6.1F, 4.9F, 6.2F, 5.0F)
lineToRelative(1.79F, 3.05F)
curveToRelative(-2.83F, 1.02F, -5.12F, 3.3F, -6.19F, 6.55F)
lineToRelative(15.44F, 1.37F)
curveToRelative(0.0F, 0.02F, -0.01F, 0.04F, -0.01F, 0.05F)
curveToRelative(0.0F, 0.59F, 0.48F, 1.07F, 1.07F, 1.07F)
curveToRelative(0.54F, 0.0F, 0.97F, -0.41F, 1.04F, -0.93F)
lineTo(22.1F, 16.4F)
close()
moveTo(2.63F, 14.07F)
curveToRelative(1.51F, -3.74F, 4.83F, -6.03F, 8.84F, -6.03F)
curveToRelative(0.35F, 0.0F, 0.71F, 0.02F, 1.07F, 0.06F)
curveToRelative(4.55F, 0.36F, 7.94F, 3.33F, 8.84F, 7.64F)
lineToRelative(-2.12F, -0.19F)
curveToRelative(-0.27F, -0.68F, -0.95F, -1.46F, -0.95F, -1.46F)
reflectiveCurveToRelative(-0.58F, 0.66F, -0.89F, 1.29F)
lineTo(2.63F, 14.07F)
close()
}
}
}
@@ -1,47 +0,0 @@
package com.hippo.ehviewer.icons.filled
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.util.unsafeLazy
val EhIcons.Filled.SadPanda by unsafeLazy {
materialIcon(name = "SadPanda") {
materialPath {
moveTo(21.1F, 7.8F)
curveTo(22.2F, 7.4F, 23.0F, 6.3F, 23.0F, 5.0F)
curveTo(23.0F, 3.3F, 21.7F, 2.0F, 20.0F, 2.0F)
curveTo(18.9F, 2.0F, 17.9F, 2.6F, 17.4F, 3.6F)
lineTo(21.1F, 7.8F)
moveTo(6.6F, 3.6F)
curveTo(6.1F, 2.6F, 5.1F, 2.0F, 4.0F, 2.0F)
curveTo(2.3F, 2.0F, 1.0F, 3.3F, 1.0F, 5.0F)
curveTo(1.0F, 6.3F, 1.8F, 7.4F, 2.9F, 7.8F)
lineTo(6.6F, 3.6F)
moveTo(10.1F, 13.0F)
lineTo(8.0F, 15.1F)
curveTo(7.4F, 15.7F, 6.5F, 15.7F, 5.9F, 15.1F)
lineTo(5.9F, 15.1F)
curveTo(5.3F, 14.5F, 5.3F, 13.6F, 5.9F, 13.0F)
lineTo(8.0F, 10.9F)
curveTo(8.6F, 10.3F, 9.5F, 10.3F, 10.1F, 10.9F)
lineTo(10.1F, 10.9F)
curveTo(10.7F, 11.5F, 10.7F, 12.4F, 10.1F, 13.0F)
moveTo(16.0F, 15.1F)
lineTo(13.9F, 13.0F)
curveTo(13.3F, 12.4F, 13.3F, 11.5F, 13.9F, 10.9F)
lineTo(13.9F, 10.9F)
curveTo(14.5F, 10.3F, 15.4F, 10.3F, 16.0F, 10.9F)
lineTo(18.1F, 13.0F)
curveTo(18.7F, 13.6F, 18.7F, 14.5F, 18.1F, 15.1F)
lineTo(18.1F, 15.1F)
curveTo(17.5F, 15.7F, 16.6F, 15.7F, 16.0F, 15.1F)
moveTo(12.0F, 17.0F)
curveTo(12.8F, 17.0F, 13.5F, 17.4F, 13.5F, 18.0F)
reflectiveCurveTo(12.8F, 19.0F, 12.0F, 19.0F)
reflectiveCurveTo(10.5F, 18.6F, 10.5F, 18.0F)
reflectiveCurveTo(11.2F, 17.0F, 12.0F, 17.0F)
close()
}
}
}
@@ -1,61 +0,0 @@
package com.hippo.ehviewer.icons.filled
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.util.unsafeLazy
val EhIcons.Filled.Subscriptions by unsafeLazy {
materialIcon(name = "Subscriptions") {
materialPath {
moveTo(20.0F, 8.0F)
horizontalLineTo(4.0F)
verticalLineTo(6.0F)
horizontalLineToRelative(16.0F)
verticalLineTo(8.0F)
moveTo(18.0F, 2.0F)
horizontalLineTo(6.0F)
verticalLineToRelative(2.0F)
horizontalLineToRelative(12.0F)
verticalLineTo(2.0F)
moveTo(20.0F, 10.0F)
horizontalLineTo(4.0F)
curveToRelative(-1.1F, 0.0F, -2.0F, 0.9F, -2.0F, 2.0F)
verticalLineToRelative(8.0F)
curveToRelative(0.0F, 1.1F, 0.9F, 2.0F, 2.0F, 2.0F)
horizontalLineToRelative(16.0F)
curveToRelative(1.1F, 0.0F, 2.0F, -0.9F, 2.0F, -2.0F)
verticalLineToRelative(-8.0F)
curveTo(22.0F, 10.9F, 21.1F, 10.0F, 20.0F, 10.0F)
close()
moveTo(11.0F, 14.0F)
horizontalLineTo(7.0F)
verticalLineToRelative(1.0F)
horizontalLineToRelative(4.0F)
verticalLineToRelative(2.0F)
horizontalLineTo(7.0F)
verticalLineToRelative(1.0F)
horizontalLineToRelative(4.0F)
verticalLineToRelative(2.0F)
horizontalLineTo(5.0F)
verticalLineToRelative(-8.0F)
horizontalLineToRelative(6.0F)
verticalLineTo(14.0F)
close()
moveTo(19.0F, 20.0F)
horizontalLineToRelative(-2.0F)
verticalLineToRelative(-3.0F)
horizontalLineToRelative(-2.0F)
verticalLineToRelative(3.0F)
horizontalLineToRelative(-2.0F)
verticalLineToRelative(-8.0F)
horizontalLineToRelative(2.0F)
verticalLineToRelative(3.0F)
horizontalLineToRelative(2.0F)
verticalLineToRelative(-3.0F)
horizontalLineToRelative(2.0F)
verticalLineTo(20.0F)
close()
}
}
}
@@ -56,7 +56,8 @@ class Image private constructor(drawable: Drawable, private val src: AutoCloseab
var mObtainedDrawable: Drawable? = drawable
private set
val size = drawable.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 }
val size: Int
get() = mObtainedDrawable!!.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 }
@Synchronized
fun recycle() {
@@ -59,7 +59,7 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
suspend fun setMode(value: Int) {
mode = value
if (mode == SpiderQueen.MODE_DOWNLOAD && downloadDir == null) {
downloadDir = getGalleryDownloadDir(mGid)?.takeIf { it.ensureDir() }
downloadDir = getGalleryDownloadDir(mGid)!!.apply { check(ensureDir()) { "Download directory $uri is not valid directory!" } }
}
}
@@ -58,7 +58,6 @@ import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.LongList
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.util.requestPermission
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
@@ -69,6 +68,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.tarsin.coroutines.runSuspendCatching
import rikka.core.util.ContextUtils.requireActivity
import splitties.init.appCtx
object CommonOperations {
@@ -314,7 +314,7 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
title = EhUtils.getSuitableTitle(info),
)
}
with(context.findActivity<MainActivity>()) {
with(requireActivity<MainActivity>(context)) {
when (selected) {
0 -> navToReader(info)
1 -> withUIContext {
@@ -19,12 +19,13 @@ import android.content.res.Resources.Theme
import android.os.Bundle
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.navigation.ActivityNavigator
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.view.setSecureScreen
import rikka.insets.WindowInsetsHelper
import rikka.layoutinflater.view.LayoutInflaterFactory
abstract class EhActivity : AppCompatActivity() {
@StyleRes
@@ -42,9 +43,8 @@ abstract class EhActivity : AppCompatActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 = LayoutInflaterFactory(delegate).addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
super.onCreate(savedInstanceState)
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !isNightMode()
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightNavigationBars = !isNightMode()
}
override fun onResume() {
@@ -32,7 +32,7 @@ import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.thumbUrl
import com.hippo.ehviewer.databinding.InfoSheetBinding
import com.hippo.ehviewer.ui.scene.navWithUrl
import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.tellClipboardWithToast
private const val INDEX_URL = 2
private const val INDEX_PARENT = 9
@@ -83,7 +83,7 @@ class GalleryInfoBottomSheet(private val detail: GalleryDetail) : BottomSheetDia
navController.navWithUrl(content)
}
} else {
context.addTextToClipboard(content, true)
context tellClipboardWithToast content
if (index == INDEX_URL) {
// Save it to avoid detect the gallery
Settings.clipboardTextHashCode = data[index].hashCode()
@@ -15,6 +15,7 @@
*/
package com.hippo.ehviewer.ui
import android.annotation.SuppressLint
import android.app.assist.AssistContent
import android.content.DialogInterface
import android.content.Intent
@@ -25,112 +26,59 @@ import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FormatListNumbered
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Whatshot
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidViewBinding
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.customview.widget.Openable
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.onNavDestinationSelected2
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.Settings.launchPage
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.hippo.ehviewer.databinding.ActivityMainBinding
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.filled.Subscriptions
import com.hippo.ehviewer.image.Image.Companion.decodeBitmap
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
import com.hippo.ehviewer.ui.scene.GalleryListScene
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.ehviewer.ui.scene.ProgressScene
import com.hippo.ehviewer.ui.scene.navAnimated
import com.hippo.ehviewer.ui.scene.navWithUrl
import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.buildWindowInsets
import com.hippo.ehviewer.util.getParcelableExtraCompat
import com.hippo.ehviewer.util.getUrlFromClipboard
import com.hippo.ehviewer.util.set
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import java.io.File
import java.io.FileOutputStream
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import splitties.systemservices.clipboardManager
import splitties.systemservices.connectivityManager
private val navItems = arrayOf(
Triple(R.id.nav_homepage, R.string.homepage, Icons.Default.Home),
Triple(R.id.nav_subscription, R.string.subscription, EhIcons.Default.Subscriptions),
Triple(R.id.nav_whats_hot, R.string.whats_hot, Icons.Default.Whatshot),
Triple(R.id.nav_toplist, R.string.toplist, Icons.Default.FormatListNumbered),
Triple(R.id.nav_favourite, R.string.favourite, Icons.Default.Favorite),
Triple(R.id.nav_history, R.string.history, Icons.Default.History),
Triple(R.id.nav_downloads, R.string.downloads, Icons.Default.Download),
Triple(R.id.nav_settings, R.string.settings, Icons.Default.Settings),
)
class MainActivity : EhActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private val isInitializedFlow = MutableStateFlow(false)
private fun saveImageToTempFile(uri: Uri): File? {
val bitmap = runCatching {
@@ -149,15 +97,17 @@ class MainActivity : EhActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
lifecycleScope.launchUI {
isInitializedFlow.first { it }
if (!handleIntent(intent)) {
if (intent != null && Intent.ACTION_VIEW == intent.action) {
if (intent.data != null) {
val url = intent.data.toString()
EditTextDialogBuilder(this@MainActivity, url, "")
.setTitle(R.string.error_cannot_parse_the_url)
.setPositiveButton(android.R.string.copy) { _, _ ->
this@MainActivity.addTextToClipboard(url)
.setPositiveButton(android.R.string.copy) { _: DialogInterface?, _: Int ->
this@MainActivity.addTextToClipboard(
url,
false,
)
}
.show()
}
@@ -211,108 +161,58 @@ class MainActivity : EhActivity() {
return false
}
var drawerLocked by mutableStateOf(false)
private var openDrawerFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
private var recomposeFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
fun recompose() {
recomposeFlow.tryEmit(Unit)
private fun setNavGraph() {
navController.apply {
graph = navInflater.inflate(R.navigation.nav_graph).apply {
when (
when (val value = launchPage) {
0 -> GalleryListScene.ACTION_HOMEPAGE
1 -> GalleryListScene.ACTION_SUBSCRIPTION
2 -> GalleryListScene.ACTION_WHATS_HOT
3 -> GalleryListScene.ACTION_TOP_LIST
else -> throw IllegalStateException("Unexpected value: $value")
}
) {
GalleryListScene.ACTION_HOMEPAGE -> setStartDestination(R.id.nav_homepage)
GalleryListScene.ACTION_SUBSCRIPTION -> setStartDestination(R.id.nav_subscription)
GalleryListScene.ACTION_WHATS_HOT -> setStartDestination(R.id.nav_whats_hot)
GalleryListScene.ACTION_TOP_LIST -> setStartDestination(R.id.nav_toplist)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setMD3Content {
val configuration = LocalConfiguration.current
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
val recomposeScope = currentRecomposeScope
fun isSelected(id: Int) = ::navController.isInitialized && id == navController.currentDestination?.id
fun closeDrawer() = scope.launch { drawerState.close() }
LaunchedEffect(Unit) {
openDrawerFlow.collect {
drawerState.open()
}
}
LaunchedEffect(Unit) {
recomposeFlow.collect {
recomposeScope.invalidate()
}
}
BackHandler(drawerState.isOpen) {
closeDrawer()
}
LocalTouchSlopProvider(3f) {
ModalNavigationDrawer(
drawerContent = {
ModalDrawerSheet(
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 56).dp),
windowInsets = WindowInsets(0, 0, 0, 0),
) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier.verticalScroll(scrollState).navigationBarsPadding(),
) {
Image(
painter = painterResource(id = R.drawable.sadpanda_low_poly),
contentDescription = null,
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
contentScale = ContentScale.FillWidth,
)
navItems.forEach { (id, stringId, icon) ->
NavigationDrawerItem(
label = {
Text(text = stringResource(id = stringId))
},
selected = isSelected(id),
onClick = {
closeDrawer()
onNavDestinationSelected2(id, navController)
},
modifier = Modifier.padding(horizontal = 12.dp),
icon = {
Icon(imageVector = icon, contentDescription = null)
},
)
}
}
}
},
drawerState = drawerState,
gesturesEnabled = !drawerLocked || drawerState.isOpen,
) {
val insets = buildWindowInsets {
set(
WindowInsetsCompat.Type.statusBars(),
WindowInsets.statusBars,
)
set(
WindowInsetsCompat.Type.navigationBars(),
WindowInsets.navigationBars,
)
set(
WindowInsetsCompat.Type.ime(),
WindowInsets.ime,
)
}
AndroidViewBinding(factory = { inflater, parent, attachToParent ->
ActivityMainBinding.inflate(inflater, parent, attachToParent).apply {
val navHostFragment = fragmentContainer.getFragment<NavHostFragment>()
navController = navHostFragment.navController.apply {
graph = navInflater.inflate(R.navigation.nav_graph).apply {
check(launchPage in 0..3)
setStartDestination(navItems[launchPage].first)
}
}
isInitializedFlow.value = true
}
}) {
ViewCompat.dispatchApplyWindowInsets(root, insets)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navController = navHostFragment.navController
if (!EhUtils.needSignedIn()) setNavGraph()
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
binding.navView.setupWithNavController(navController)
// Trick: Tweak NavigationUI to disable multiple backstack
binding.navView.setNavigationItemSelectedListener {
val navigationView = binding.navView
val handled = navigationView.checkedItem?.itemId == it.itemId || onNavDestinationSelected2(it, navController)
if (handled) {
val parent = navigationView.parent
if (parent is Openable) {
parent.close()
} else {
@SuppressLint("RestrictedApi")
val bottomSheetBehavior = NavigationUI.findBottomSheetBehavior(navigationView)
if (bottomSheetBehavior != null) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
}
}
handled
}
// Trick End
if (savedInstanceState == null) {
if (intent.action != Intent.ACTION_MAIN) {
@@ -320,10 +220,7 @@ class MainActivity : EhActivity() {
}
checkDownloadLocation()
if (Settings.meteredNetworkWarning) {
lifecycleScope.launchUI {
isInitializedFlow.first { it }
checkMeteredNetwork()
}
checkMeteredNetwork()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!Settings.appLinkVerifyTip) {
@@ -385,7 +282,7 @@ class MainActivity : EhActivity() {
if (connectivityManager.isActiveNetworkMetered) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Snackbar.make(
findViewById(R.id.fragment_container),
binding.drawView,
R.string.metered_network_warning,
Snackbar.LENGTH_LONG,
)
@@ -401,19 +298,25 @@ class MainActivity : EhActivity() {
}
}
private val loginLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
setNavGraph()
}
override fun onResume() {
if (Settings.needSignIn) {
startActivity(Intent(this, ConfigureActivity::class.java))
if (EhUtils.needSignedIn()) {
loginLauncher.launch(Intent(this, ConfigureActivity::class.java))
}
super.onResume()
lifecycleScope.launch {
delay(300)
mDrawerOnBackPressedCallback.remove()
onBackPressedDispatcher.addCallback(mDrawerOnBackPressedCallback)
checkClipboardUrl()
}
}
private suspend fun checkClipboardUrl() {
isInitializedFlow.first { it }
val text = clipboardManager.getUrlFromClipboard(this)
val hashCode = text?.hashCode() ?: 0
if (text != null && hashCode != 0 && Settings.clipboardTextHashCode != hashCode) {
@@ -438,7 +341,7 @@ class MainActivity : EhActivity() {
launch?.let {
withUIContext {
val snackbar = Snackbar.make(
findViewById(R.id.fragment_container),
binding.drawView,
R.string.clipboard_gallery_url_snack_message,
Snackbar.LENGTH_SHORT,
)
@@ -452,8 +355,42 @@ class MainActivity : EhActivity() {
Settings.clipboardTextHashCode = hashCode
}
fun openDrawer() {
openDrawerFlow.tryEmit(Unit)
fun addAboveSnackView(view: View) {
binding.absnacker.addAboveSnackView(view)
}
fun removeAboveSnackView(view: View) {
binding.absnacker.removeAboveSnackView(view)
}
fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {
binding.drawView.setDrawerLockMode(lockMode, edgeGravity)
}
fun getDrawerLockMode(edgeGravity: Int): Int {
return binding.drawView.getDrawerLockMode(edgeGravity)
}
fun isDrawerOpen(drawerGravity: Int) = binding.drawView.isDrawerOpen(drawerGravity)
fun openDrawer(drawerGravity: Int) {
binding.drawView.openDrawer(drawerGravity)
}
fun closeDrawer(drawerGravity: Int) {
binding.drawView.closeDrawer(drawerGravity)
}
fun toggleDrawer(drawerGravity: Int) {
if (isDrawerOpen(drawerGravity)) {
closeDrawer(drawerGravity)
} else {
openDrawer(drawerGravity)
}
}
fun clearNavCheckedItem() {
binding.navView.setCheckedItem(R.id.nav_stub)
}
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
@@ -464,19 +401,17 @@ class MainActivity : EhActivity() {
* If activity is running, show snack bar, otherwise show toast
*/
fun showTip(message: CharSequence, length: Int, useToast: Boolean = false) {
if (!useToast) {
findViewById<View>(R.id.snackbar)?.takeUnless { useToast }?.apply {
Snackbar.make(
findViewById(R.id.fragment_container),
this,
message,
if (length == BaseScene.LENGTH_LONG) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT,
).show()
} else {
Toast.makeText(
this,
message,
if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT,
).show()
}
} ?: Toast.makeText(
this,
message,
if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT,
).show()
}
var mShareUrl: String? = null
@@ -484,4 +419,20 @@ class MainActivity : EhActivity() {
super.onProvideAssistContent(outContent)
mShareUrl?.let { outContent?.webUri = Uri.parse(mShareUrl) }
}
private val mDrawerOnBackPressedCallback =
object : OnBackPressedCallback(false), DrawerLayout.DrawerListener {
val slideThreshold = 0.05
override fun handleOnBackPressed() {
binding.drawView.closeDrawers()
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
isEnabled = slideOffset > slideThreshold
}
override fun onDrawerOpened(drawerView: View) {}
override fun onDrawerClosed(drawerView: View) {}
override fun onDrawerStateChanged(newState: Int) {}
}
}
@@ -3,31 +3,9 @@ package com.hippo.ehviewer.ui
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LocalDialogState
// To make sure compose theme and legacy view theme are in sync, we only use Mdc3Theme
fun ComposeView.setMD3Content(content: @Composable () -> Unit) = setContent {
Mdc3Theme {
val dialogState = remember { DialogState() }
dialogState.Intercept()
CompositionLocalProvider(LocalDialogState provides dialogState) {
content()
}
}
}
fun ComponentActivity.setMD3Content(content: @Composable () -> Unit) = setContent {
Mdc3Theme {
val dialogState = remember { DialogState() }
dialogState.Intercept()
CompositionLocalProvider(LocalDialogState provides dialogState) {
content()
}
}
}
fun ComposeView.setMD3Content(content: @Composable () -> Unit) = setContent { Mdc3Theme(content = content) }
fun ComponentActivity.setMD3Content(content: @Composable () -> Unit) = setContent { Mdc3Theme(content = content) }
@@ -0,0 +1,217 @@
package com.hippo.ehviewer.ui.legacy
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout.AttachedBehavior
import androidx.core.util.ObjectsCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.drakeet.drawer.FullDraggableHelper
import com.google.android.material.snackbar.Snackbar.SnackbarLayout
import com.hippo.ehviewer.util.dp2px
class AboveSnackerLayout : FrameLayout, AttachedBehavior, FullDraggableHelper.Callback {
private val helper: FullDraggableHelper
private var drawerLayout: DrawerLayout? = null
private var mAboveSnackViewList: MutableList<View>? = null
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {
helper = FullDraggableHelper(context, this)
}
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) :
super(context!!, attrs, defStyle) {
helper = FullDraggableHelper(context, this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
ensureDrawerLayout()
}
private fun ensureDrawerLayout() {
val parent = parent.parent
check(parent is DrawerLayout) { "This $this must be added to a DrawerLayout" }
drawerLayout = parent
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return helper.onInterceptTouchEvent(event)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
return helper.onTouchEvent(event)
}
override fun getDrawerMainContainer(): View {
return this
}
override fun isDrawerOpen(gravity: Int): Boolean {
return drawerLayout!!.isDrawerOpen(gravity)
}
override fun hasEnabledDrawer(gravity: Int): Boolean {
return drawerLayout!!.getDrawerLockMode(gravity) == DrawerLayout.LOCK_MODE_UNLOCKED &&
findDrawerWithGravity(gravity) != null
}
override fun offsetDrawer(gravity: Int, offset: Float) {
setDrawerToOffset(gravity, offset)
drawerLayout!!.invalidate()
}
override fun smoothOpenDrawer(gravity: Int) {
drawerLayout!!.openDrawer(gravity, true)
}
override fun smoothCloseDrawer(gravity: Int) {
drawerLayout!!.closeDrawer(gravity, true)
}
override fun onDrawerDragging() {
val drawerListeners = drawerListeners
if (drawerListeners != null) {
val listenerCount = drawerListeners.size
for (i in listenerCount - 1 downTo 0) {
drawerListeners[i].onDrawerStateChanged(DrawerLayout.STATE_DRAGGING)
}
}
}
private val drawerListeners: List<DrawerListener>?
get() = try {
val listeners = DrawerLayout::class.java.getDeclaredField("mListeners")
listeners.isAccessible = true
listeners[drawerLayout] as List<DrawerListener>
} catch (e: Exception) {
// throw to let developer know the api is changed
throw RuntimeException(e)
}
private fun setDrawerToOffset(gravity: Int, offset: Float) {
val drawerView = findDrawerWithGravity(gravity)
val slideOffsetPercent =
(offset / ObjectsCompat.requireNonNull(drawerView).width).coerceIn(0f, 1f)
try {
val method = DrawerLayout::class.java.getDeclaredMethod(
"moveDrawerToOffset",
View::class.java,
Float::class.javaPrimitiveType,
)
method.isAccessible = true
method.invoke(drawerLayout, drawerView, slideOffsetPercent)
drawerView!!.visibility = VISIBLE
} catch (e: Exception) {
// throw to let developer know the api is changed
throw RuntimeException(e)
}
}
// Copied from DrawerLayout
private fun findDrawerWithGravity(gravity: Int): View? {
val absHorizontalGravity = GravityCompat.getAbsoluteGravity(
gravity,
ViewCompat.getLayoutDirection(drawerLayout!!),
) and Gravity.HORIZONTAL_GRAVITY_MASK
val childCount = drawerLayout!!.childCount
for (i in 0 until childCount) {
val child = drawerLayout!!.getChildAt(i)
val childAbsGravity = getDrawerViewAbsoluteGravity(child)
if (childAbsGravity and Gravity.HORIZONTAL_GRAVITY_MASK == absHorizontalGravity) {
return child
}
}
return null
}
// Copied from DrawerLayout
private fun getDrawerViewAbsoluteGravity(drawerView: View): Int {
val gravity = (drawerView.layoutParams as DrawerLayout.LayoutParams).gravity
return GravityCompat.getAbsoluteGravity(
gravity,
ViewCompat.getLayoutDirection(drawerLayout!!),
)
}
fun addAboveSnackView(view: View) {
if (null == mAboveSnackViewList) {
mAboveSnackViewList = ArrayList()
}
mAboveSnackViewList!!.add(view)
}
fun removeAboveSnackView(view: View) {
if (null == mAboveSnackViewList) {
return
}
mAboveSnackViewList!!.remove(view)
}
val aboveSnackViewCount: Int
get() = if (null == mAboveSnackViewList) 0 else mAboveSnackViewList!!.size
fun getAboveSnackViewAt(index: Int): View? {
return if (null == mAboveSnackViewList || index < 0 || index >= mAboveSnackViewList!!.size) {
null
} else {
mAboveSnackViewList!![index]
}
}
override fun getBehavior(): Behavior {
return Behavior()
}
class Behavior : CoordinatorLayout.Behavior<AboveSnackerLayout>() {
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: AboveSnackerLayout,
dependency: View,
): Boolean {
return dependency is SnackbarLayout
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: AboveSnackerLayout,
dependency: View,
): Boolean {
for (i in 0 until child.aboveSnackViewCount) {
val view = child.getAboveSnackViewAt(i)
if (view != null) {
val translationY =
(dependency.translationY - dependency.height - dp2px(view.context, 8f))
.coerceAtMost(0f)
ViewCompat.animate(view).setInterpolator(FastOutSlowInInterpolator())
.translationY(translationY).setDuration(150).start()
}
}
return false
}
override fun onDependentViewRemoved(
parent: CoordinatorLayout,
child: AboveSnackerLayout,
dependency: View,
) {
for (i in 0 until child.aboveSnackViewCount) {
val view = child.getAboveSnackViewAt(i)
if (view != null) {
ViewCompat.animate(view).setInterpolator(FastOutSlowInInterpolator())
.translationY(0f).setDuration(75).start()
}
}
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
class DrawerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : FrameLayout(context, attrs) {
private var mMaxWidth = 0
init {
val a = context.obtainStyledAttributes(attrs, SIZE_ATTRS)
mMaxWidth = a.getDimensionPixelOffset(0, 0)
a.recycle()
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
var widthSpec = widthSpec
when (MeasureSpec.getMode(widthSpec)) {
MeasureSpec.EXACTLY -> {}
MeasureSpec.AT_MOST -> widthSpec = MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(widthSpec).coerceAtMost(mMaxWidth),
MeasureSpec.EXACTLY,
)
MeasureSpec.UNSPECIFIED ->
widthSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY)
}
// Let super sort out the height
super.onMeasure(widthSpec, heightSpec)
}
companion object {
private val SIZE_ATTRS = intArrayOf(android.R.attr.maxWidth)
}
}
@@ -16,11 +16,9 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -47,7 +45,6 @@ import com.hippo.ehviewer.ui.main.ImageSearch
import com.hippo.ehviewer.ui.main.NormalSearch
import com.hippo.ehviewer.ui.main.SearchAdvanced
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.util.pickVisualMedia
import kotlinx.coroutines.launch
@@ -74,7 +71,6 @@ class SearchLayout @JvmOverloads constructor(
@Composable
override fun Content() {
val coroutineScope = rememberCoroutineScope()
val windowSizeClass = calculateWindowSizeClass(activity = context.findActivity())
fun selectImage() = coroutineScope.launch {
context.pickVisualMedia(ActivityResultContracts.PickVisualMedia.ImageOnly)?.let {
vm.path = it.toString()
@@ -97,11 +93,6 @@ class SearchLayout @JvmOverloads constructor(
isAdvanced = vm.isAdvancedMode,
onAdvancedChanged = { vm.isAdvancedMode = it },
showInfo = { BaseDialogBuilder(context).setMessage(R.string.search_tip).show() },
maxItemsInEachRow = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> 2
WindowWidthSizeClass.Medium -> 3
else -> 5
},
)
}
}
@@ -132,7 +123,7 @@ class SearchLayout @JvmOverloads constructor(
}
}
}
TabRow(
SecondaryTabRow(
selectedTabIndex = if (vm.isNormalMode) 0 else 1,
divider = {},
) {
@@ -59,7 +59,7 @@ import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.FINISH_ROUTE_NAME
import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.SELECT_SITE_ROUTE_NAME
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.rememberDialogState
import com.hippo.ehviewer.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
@@ -87,7 +87,8 @@ fun CookieSignInScene(windowSizeClass: WindowSizeClass) {
var signInJob by remember { mutableStateOf<Job?>(null) }
val dialogState = LocalDialogState.current
val dialogState = rememberDialogState()
dialogState.Intercept()
val noCookies = stringResource(R.string.from_clipboard_error)
@@ -68,7 +68,7 @@ import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.SELECT_SITE_ROUTE_NAME
import com.hippo.ehviewer.ui.WEBVIEW_SIGN_IN_ROUTE_NAME
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.rememberDialogState
import com.hippo.ehviewer.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
@@ -88,7 +88,9 @@ fun SignInScreen(windowSizeClass: WindowSizeClass) {
var passwordHidden by rememberSaveable { mutableStateOf(true) }
val context = LocalContext.current
var signInJob by remember { mutableStateOf<Job?>(null) }
val dialogState = LocalDialogState.current
val dialogState = rememberDialogState()
dialogState.Intercept()
BackHandler {
(context as Activity).moveTaskToBack(true)
@@ -26,6 +26,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -34,8 +35,6 @@ import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.SadAndroid
@Composable
fun GalleryDetailHeaderInfoCard(
@@ -161,7 +160,7 @@ fun GalleryDetailErrorTip(error: String, onClick: () -> Unit) = Column(
verticalArrangement = Arrangement.Center,
) {
Icon(
imageVector = EhIcons.Big.Default.SadAndroid,
painter = painterResource(id = R.drawable.big_sad_pandroid),
contentDescription = null,
modifier = Modifier.clickable(onClick = onClick),
)
@@ -67,8 +67,11 @@ fun GalleryInfoListItem(
modifier = Modifier.aspectRatio(DEFAULT_ASPECT).fillMaxSize(),
)
}
val showFav by FavouriteStatusRouter.collectAsState(info) {
it != NOT_FAVORITED && !isInFavScene
val showFav by produceState(false, info.gid) {
value = info.favoriteSlot != NOT_FAVORITED && !isInFavScene
FavouriteStatusRouter.stateFlow(info.gid).collect {
value = it != NOT_FAVORITED && !isInFavScene
}
}
ConstraintLayout(modifier = Modifier.padding(8.dp, 4.dp).fillMaxSize()) {
val (titleRef, uploaderRef, ratingRef, categoryRef, postedRef, favRef, iconsRef) = createRefs()
@@ -1,13 +1,11 @@
package com.hippo.ehviewer.ui.main
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
@@ -57,24 +55,16 @@ fun NormalSearch(
isAdvanced: Boolean,
onAdvancedChanged: (Boolean) -> Unit,
showInfo: () -> Unit,
maxItemsInEachRow: Int,
) {
FlowRow(
modifier = Modifier.wrapContentHeight(align = Alignment.Top),
maxItemsInEachRow = maxItemsInEachRow,
) {
FlowRow(modifier = Modifier.wrapContentHeight(align = Alignment.Top)) {
categoryTable.forEach {
val selected = category and it.first != 0
FilterChip(
selected = selected,
onClick = { onCategoryChanged(if (selected) category xor it.first else category or it.first) },
label = { Text(text = stringResource(id = it.second)) },
leadingIcon = {
AnimatedVisibility(selected) {
Icon(imageVector = Icons.Default.Check, contentDescription = null)
}
},
modifier = Modifier.padding(horizontal = 4.dp).align(alignment = Alignment.CenterVertically).widthIn(min = 145.dp),
leadingIcon = { if (selected) Icon(imageVector = Icons.Default.Check, contentDescription = null) },
modifier = Modifier.padding(horizontal = 4.dp).align(alignment = Alignment.CenterVertically),
)
}
}
@@ -22,7 +22,9 @@ import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.StringRes
import androidx.core.view.GravityCompat
import androidx.core.view.SoftwareKeyboardControllerCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
@@ -39,20 +41,60 @@ abstract class BaseScene : Fragment() {
private var drawerView: View? = null
private var drawerViewState: SparseArray<Parcelable>? = null
private var sideSheetDialog: SideSheetDialog? = null
val isDrawerLocked
get() = mainActivity?.drawerLocked == true
fun lockDrawer() {
mainActivity?.drawerLocked = true
fun addAboveSnackView(view: View) {
val activity = activity
if (activity is MainActivity) {
activity.addAboveSnackView(view)
}
}
fun unlockDrawer() {
mainActivity?.drawerLocked = false
fun removeAboveSnackView(view: View) {
val activity = activity
if (activity is MainActivity) {
activity.removeAboveSnackView(view)
}
}
fun openDrawer() {
mainActivity?.openDrawer()
fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.setDrawerLockMode(lockMode, edgeGravity)
}
}
fun getDrawerLockMode(edgeGravity: Int): Int? {
val activity = activity
return if (activity is MainActivity) {
activity.getDrawerLockMode(edgeGravity)
} else {
null
}
}
fun isDrawerOpen(drawerGravity: Int): Boolean {
val activity = activity
return activity is MainActivity && activity.isDrawerOpen(drawerGravity)
}
fun openDrawer(drawerGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.openDrawer(drawerGravity)
}
}
fun closeDrawer(drawerGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.closeDrawer(drawerGravity)
}
}
fun toggleDrawer(drawerGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.toggleDrawer(drawerGravity)
}
}
fun openSideSheet() {
@@ -65,14 +107,20 @@ abstract class BaseScene : Fragment() {
fun closeSideSheet() = sideSheetDialog!!.hide()
fun showTip(message: CharSequence?, length: Int) {
mainActivity?.showTip(message!!, length)
val activity = activity
if (activity is MainActivity) {
activity.showTip(message!!, length)
}
}
fun showTip(@StringRes id: Int, length: Int) {
mainActivity?.showTip(id, length)
val activity = activity
if (activity is MainActivity) {
activity.showTip(id, length)
}
}
open val enableDrawerGestures = false
open val showLeftDrawer = false
private fun createDrawerView(savedInstanceState: Bundle?): View? {
drawerView = onCreateDrawerView(layoutInflater)
@@ -108,10 +156,10 @@ abstract class BaseScene : Fragment() {
view.background = requireActivity().theme.resolveDrawable(android.R.attr.windowBackground)
// Update left drawer locked state
if (enableDrawerGestures) {
unlockDrawer()
if (showLeftDrawer) {
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
} else {
lockDrawer()
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
}
hideSoftInput()
createDrawerView(savedInstanceState)?.let {
@@ -128,7 +176,6 @@ abstract class BaseScene : Fragment() {
setContentView(it)
}
}
mainActivity?.recompose()
}
override fun onDestroyView() {
@@ -137,7 +184,14 @@ abstract class BaseScene : Fragment() {
}
val mainActivity: MainActivity?
get() = activity as? MainActivity
get() {
val activity = activity
return if (activity is MainActivity) {
activity
} else {
null
}
}
fun hideSoftInput() = activity?.window?.decorView?.run { SoftwareKeyboardControllerCompat(this) }?.hide()
@@ -22,6 +22,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.hippo.ehviewer.R
import com.hippo.ehviewer.databinding.SceneToolbarBinding
abstract class BaseToolbarScene : BaseScene(), ToolBarScene {
@@ -60,7 +61,12 @@ abstract class BaseToolbarScene : BaseScene(), ToolBarScene {
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return false
return if (item.itemId == R.id.action_open_side_sheet) {
openSideSheet()
true
} else {
false
}
}
override fun onNavigationClick() {
@@ -53,6 +53,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
@@ -143,9 +145,6 @@ class DownloadsScene :
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
override val fastScroller get() = binding.fastScroller
override val recyclerView get() = binding.recyclerView
override val contentView get() = binding.content
private fun initLabels() {
context ?: return
@@ -268,10 +267,12 @@ class DownloadsScene :
).apply {
addCustomChoiceListener({
binding.fabLayout.isExpanded = true
lockDrawer()
// Lock drawer
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
}) {
binding.fabLayout.isExpanded = false
unlockDrawer()
// Unlock drawer
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
}
restoreSelection(savedInstanceState)
}
@@ -345,6 +346,7 @@ class DownloadsScene :
fabLayout.setHidePrimaryFab(true)
fabLayout.setAutoCancel(false)
fabLayout.setOnClickFabListener(this@DownloadsScene)
addAboveSnackView(fabLayout)
updateForLabel()
}
viewLifecycleOwner.lifecycleScope.launch {
@@ -367,6 +369,7 @@ class DownloadsScene :
override fun onDestroyView() {
super.onDestroyView()
binding.recyclerView.stopScroll()
removeAboveSnackView(binding.fabLayout)
mViewTransition = null
mAdapter = null
mLabelAdapter = null
@@ -375,7 +378,7 @@ class DownloadsScene :
}
override fun onNavigationClick() {
openDrawer()
toggleDrawer(GravityCompat.START)
}
override fun getMenuResId(): Int {
@@ -33,11 +33,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
@@ -85,7 +88,6 @@ import com.hippo.ehviewer.util.lazyMut
import com.hippo.ehviewer.util.setValue
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.dpToPx
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
@@ -93,7 +95,6 @@ import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
@@ -102,6 +103,7 @@ import rikka.core.res.resolveColor
// Note that we do not really follow mvvm structure, just use it as ... storage
class VMStorage : ViewModel() {
var urlBuilder = FavListUrlBuilder(favCat = Settings.recentFavCat)
var shouldScrollToTop = false
private val cloudDataFlow = Pager(PagingConfig(25)) {
object : PagingSource<String, BaseGalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
@@ -118,6 +120,7 @@ class VMStorage : ViewModel() {
} else {
urlBuilder.setIndex(key, false)
}
shouldScrollToTop = true
}
}
val r = runSuspendCatching {
@@ -153,14 +156,19 @@ class FavoritesScene : SearchBarScene() {
private val binding get() = _binding!!
private var mAdapter: GalleryAdapter? = null
private val tracker get() = mAdapter!!.tracker!!
private var showNormalFabsJob: Job? = null
private val showNormalFabsRunnable = Runnable {
updateJumpFab() // index: 0, 2
binding.fabLayout.run {
setSecondaryFabVisibilityAt(1, true)
for (i in 3..6) {
setSecondaryFabVisibilityAt(i, false)
}
}
}
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
override val fastScroller get() = binding.fastScroller
override val recyclerView get() = binding.recyclerView
override val contentView get() = binding.contentLayout.contentView
private fun onItemClick(position: Int) {
// Skip if in search mode
@@ -225,7 +233,7 @@ class FavoritesScene : SearchBarScene() {
savedInstanceState: Bundle?,
): View {
_binding = SceneFavoritesBinding.inflate(inflater, container!!)
container.addView(ComposeWithViewLifecycle().apply { setMD3Content { dialogState.Intercept() } })
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
setOnApplySearch {
if (!tracker.isInCustomChoice) {
switchFav(urlBuilder.favCat, it)
@@ -301,6 +309,7 @@ class FavoritesScene : SearchBarScene() {
if (tracker.isInCustomChoice) tracker.clearSelection()
}
}
addAboveSnackView(this)
}
binding.fastScroller.setOnDragHandlerListener(object : OnDragHandlerListener {
override fun onStartDragHandler() {}
@@ -327,7 +336,6 @@ class FavoritesScene : SearchBarScene() {
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
binding.tip.setCompoundDrawables(null, drawable, null, null)
binding.tip.setOnClickListener { mAdapter?.retry() }
binding.refreshLayout.setProgressViewOffset(true, 0, 64.dpToPx)
binding.refreshLayout.setOnRefreshListener { switchFav(urlBuilder.favCat) }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
@@ -337,7 +345,10 @@ class FavoritesScene : SearchBarScene() {
is LoadState.Loading -> {
showSearchBar()
if (!binding.refreshLayout.isRefreshing) {
transition.showView(1)
// https://github.com/FooIbar/EhViewer/issues/45
// transition.showView(1)
transition.showView(0, false)
binding.refreshLayout.isRefreshing = true
}
}
is LoadState.Error -> {
@@ -351,7 +362,11 @@ class FavoritesScene : SearchBarScene() {
binding.tip.text = empty
transition.showView(2)
} else {
transition.showView(0)
transition.showView(0, false)
if (vm.shouldScrollToTop) {
vm.shouldScrollToTop = false
binding.recyclerView.scrollToPosition(0)
}
}
}
}
@@ -369,13 +384,15 @@ class FavoritesScene : SearchBarScene() {
// Delay expanding action to make layout work fine
SimpleHandler.post { binding.fabLayout.isExpanded = true }
binding.refreshLayout.isEnabled = false
lockDrawer()
// Lock drawer
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
}) {
showNormalFab()
binding.fabLayout.setAutoCancel(true)
binding.fabLayout.isExpanded = false
binding.refreshLayout.isEnabled = true
unlockDrawer()
// Unlock drawer
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
}
restoreSelection(savedInstanceState)
}
@@ -434,12 +451,12 @@ class FavoritesScene : SearchBarScene() {
override fun onDestroyView() {
super.onDestroyView()
binding.recyclerView.stopScroll()
removeAboveSnackView(binding.fabLayout)
mAdapter = null
_binding = null
showNormalFabsJob = null
}
override fun onCreateDrawerView(inflater: LayoutInflater) = ComposeWithViewLifecycle().apply {
override fun onCreateDrawerView(inflater: LayoutInflater) = ComposeView(inflater.context).apply {
setMD3Content {
val localFavCount by vm.localFavCount.collectAsState(0)
ElevatedCard {
@@ -515,20 +532,12 @@ class FavoritesScene : SearchBarScene() {
private fun showNormalFab() {
// Delay showing normal fab to avoid mutation
showNormalFabsJob = viewLifecycleOwner.lifecycleScope.launch {
delay(300)
updateJumpFab() // index: 0, 2
binding.fabLayout.run {
setSecondaryFabVisibilityAt(1, true)
for (i in 3..6) {
setSecondaryFabVisibilityAt(i, false)
}
}
}
SimpleHandler.removeCallbacks(showNormalFabsRunnable)
SimpleHandler.postDelayed(showNormalFabsRunnable, 300)
}
private fun showSelectionFab() {
showNormalFabsJob?.cancel()
SimpleHandler.removeCallbacks(showNormalFabsRunnable)
binding.fabLayout.run {
for (i in 0..2) {
setSecondaryFabVisibilityAt(i, false)
@@ -87,10 +87,8 @@ import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.hippo.ehviewer.util.TextUrl
import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.applyNavigationBarsPadding
import com.hippo.ehviewer.util.getParcelableCompat
import com.hippo.ehviewer.util.toBBCode
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlin.math.hypot
@@ -258,19 +256,10 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
}
}
binding.fab.setOnClickListener(this)
addAboveSnackView(binding.editPanel)
addAboveSnackView(binding.fabLayout)
mViewTransition = ViewTransition(binding.refreshLayout, tip)
updateView(false)
binding.editPanel.applyInsetter {
type(ime = true, navigationBars = true) {
padding()
}
}
binding.recyclerView.applyInsetter {
type(ime = true, navigationBars = true) {
padding()
}
}
binding.fabLayout.applyNavigationBarsPadding()
return binding.root
}
@@ -293,6 +282,8 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
super.onDestroyView()
callback.remove()
binding.recyclerView.stopScroll()
removeAboveSnackView(binding.editPanel)
removeAboveSnackView(binding.fabLayout)
mAdapter = null
mViewTransition = null
_binding = null
@@ -437,7 +428,7 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
return@setItems
}
when (menuId[which]) {
R.id.copy -> requireActivity().addTextToClipboard(text)
R.id.copy -> requireActivity().addTextToClipboard(text, false)
R.id.block_commenter -> showFilterCommenterDialog(comment.user, position)
R.id.vote_up -> voteComment(comment.id, 1)
R.id.vote_down -> voteComment(comment.id, -1)
@@ -21,8 +21,10 @@ import android.app.Activity
import android.app.Dialog
import android.app.DownloadManager
import android.content.DialogInterface
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.LayoutInflater
@@ -30,6 +32,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
@@ -79,6 +82,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -87,11 +91,13 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.LocalPinnableContainer
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidViewBinding
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.text.parseAsHtml
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
@@ -165,10 +171,7 @@ import com.hippo.ehviewer.util.IntList
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.getParcelableCompat
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.requestPermission
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlin.math.roundToInt
import kotlinx.coroutines.CancellationException
@@ -195,6 +198,20 @@ class GalleryDetailScene : BaseScene() {
private var getDetailError by mutableStateOf("")
private var mTorrentList: TorrentResult? = null
private var requestStoragePermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { result: Boolean ->
if (result && composeBindingGD != null) {
val helper = TorrentListDialogHelper()
val binding = DialogTorrentListBinding.inflate(layoutInflater)
val dialog: Dialog = BaseDialogBuilder(requireActivity())
.setTitle(R.string.torrents)
.setView(binding.root)
.setOnDismissListener(helper)
.show()
helper.setDialog(dialog, binding, composeBindingGD!!.torrentUrl)
}
}
private var mArchiveFormParamOr: String? = null
private var mArchiveList: List<ArchiveParser.Archive>? = null
private var mCurrentFunds: HomeParser.Funds? = null
@@ -399,7 +416,7 @@ class GalleryDetailScene : BaseScene() {
getDetailError = getString(R.string.error_cannot_find_gallery)
}
(requireActivity() as MainActivity).mShareUrl = galleryDetailUrl
return ComposeWithViewLifecycle().apply {
return ComposeView(requireContext()).apply {
setMD3Content {
LaunchedEffect(gid) {
EhDownloadManager.stateFlow(gid).collect {
@@ -709,13 +726,12 @@ class GalleryDetailScene : BaseScene() {
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
}
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
) {
val favored by FavouriteStatusRouter.collectAsState(galleryDetail) {
it != NOT_FAVORITED
val favored by produceState(initialValue = false) {
value = galleryDetail.favoriteSlot != NOT_FAVORITED
FavouriteStatusRouter.stateFlow(galleryDetail.gid).collect { value = it != NOT_FAVORITED }
}
val favButtonText = if (favored) {
galleryDetail.favoriteName ?: stringResource(id = R.string.local_favorites)
@@ -963,7 +979,7 @@ class GalleryDetailScene : BaseScene() {
context ?: return
if (mPage != 0) {
Snackbar.make(
requireActivity().findViewById(R.id.fragment_container),
requireActivity().findViewById(R.id.snackbar),
getString(R.string.read_from, mPage),
Snackbar.LENGTH_LONG,
).setAction(R.string.read) { context?.navToReader(gd.galleryInfo, mPage) }.show()
@@ -998,9 +1014,9 @@ class GalleryDetailScene : BaseScene() {
@SuppressLint("InflateParams")
@Composable
private fun GalleryDetailComment(commentsList: Array<GalleryComment>) {
private fun GalleryDetailComment(commentsList: Array<GalleryComment>?) {
val maxShowCount = 2
val commentText = if (commentsList.isEmpty()) {
val commentText = if (commentsList.isNullOrEmpty()) {
stringResource(R.string.no_comments)
} else if (commentsList.size <= maxShowCount) {
stringResource(R.string.no_more_comments)
@@ -1008,24 +1024,25 @@ class GalleryDetailScene : BaseScene() {
stringResource(R.string.more_comment)
}
CrystalCard {
val length = maxShowCount.coerceAtMost(commentsList.size)
for (i in 0 until length) {
val item = commentsList[i]
AndroidViewBinding(factory = ItemGalleryCommentBinding::inflate) {
card.setOnClickListener { onNavigateToCommentScene() }
user.text = item.user
user.setBackgroundColor(Color.TRANSPARENT)
time.text = ReadableTime.getTimeAgo(item.time)
comment.maxLines = 5
comment.text = item.comment.orEmpty().parseAsHtml(imageGetter = CoilImageGetter(comment))
if (commentsList != null) {
val length = maxShowCount.coerceAtMost(commentsList.size)
for (i in 0 until length) {
val comment = commentsList[i]
AndroidView(factory = { context ->
ItemGalleryCommentBinding.inflate(LayoutInflater.from(context), null, false).apply {
card.setOnClickListener { onNavigateToCommentScene() }
user.text = comment.user
user.setBackgroundColor(Color.TRANSPARENT)
time.text = ReadableTime.getTimeAgo(comment.time)
this.comment.maxLines = 5
this.comment.text = comment.comment.orEmpty().parseAsHtml(imageGetter = CoilImageGetter(this.comment))
}.root
})
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = dimensionResource(id = R.dimen.strip_item_padding_v))
.clip(RoundedCornerShape(16.dp))
.clickable(onClick = ::onNavigateToCommentScene),
modifier = Modifier.fillMaxWidth().padding(bottom = dimensionResource(id = R.dimen.strip_item_padding_v))
.clip(RoundedCornerShape(16.dp)).clickable(onClick = ::onNavigateToCommentScene),
contentAlignment = Alignment.Center,
) {
Text(commentText)
@@ -1096,20 +1113,21 @@ class GalleryDetailScene : BaseScene() {
private fun showTorrentDialog() {
val galleryDetail = composeBindingGD ?: return
viewLifecycleOwner.lifecycleScope.launchUI {
val granted = isAtLeastQ || requireContext().requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (granted) {
val helper = TorrentListDialogHelper()
val binding = DialogTorrentListBinding.inflate(layoutInflater)
val dialog: Dialog = BaseDialogBuilder(requireContext())
.setTitle(R.string.torrents)
.setView(binding.root)
.setOnDismissListener(helper)
.show()
helper.setDialog(dialog, binding, galleryDetail.torrentUrl)
} else {
showTip(getString(R.string.permission_denied), LENGTH_SHORT)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.WRITE_EXTERNAL_STORAGE,
) != PackageManager.PERMISSION_GRANTED
) {
requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
val helper = TorrentListDialogHelper()
val binding = DialogTorrentListBinding.inflate(layoutInflater)
val dialog: Dialog = BaseDialogBuilder(requireContext())
.setTitle(R.string.torrents)
.setView(binding.root)
.setOnDismissListener(helper)
.show()
helper.setDialog(dialog, binding, galleryDetail.torrentUrl)
}
}
@@ -1193,11 +1211,11 @@ class GalleryDetailScene : BaseScene() {
}
R.id.copy -> {
requireActivity().addTextToClipboard(tag)
requireActivity().addTextToClipboard(tag, false)
}
R.id.copy_trans -> {
requireActivity().addTextToClipboard(translated)
requireActivity().addTextToClipboard(translated, false)
}
}
}.show()
@@ -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
@@ -30,11 +29,14 @@ import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.material3.Text
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
@@ -70,6 +72,7 @@ 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,17 +101,13 @@ import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.updater.AppUpdater
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.hippo.ehviewer.util.applyNavigationBarsPadding
import com.hippo.ehviewer.util.getParcelableCompat
import com.hippo.ehviewer.util.getValue
import com.hippo.ehviewer.util.lazyMut
import com.hippo.ehviewer.util.setValue
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.dpToPx
import java.io.File
import java.time.Instant
import java.time.LocalDateTime
@@ -118,7 +117,6 @@ import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import moe.tarsin.coroutines.runSuspendCatching
@@ -126,6 +124,7 @@ import rikka.core.res.resolveColor
class VMStorage1 : ViewModel() {
var urlBuilder = ListUrlBuilder()
var shouldScrollToTop = false
val dataFlow = Pager(PagingConfig(25)) {
object : PagingSource<String, BaseGalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
@@ -156,6 +155,7 @@ class VMStorage1 : ViewModel() {
} else {
urlBuilder.setIndex(key, false)
}
shouldScrollToTop = true
}
}
val r = runSuspendCatching {
@@ -201,16 +201,8 @@ class GalleryListScene : SearchBarScene() {
lateinit var mQuickSearchList: MutableList<QuickSearch>
private var mHideActionFabSlop = 0
private var mState = State.NORMAL
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
binding.searchFab.visibility = View.INVISIBLE
}
}
override val fabLayout get() = binding.fabLayout
override val fastScroller get() = binding.fastScroller
override val recyclerView get() = binding.recyclerView
override val contentView get() = binding.contentLayout.contentView
override fun getMenuResId(): Int {
return R.menu.scene_gallery_list_searchbar_menu
@@ -222,7 +214,7 @@ class GalleryListScene : SearchBarScene() {
ACTION_HOMEPAGE -> ListUrlBuilder()
ACTION_SUBSCRIPTION -> ListUrlBuilder(MODE_SUBSCRIPTION)
ACTION_WHATS_HOT -> ListUrlBuilder(MODE_WHATS_HOT)
ACTION_TOP_LIST -> ListUrlBuilder(MODE_TOPLIST, mKeyword = Settings.recentToplist)
ACTION_TOP_LIST -> ListUrlBuilder(MODE_TOPLIST, mKeyword = "11")
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!")
}
@@ -294,6 +286,14 @@ class GalleryListScene : SearchBarScene() {
title = resources.getString(R.string.search)
}
setSearchBarHint(title)
when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> if (category != EhUtils.NONE || !keyword.isNullOrEmpty()) {
mainActivity?.clearNavCheckedItem()
}
MODE_TAG, MODE_UPLOADER, MODE_IMAGE_SEARCH -> mainActivity?.clearNavCheckedItem()
else -> Unit
}
}
private val dialogState = DialogState()
@@ -321,7 +321,7 @@ class GalleryListScene : SearchBarScene() {
savedInstanceState: Bundle?,
): View {
_binding = SceneGalleryListBinding.inflate(inflater, container!!)
container.addView(ComposeWithViewLifecycle().apply { setMD3Content { dialogState.Intercept() } })
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
checkForUpdates()
requireActivity().onBackPressedDispatcher.addCallback(stateBackPressedCallback)
mHideActionFabSlop = ViewConfiguration.get(requireContext()).scaledTouchSlop
@@ -331,7 +331,7 @@ class GalleryListScene : SearchBarScene() {
WindowInsetsAnimationHelper(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
binding.fabLayout,
binding.searchFab.parent as View,
mSearchFab.parent as View,
),
)
mViewTransition = BringOutTransition(binding.contentLayout.contentView, binding.searchLayout)
@@ -366,7 +366,6 @@ class GalleryListScene : SearchBarScene() {
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
binding.tip.setCompoundDrawables(null, drawable, null, null)
binding.tip.setOnClickListener { mAdapter?.retry() }
binding.refreshLayout.setProgressViewOffset(true, 0, 64.dpToPx)
binding.refreshLayout.setOnRefreshListener {
mUrlBuilder.setIndex(null, true)
mUrlBuilder.mJumpTo = null
@@ -380,10 +379,12 @@ class GalleryListScene : SearchBarScene() {
is LoadState.Loading -> {
showSearchBar()
if (!binding.refreshLayout.isRefreshing) {
transition.showView(1)
// https://github.com/FooIbar/EhViewer/issues/45
// transition.showView(1)
transition.showView(0, false)
binding.refreshLayout.isRefreshing = true
}
}
is LoadState.Error -> {
binding.refreshLayout.isRefreshing = false
binding.tip.text = ExceptionUtils.getReadableString(state.error)
@@ -399,20 +400,22 @@ class GalleryListScene : SearchBarScene() {
navAnimated(R.id.webView, bundleOf(WebViewActivity.KEY_URL to EhUrl.host))
}
}
is LoadState.NotLoading -> {
binding.refreshLayout.isRefreshing = false
if (mAdapter?.itemCount == 0) {
binding.tip.text = empty
transition.showView(2)
} else {
transition.showView(0)
transition.showView(0, false)
if (vm.shouldScrollToTop) {
vm.shouldScrollToTop = false
binding.recyclerView.scrollToPosition(0)
}
}
}
}
}
}
Settings.needSignInFlow.first { !it }
vm.dataFlow.collectLatest {
adapter.submitData(it)
}
@@ -460,7 +463,6 @@ class GalleryListScene : SearchBarScene() {
mUrlBuilder.mJumpTo = null
mAdapter?.refresh()
}
2 -> { // Last
if (mIsTopList) {
mUrlBuilder.mJumpTo = "${TOPLIST_PAGES - 1}"
@@ -478,24 +480,15 @@ class GalleryListScene : SearchBarScene() {
val actionFabDrawable = AddDeleteDrawable(requireContext(), colorID)
binding.fabLayout.addOnExpandListener {
if (it) {
lockDrawer()
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
actionFabDrawable.setDelete(ANIMATE_TIME)
} else {
unlockDrawer()
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
actionFabDrawable.setAdd(ANIMATE_TIME)
}
}
binding.fabLayout.primaryFab!!.setImageDrawable(actionFabDrawable)
binding.searchFab.setOnClickListener { onApplySearch() }
(binding.searchFab.parent as View).apply {
bringToFront()
applyNavigationBarsPadding()
}
binding.searchLayout.applyInsetter {
type(statusBars = true) {
padding()
}
}
addAboveSnackView(binding.fabLayout)
// Update list url builder
onUpdateUrlBuilder()
@@ -511,6 +504,7 @@ class GalleryListScene : SearchBarScene() {
super.onDestroyView()
stateBackPressedCallback.remove()
binding.recyclerView.stopScroll()
removeAboveSnackView(binding.fabLayout)
_binding = null
fabAnimator = null
mAdapter = null
@@ -711,47 +705,6 @@ class GalleryListScene : SearchBarScene() {
}
}
private fun showSearchFab(animation: Boolean, delay: Long = 0) {
if (animation) {
binding.searchFab.visibility = View.VISIBLE
binding.searchFab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
binding.searchFab.visibility = View.VISIBLE
binding.searchFab.scaleX = 1.0f
binding.searchFab.scaleY = 1.0f
}
}
private fun hideSearchFab(animation: Boolean): Long {
return if (View.INVISIBLE == binding.searchFab.visibility) {
0L
} else {
if (animation) {
binding.searchFab.animate().scaleX(0.0f).scaleY(0.0f)
.setListener(mSearchFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
} else {
binding.searchFab.visibility = View.INVISIBLE
binding.searchFab.scaleX = 0.0f
binding.searchFab.scaleY = 0.0f
}
ANIMATE_TIME
}
}
private fun selectSearchFab(animation: Boolean) {
val delay = hideActionFab(animation)
showSearchFab(animation, delay)
}
private fun selectActionFab(animation: Boolean) {
val delay = hideSearchFab(animation)
showActionFab(animation, delay)
}
override fun onSearchViewExpanded() {
if (mState == State.NORMAL) {
hideActionFab(true)
@@ -901,9 +854,9 @@ class GalleryListScene : SearchBarScene() {
private fun onStateChange(newState: State) {
stateBackPressedCallback.isEnabled = newState != State.NORMAL
if (newState == State.NORMAL || newState == State.SIMPLE_SEARCH) {
unlockDrawer()
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
} else {
lockDrawer()
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
}
}
@@ -911,8 +864,7 @@ class GalleryListScene : SearchBarScene() {
private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) :
RecyclerView.Adapter<QsDrawerHolder>() {
private val toplists = resources.getStringArray(R.array.toplist_entries)
private val keywords = resources.getStringArray(R.array.toplist_values)
private val keywords = intArrayOf(11, 12, 13, 15)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {
val holder = QsDrawerHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
@@ -922,9 +874,7 @@ class GalleryListScene : SearchBarScene() {
navAnimated(R.id.galleryListScene, ListUrlBuilder().apply { set(q) }.toStartArgs())
} else {
if (mIsTopList) {
val keyword = keywords[holder.bindingAdapterPosition]
Settings.recentToplist = keyword
mUrlBuilder.keyword = keyword
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
mUrlBuilder.mJumpTo = null
} else {
val q = mQuickSearchList[holder.bindingAdapterPosition]
@@ -944,7 +894,13 @@ class GalleryListScene : SearchBarScene() {
if (!mIsTopList) {
text.text = mQuickSearchList[position].name
} else {
text.text = toplists[position]
val toplists = intArrayOf(
R.string.toplist_alltime,
R.string.toplist_pastyear,
R.string.toplist_pastmonth,
R.string.toplist_yesterday,
)
text.text = getString(toplists[position])
option.visibility = View.GONE
}
}
@@ -2,7 +2,6 @@ package com.hippo.ehviewer.ui.scene
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.padding
@@ -54,14 +53,13 @@ import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.legacy.calculateSuitableSpanCount
import com.hippo.ehviewer.ui.main.EhPreviewItem
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid
import com.hippo.ehviewer.ui.tools.rememberDialogState
import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.util.getParcelableCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -77,6 +75,8 @@ class GalleryPreviewScreen : Fragment() {
val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior()
val columnCount = calculateSuitableSpanCount()
val state = rememberLazyGridState()
val dialogState = rememberDialogState()
dialogState.Intercept()
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val pages = galleryDetail.pages
val pgSize = galleryDetail.previewList.size
@@ -158,11 +158,6 @@ class GalleryPreviewScreen : Fragment() {
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireContext().findActivity<MainActivity>().recompose()
}
}
const val KEY_GALLERY_DETAIL = "gallery_detail"
@@ -44,10 +44,13 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat
import androidx.lifecycle.lifecycleScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
@@ -58,27 +61,24 @@ import androidx.paging.compose.itemKey
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings.listThumbSize
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.History
import com.hippo.ehviewer.ui.doGalleryInfoAction
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.Deferred
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider
import com.hippo.ehviewer.ui.tools.rememberDialogState
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.pxToDp
import kotlinx.coroutines.delay
class HistoryScene : BaseScene() {
// Disabled for breaking swipe-to-dismiss
override val enableDrawerGestures = false
override val showLeftDrawer = true
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = ComposeWithViewLifecycle().apply {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = ComposeView(inflater.context).apply {
setMD3Content {
val dialogState = LocalDialogState.current
val dialogState = rememberDialogState()
dialogState.Intercept()
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val historyData = remember { Pager(PagingConfig(20, jumpThreshold = 40)) { EhDB.historyLazyList }.flow.cachedIn(lifecycleScope) }.collectAsLazyPagingItems()
@@ -88,7 +88,7 @@ class HistoryScene : BaseScene() {
TopAppBar(
title = { Text(text = stringResource(id = R.string.history)) },
navigationIcon = {
IconButton(onClick = { openDrawer() }) {
IconButton(onClick = { toggleDrawer(GravityCompat.START) }) {
Icon(imageVector = Icons.Default.Menu, contentDescription = null)
}
},
@@ -133,55 +133,47 @@ class HistoryScene : BaseScene() {
true
},
)
LocalTouchSlopProvider(3f) {
SwipeToDismiss(
state = dismissState,
background = {},
dismissContent = {
// TODO: item delete & add animation
// Bug tracker: https://issuetracker.google.com/issues/150812265
GalleryInfoListItem(
onClick = {
navAnimated(
R.id.galleryDetailScene,
bundleOf(
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
GalleryDetailScene.KEY_GALLERY_INFO to info,
),
)
},
onLongClick = {
coroutineScope.launchIO {
dialogState.doGalleryInfoAction(info, context)
}
},
info = info,
modifier = Modifier.height(cardHeight),
)
},
directions = setOf(DismissDirection.EndToStart),
)
}
SwipeToDismiss(
state = dismissState,
background = {},
dismissContent = {
// TODO: item delete & add animation
// Bug tracker: https://issuetracker.google.com/issues/150812265
GalleryInfoListItem(
onClick = {
navAnimated(
R.id.galleryDetailScene,
bundleOf(
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
GalleryDetailScene.KEY_GALLERY_INFO to info,
),
)
},
onLongClick = {
coroutineScope.launchIO {
dialogState.doGalleryInfoAction(info, context)
}
},
info = info,
modifier = Modifier.height(cardHeight),
)
},
directions = setOf(DismissDirection.EndToStart),
)
} else {
CrystalCard(
modifier = Modifier
.height(cardHeight)
.fillMaxWidth(),
) {}
CrystalCard(modifier = Modifier.height(cardHeight).fillMaxWidth()) {}
}
}
}
Deferred({ delay(200) }) {
if (historyData.itemCount == 0) {
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
modifier = Modifier.padding(paddingValues).fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
imageVector = EhIcons.Big.Default.History,
painter = painterResource(id = R.drawable.big_history),
contentDescription = null,
Modifier.padding(16.dp),
)
@@ -20,14 +20,14 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.navigation.fragment.findNavController
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.SadAndroid
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.withUIContext
@@ -35,7 +35,7 @@ import moe.tarsin.coroutines.runSuspendCatching
class ProgressScene : BaseScene() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeWithViewLifecycle().apply {
return ComposeView(inflater.context).apply {
setMD3Content {
val action = rememberSaveable { requireArguments().getString(KEY_ACTION, INVALID) }
val gid = rememberSaveable { requireArguments().getLong(KEY_GID, -1) }
@@ -77,7 +77,7 @@ class ProgressScene : BaseScene() {
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
imageVector = EhIcons.Big.Default.SadAndroid,
painter = painterResource(id = R.drawable.big_sad_pandroid),
contentDescription = null,
modifier = Modifier.padding(16.dp),
)
@@ -1,15 +0,0 @@
package com.hippo.ehviewer.ui.scene
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
/**
* compose view with fragment view lifecycle
* use it after [Fragment.onCreateView]
* also @see [Fragment.getViewLifecycleOwner]
*/
@Suppress("FunctionName")
fun Fragment.ComposeWithViewLifecycle() = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner))
}
@@ -9,9 +9,10 @@ import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.annotation.CallSuper
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
@@ -27,8 +28,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.GravityCompat
import androidx.core.widget.addTextChangedListener
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import com.google.android.material.search.SearchView
import com.google.android.material.shape.MaterialShapeDrawable
@@ -44,10 +46,8 @@ import com.hippo.ehviewer.ui.legacy.FabLayout
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.hippo.ehviewer.util.applyNavigationBarsPadding
import com.jamal.composeprefs3.ui.ifNotNullThen
import com.jamal.composeprefs3.ui.ifTrueThen
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -58,22 +58,24 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
private val binding get() = _binding!!
private var mSuggestionList by mutableStateOf(emptyList<Suggestion>())
private var mSuggestionProvider: SuggestionProvider? = null
private var allowEmptySearch = true
var allowEmptySearch = true
private val mSearchDatabase = searchDatabase.searchDao()
private var onApplySearch: (String) -> Unit = {}
protected val mSearchFab get() = binding.searchFab
protected abstract val fabLayout: FabLayout
protected var mShowActionFab = true
protected val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
fabLayout.primaryFab?.visibility = View.INVISIBLE
}
}
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mSearchFab.visibility = View.INVISIBLE
}
}
protected abstract val fabLayout: FabLayout
protected abstract val fastScroller: View
protected abstract val recyclerView: View
protected abstract val contentView: View
override val enableDrawerGestures = true
override val showLeftDrawer = true
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = SceneSearchbarBinding.inflate(inflater, container, false)
@@ -84,9 +86,8 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
true
}
binding.searchBarList.consumeWindowInsets = false
binding.searchBarList.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner))
binding.searchBarList.setMD3Content {
LazyColumn(modifier = Modifier.navigationBarsPadding().imePadding()) {
LazyColumn(contentPadding = WindowInsets.navigationBars.asPaddingValues()) {
items(mSuggestionList) {
ListItem(
headlineContent = { Text(text = it.keyword) },
@@ -118,20 +119,13 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
onSearchViewHidden()
}
}
mSearchFab.setOnClickListener { onApplySearch() }
onCreateViewWithToolbar(inflater, binding.root, savedInstanceState)
// This has to be placed after onCreateViewWithToolbar() since
// callbacks are invoked in the reverse order in which they are added
binding.searchview.addTransitionListener(mSearchViewOnBackPressedCallback)
requireActivity().onBackPressedDispatcher.addCallback(mSearchViewOnBackPressedCallback)
binding.appbar.bringToFront()
fabLayout.applyNavigationBarsPadding()
fastScroller.applyNavigationBarsPadding()
recyclerView.applyNavigationBarsPadding()
contentView.applyInsetter {
type(statusBars = true) {
padding()
}
}
return binding.root
}
@@ -154,13 +148,16 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
_binding = null
}
private var drawerLocked = false
private var privLockModeStart: Int? = null
@CallSuper
open fun onSearchViewExpanded() {
if (!isDrawerLocked) {
drawerLocked = true
lockDrawer()
privLockModeStart = getDrawerLockMode(GravityCompat.START)
privLockModeStart?.let {
setDrawerLockMode(
DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
GravityCompat.START,
)
}
updateSuggestions()
}
@@ -168,9 +165,38 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
@CallSuper
open fun onSearchViewHidden() {
binding.toolbar.setText(binding.searchview.text)
if (drawerLocked) {
unlockDrawer()
drawerLocked = false
privLockModeStart?.let { setDrawerLockMode(it, GravityCompat.START) }
privLockModeStart = null
}
fun showSearchFab(animation: Boolean, delay: Long = 0) {
if (animation) {
mSearchFab.visibility = View.VISIBLE
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
mSearchFab.visibility = View.VISIBLE
mSearchFab.scaleX = 1.0f
mSearchFab.scaleY = 1.0f
}
}
fun hideSearchFab(animation: Boolean): Long {
return if (View.INVISIBLE == mSearchFab.visibility) {
0L
} else {
if (animation) {
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
.setListener(mSearchFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
} else {
mSearchFab.visibility = View.INVISIBLE
mSearchFab.scaleX = 0.0f
mSearchFab.scaleY = 0.0f
}
ANIMATE_TIME
}
}
@@ -214,6 +240,16 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
}
}
fun selectSearchFab(animation: Boolean) {
val delay = hideActionFab(animation)
showSearchFab(animation, delay)
}
fun selectActionFab(animation: Boolean) {
val delay = hideSearchFab(animation)
showActionFab(animation, delay)
}
fun setSearchBarHint(hint: String?) {
binding.toolbar.hint = hint
}
@@ -228,7 +264,7 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
}
override fun onNavigationClick() {
openDrawer()
toggleDrawer(GravityCompat.START)
}
override fun getMenuResId(): Int {
@@ -34,8 +34,8 @@ import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.ui.LICENSE_SCREEN
import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.ui.tools.rememberDialogState
import com.hippo.ehviewer.ui.tools.toAnnotatedString
import com.hippo.ehviewer.updater.AppUpdater
import com.hippo.ehviewer.updater.Release
@@ -66,7 +66,8 @@ fun AboutScreen() {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val dialogState = LocalDialogState.current
val dialogState = rememberDialogState()
dialogState.Intercept()
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
Scaffold(
topBar = {
@@ -21,10 +21,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@@ -33,12 +31,10 @@ import com.hippo.ehviewer.BuildConfig
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.CHROME_USER_AGENT
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.ReadableTime
@@ -58,7 +54,6 @@ fun AdvancedScreen() {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
val dialogState = LocalDialogState.current
Scaffold(
topBar = {
TopAppBar(
@@ -138,19 +133,6 @@ fun AdvancedScreen() {
title = stringResource(id = R.string.preload_thumb_aggressively),
value = Settings::preloadThumbAggressively,
)
var userAgent by Settings::userAgent.observed
val userAgentTitle = stringResource(id = R.string.user_agent)
Preference(
title = userAgentTitle,
summary = userAgent,
) {
coroutineScope.launch {
userAgent = dialogState.awaitInputText(
initial = userAgent,
title = userAgentTitle,
).trim().takeUnless { it.isBlank() } ?: CHROME_USER_AGENT
}
}
val exportFailed = stringResource(id = R.string.settings_advanced_export_data_failed)
LauncherPreference(
title = stringResource(id = R.string.settings_advanced_export_data),
@@ -17,10 +17,9 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.hippo.ehviewer.R
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.filled.SadPanda
import com.hippo.ehviewer.ui.ABOUT_SETTINGS_SCREEN
import com.hippo.ehviewer.ui.ADVANCED_SETTINGS_SCREEN
import com.hippo.ehviewer.ui.DOWNLOAD_SETTINGS_SCREEN
@@ -48,7 +47,7 @@ fun BaseScreen() {
) {
Column(modifier = Modifier.padding(it).nestedScroll(scrollBehavior.nestedScrollConnection)) {
PreferenceHeader(
icon = EhIcons.Default.SadPanda,
icon = painterResource(id = R.drawable.v_sad_panda_primary_x24),
title = R.string.settings_eh,
childRouteName = EH_SETTINGS_SCREEN,
)
@@ -49,11 +49,11 @@ import com.hippo.ehviewer.ui.MYTAGS_SCREEN
import com.hippo.ehviewer.ui.SIGN_IN_ROUTE_NAME
import com.hippo.ehviewer.ui.UCONFIG_SCREEN
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.TimePickerDialog
import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.ui.tools.rememberDialogState
import com.hippo.ehviewer.ui.tools.rememberedAccessor
import com.hippo.ehviewer.util.copyTextToClipboard
import com.hippo.ehviewer.util.whisperClipboard
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -70,7 +70,8 @@ fun EhScreen() {
val context = LocalContext.current
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
val signin = EhCookieStore.hasSignedIn()
val dialogState = LocalDialogState.current
val dialogState = rememberDialogState()
dialogState.Intercept()
Scaffold(
topBar = {
TopAppBar(
@@ -110,7 +111,7 @@ fun EhScreen() {
val spanned = context.getString(R.string.settings_eh_identity_cookies_signed, str).parseAsHtml()
setMessage(spanned)
setNeutralButton(R.string.settings_eh_identity_cookies_copy) { _, _ ->
copyTextToClipboard(str.replace("<br>", "\n"), true)
context whisperClipboard str.replace("<br>", "\n")
// Avoid double notify user since system have done that on Tiramisu above
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) launchSnackBar(copiedToClipboard)
}
@@ -212,7 +213,7 @@ fun EhScreen() {
}
}
SimpleMenuPreferenceInt(
title = stringResource(id = R.string.dark_theme),
title = stringResource(id = rikka.core.R.string.dark_theme),
entry = R.array.night_mode_entries,
entryValueRes = R.array.night_mode_values,
value = Settings::theme.observed,
@@ -276,22 +277,11 @@ fun EhScreen() {
summary = stringResource(id = R.string.settings_eh_show_gallery_pages_summary),
value = Settings::showGalleryPages,
)
val showComments = Settings::showComments.observed
SwitchPreference(
title = stringResource(id = R.string.settings_eh_show_gallery_comments),
summary = stringResource(id = R.string.settings_eh_show_gallery_comments_summary),
value = showComments.rememberedAccessor,
value = Settings::showComments,
)
AnimatedVisibility(visible = showComments.value) {
IntSliderPreference(
maxValue = 100,
minValue = -101,
showTicks = false,
title = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold),
summary = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold_summary),
value = Settings::commentThreshold,
)
}
if (EhTagDatabase.isTranslatable(context)) {
SwitchPreference(
title = stringResource(id = R.string.settings_eh_show_tag_translations),
@@ -49,11 +49,8 @@ import com.hippo.ehviewer.databinding.DialogAddFilterBinding
import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.tools.Deferred
import com.hippo.ehviewer.ui.tools.LocalDialogState
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.groupByToObserved
@Composable
@@ -63,7 +60,6 @@ fun FilterScreen() {
val scope = rememberCoroutineScope { Dispatchers.IO }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val allFilterMap = remember { scope.async { EhFilter.filters.await().groupByToObserved { it.mode } } }
val dialogState = LocalDialogState.current
class AddFilterDialogHelper(private val dialog: AlertDialog) : View.OnClickListener {
private val mArray = dialog.context.resources.getStringArray(R.array.filter_entries)
private val binding = DialogAddFilterBinding.bind(dialog.findViewById(R.id.base)!!)
@@ -164,22 +160,15 @@ fun FilterScreen() {
checked = filter.enable,
onCheckedChange = { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } },
)
Text(text = filter.text, modifier = Modifier.weight(1F))
Text(text = filter.text)
Spacer(modifier = Modifier.weight(1F))
IconButton(
onClick = {
scope.launch {
dialogState.awaitPermissionOrCancel(
confirmText = R.string.delete,
dismissText = android.R.string.cancel,
) {
Text(text = stringResource(id = R.string.delete_filter, filter.text))
BaseDialogBuilder(context).setMessage(context.getString(R.string.delete_filter, filter.text)).setPositiveButton(R.string.delete) { _, which ->
if (DialogInterface.BUTTON_POSITIVE == which) {
filter.forget { filters.remove(filter) }
}
filter.forget {
withUIContext {
filters.remove(filter)
}
}
}
}.setNegativeButton(android.R.string.cancel, null).show()
},
) {
Icon(imageVector = Icons.Default.Delete, contentDescription = null)
@@ -95,10 +95,10 @@ fun SwitchPreference(title: String, summary: String? = null, value: KMutableProp
}
@Composable
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, showTicks: Boolean = true, title: String, summary: String? = null, value: KMutableProperty0<Int>, enabled: Boolean = true) {
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, title: String, summary: String? = null, value: KMutableProperty0<Int>, enabled: Boolean = true) {
var v by remember { mutableIntStateOf(value.get()) }
fun set(float: Float) = value.set(float.roundToInt().also { v = it })
SliderPref(title = title, summary = summary, defaultValue = v.toFloat(), onValueChangeFinished = ::set, valueRange = minValue.toFloat()..maxValue.toFloat(), showValue = true, showInteger = true, steps = step, showTicks = showTicks, enabled = enabled)
SliderPref(title = title, summary = summary, defaultValue = v.toFloat(), onValueChangeFinished = ::set, valueRange = minValue.toFloat()..maxValue.toFloat(), showValue = true, showInteger = true, steps = step, enabled = enabled)
}
@Composable
@@ -24,8 +24,8 @@ import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.ui.LocalNavController
import com.hippo.ehviewer.ui.isAuthenticationSupported
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.observed
import com.hippo.ehviewer.ui.tools.rememberDialogState
import com.hippo.ehviewer.ui.tools.rememberedAccessor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -37,7 +37,8 @@ fun PrivacyScreen() {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
val dialogState = LocalDialogState.current
val dialogState = rememberDialogState()
dialogState.Intercept()
Scaffold(
topBar = {
TopAppBar(
@@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
@Composable
fun <T> Deferred(block: suspend () -> T, content: @Composable (T) -> Unit) {
var completed by remember { mutableStateOf<T?>(null) }
var completed by remember(block) { mutableStateOf<T?>(null) }
LaunchedEffect(key1 = Unit) {
completed = withIOContext { block() }
}
@@ -31,11 +31,9 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -51,7 +49,6 @@ import androidx.compose.ui.window.DialogProperties
import com.jamal.composeprefs3.ui.ifNotNullThen
import kotlin.coroutines.resume
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
interface DialogScope<R> {
@@ -109,24 +106,17 @@ class DialogState {
title: String? = null,
hint: String? = null,
isNumber: Boolean = false,
invalidator: (suspend (String) -> String?)? = null,
invalidator: ((String) -> String?)? = null,
): String {
return dialog { cont ->
val coroutineScope = rememberCoroutineScope()
var state by remember(cont) { mutableStateOf(initial) }
var error by remember(cont) { mutableStateOf<String?>(null) }
AlertDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = {
if (invalidator == null) {
cont.resume(state)
} else {
coroutineScope.launch {
error = invalidator.invoke(state)
error ?: cont.resume(state)
}
}
error = invalidator?.invoke(state)
error ?: cont.resume(state)
}) {
Text(text = stringResource(id = android.R.string.ok))
}
@@ -319,4 +309,7 @@ class DialogState {
private val IconWithTextCorner = RoundedCornerShape(8.dp)
val LocalDialogState = compositionLocalOf<DialogState> { error("CompositionLocal LocalDialogState not present!") }
@Composable
fun rememberDialogState(): DialogState {
return remember { DialogState() }
}
@@ -1,19 +0,0 @@
package com.hippo.ehviewer.ui.tools
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
// TODO: https://issuetracker.google.com/issues/269627294
@Composable
fun LocalTouchSlopProvider(factor: Float, content: @Composable () -> Unit) {
val current = LocalViewConfiguration.current
CompositionLocalProvider(
value = LocalViewConfiguration provides object : ViewConfiguration by current {
override val touchSlop: Float
get() = current.touchSlop * factor
},
content = content,
)
}
@@ -17,10 +17,12 @@
*/
package com.hippo.ehviewer.util
import android.app.Activity
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.view.textclassifier.TextClassifier
import androidx.core.os.persistableBundleOf
@@ -29,7 +31,15 @@ import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.scene.BaseScene
import splitties.systemservices.clipboardManager
fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
infix fun Context.tellClipboardWithToast(text: String?) {
addTextToClipboard(text, false, true)
}
infix fun Context.whisperClipboard(text: String?) {
addTextToClipboard(text, true, false)
}
fun Context.addTextToClipboard(text: CharSequence?, isSensitive: Boolean, useToast: Boolean = false) {
clipboardManager.apply {
setPrimaryClip(
ClipData.newPlainText(null, text).apply {
@@ -39,14 +49,12 @@ fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
},
)
}
}
fun Context.addTextToClipboard(text: CharSequence?, useToast: Boolean = false) {
copyTextToClipboard(text, false)
// Avoid double notify user since system have done that on Tiramisu above
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
val activity = findActivity<MainActivity>()
activity.showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT, useToast)
val activity = findActivity()
if (activity is MainActivity) {
activity.showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT, useToast)
}
}
}
@@ -58,3 +66,15 @@ fun ClipboardManager.getUrlFromClipboard(context: Context): String? {
val string = item?.coerceToText(context).toString()
return string.ifEmpty { null }
}
/**
* Find the closest Activity in a given Context.
*/
internal fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
throw IllegalStateException("findActivity() should be called in the context of an Activity")
}
@@ -1,17 +0,0 @@
package com.hippo.ehviewer.util
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
/**
* Find the closest Activity in a given Context.
*/
inline fun <reified T : Activity> Context.findActivity(): T {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context as T
context = context.baseContext
}
throw IllegalStateException("findActivity() should be called in the context of an Activity")
}
@@ -15,17 +15,11 @@
*/
package com.hippo.ehviewer.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
@@ -46,11 +40,5 @@ object FavouriteStatusRouter {
val globalFlow = _globalFlow.asSharedFlow()
@Composable
inline fun <R> collectAsState(initial: GalleryInfo, crossinline transform: @DisallowComposableCalls (Int) -> R) = remember(initial) {
globalFlow.transform { (gid, slot) -> if (initial.gid == gid) emit(slot) }
.map { transform(it) }
// We cannot use .map(transform) otherwise we will get a strange NPE
// TODO: Report to kotlin compiler issue-tracker
}.collectAsState(transform(initial.favoriteSlot))
fun stateFlow(targetGid: Long) = globalFlow.transform { (gid, slot) -> if (targetGid == gid) emit(slot) }
}
@@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import rikka.core.util.ContextUtils.requireActivity
// Fuck off the silly Android launcher and callback :)
@@ -57,7 +58,7 @@ private suspend fun <I, O> Context.awaitActivityResult(contract: ActivityResultC
lifecycle.addObserver(observer)
return withUIContext {
suspendCoroutine { cont -> // No cancellation support here since we cannot cancel a launched Intent
val activity = findActivity<ComponentActivity>()
val activity = requireActivity<ComponentActivity>(this@awaitActivityResult)
launcher = activity.activityResultRegistry.register(key, contract) {
launcher?.unregister()
lifecycle.removeObserver(observer)
@@ -5,8 +5,6 @@ package com.hippo.ehviewer.util
import kotlin.reflect.KMutableProperty0
import kotlin.reflect.KProperty
fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer)
interface Lazy<T> {
var value: T
}
@@ -2,12 +2,12 @@ package com.hippo.ehviewer.util
import android.annotation.SuppressLint
import android.webkit.WebView
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.CHROME_USER_AGENT
@SuppressLint("SetJavaScriptEnabled")
fun WebView.setDefaultSettings() = settings.run {
builtInZoomControls = true
displayZoomControls = false
javaScriptEnabled = true
userAgentString = Settings.userAgent
userAgentString = CHROME_USER_AGENT
}
@@ -1,57 +0,0 @@
package com.hippo.ehviewer.util
import android.graphics.Rect
import android.view.View
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.core.graphics.Insets
import androidx.core.view.WindowInsetsCompat
import dev.chrisbanes.insetter.applyInsetter
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
inline fun buildWindowInsets(builderAction: WindowInsetsCompat.Builder.() -> Unit): WindowInsetsCompat {
contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
val builder = if (isAtLeastQ) {
WindowInsetsCompat.Builder()
} else {
// TODO: Report to google issue tracker
// On API 20-28, WindowInsetsCompat.Builder() will copy the WindowInsets.CONSUMED static field,
// which isConsumed() returns true, resulting in the insets not being dispatched
runCatching {
val constructor = android.view.WindowInsets::class.java.getConstructor(Rect::class.java)
val platformInsets = constructor.newInstance(Rect())
WindowInsetsCompat.Builder(WindowInsetsCompat.toWindowInsetsCompat(platformInsets))
}.getOrElse {
it.printStackTrace()
WindowInsetsCompat.Builder()
}
}
builder.builderAction()
return builder.build()
}
@Composable
fun WindowInsetsCompat.Builder.set(type: Int, insets: WindowInsets): WindowInsetsCompat.Builder {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
return setInsets(
type,
Insets.of(
insets.getLeft(density, layoutDirection),
insets.getTop(density),
insets.getRight(density, layoutDirection),
insets.getBottom(density),
),
)
}
fun View.applyNavigationBarsPadding() {
applyInsetter {
type(navigationBars = true) {
padding()
}
}
}
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -31,7 +30,6 @@ fun SliderPref(
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
showValue: Boolean = false,
showInteger: Boolean = false,
showTicks: Boolean = true,
steps: Int = 0,
textColor: Color = MaterialTheme.colorScheme.onBackground,
enabled: Boolean = true,
@@ -63,14 +61,6 @@ fun SliderPref(
steps = steps,
onValueChangeFinished = { onValueChangeFinished?.invoke(value) },
enabled = enabled,
colors = if (showTicks) {
SliderDefaults.colors()
} else {
SliderDefaults.colors(
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
)
},
)
if (showValue) {
Text(
@@ -8,6 +8,7 @@ import android.webkit.WebViewClient
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.exception.CloudflareBypassException
import java.io.IOException
import java.util.concurrent.CountDownLatch
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
@@ -26,11 +27,14 @@ class CloudflareInterceptor(context: Context) : WebViewInterceptor(context) {
request: Request,
response: Response,
): Response {
response.close()
EhCookieStore.deleteCookie(request.url, COOKIE_NAME)
resolveWithWebView(request)
return chain.proceed(request)
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app
return runCatching {
response.close()
EhCookieStore.deleteCookie(request.url, COOKIE_NAME)
resolveWithWebView(request)
chain.proceed(request)
}.getOrElse { throw IOException(it) }
}
private fun resolveWithWebView(originalRequest: Request) {
@@ -1,28 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import java.io.IOException
import okhttp3.Interceptor
import okhttp3.Response
/**
* Catches any uncaught exceptions from later in the chain and rethrows as a non-fatal
* IOException to avoid catastrophic failure.
*
* This should be the first interceptor in the client.
*
* See [Interceptor](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-interceptor/index.html)
*/
class UncaughtExceptionInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: Exception) {
if (e is IOException) {
throw e
} else {
throw IOException(e)
}
}
}
}
@@ -23,6 +23,7 @@ import android.content.ClipData
import android.content.ContentValues
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
@@ -42,21 +43,22 @@ import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.activity.viewModels
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.graphics.ColorUtils
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import com.google.android.material.shape.MaterialShapeDrawable
import com.hippo.ehviewer.BuildConfig
@@ -69,21 +71,16 @@ import com.hippo.ehviewer.gallery.EhPageLoader
import com.hippo.ehviewer.gallery.PageLoader2
import com.hippo.ehviewer.image.Image
import com.hippo.ehviewer.ui.EhActivity
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.getParcelableCompat
import com.hippo.ehviewer.util.getParcelableExtraCompat
import com.hippo.ehviewer.util.getValue
import com.hippo.ehviewer.util.isAtLeastO
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.lazyMut
import com.hippo.ehviewer.util.requestPermission
import com.hippo.ehviewer.util.sendTo
import com.hippo.ehviewer.util.setValue
import com.hippo.unifile.UniFile
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@@ -94,7 +91,6 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.isNightMode
@@ -104,25 +100,22 @@ import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
import java.io.File
import java.io.IOException
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.math.abs
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import splitties.init.appCtx
import kotlinx.coroutines.suspendCancellableCoroutine
import splitties.systemservices.clipboardManager
class GalleryModel : ViewModel() {
var galleryProvider: PageLoader2? = null
override fun onCleared() {
super.onCleared()
galleryProvider?.stop()
galleryProvider = null
}
}
class ReaderActivity : EhActivity() {
lateinit var binding: ReaderActivityBinding
private var mAction: String? = null
@@ -131,8 +124,6 @@ class ReaderActivity : EhActivity() {
private var mGalleryInfo: BaseGalleryInfo? = null
private var mPage: Int = 0
private var mCacheFileName: String? = null
private val vm: GalleryModel by viewModels()
private val dialogState = DialogState()
/**
* Whether the menu is currently visible.
@@ -168,8 +159,22 @@ class ReaderActivity : EhActivity() {
}
}
}
var mGalleryProvider by lazyMut { vm::galleryProvider }
var mGalleryProvider: PageLoader2? = null
private var mCurrentIndex: Int = 0
private var mSavingPage = -1
private lateinit var builder: EditTextDialogBuilder
private var dialogShown = false
private lateinit var dialog: AlertDialog
private var requestStoragePermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { result ->
if (result!! && mSavingPage != -1) {
saveImage(mSavingPage)
} else {
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
}
mSavingPage = -1
}
private val galleryDetailUrl: String?
get() {
@@ -185,6 +190,13 @@ class ReaderActivity : EhActivity() {
}
private fun buildProvider(replace: Boolean = false) {
// TODO: Better solution for this dirty workaround
if (setOrientation(ReaderPreferences.defaultOrientationType().get())) {
// Orientation changed, we don't know if the activity will recreate, just recreate anyway
recreate()
return
}
if (mGalleryProvider != null) {
if (replace) mGalleryProvider!!.stop() else return
}
@@ -204,27 +216,43 @@ class ReaderActivity : EhActivity() {
Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show()
}
val continuation: AtomicReference<Continuation<String>?> = AtomicReference(null)
mGalleryProvider = ArchivePageLoader(
appCtx,
this,
mUri!!,
) { invalidator ->
runCatching {
dialogState.awaitInputText(
title = getString(R.string.archive_need_passwd),
hint = getString(R.string.archive_passwd),
) {
if (it.isBlank()) {
getString(R.string.passwd_cannot_be_empty)
} else if (invalidator(it)) {
null
} else {
getString(R.string.passwd_wrong)
flow {
if (!dialogShown) {
withUIContext {
dialogShown = true
dialog.run {
show()
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val passwd = builder.text
if (passwd.isEmpty()) {
builder.setError(getString(R.string.passwd_cannot_be_empty))
} else {
continuation.getAndSet(null)?.resume(passwd)
}
}
setOnCancelListener {
finish()
}
}
}
}
}.onFailure {
finish()
}.getOrThrow()
}
while (true) {
currentCoroutineContext().ensureActive()
val r = suspendCancellableCoroutine {
continuation.set(it)
it.invokeOnCancellation { dialog.dismiss() }
}
emit(r)
withUIContext {
builder.setError(getString(R.string.passwd_wrong))
}
}
},
)
}
} else {
Toast.makeText(this, "Archives are not supported before Android P", Toast.LENGTH_LONG).show()
@@ -243,6 +271,7 @@ class ReaderActivity : EhActivity() {
private fun onInit() {
handleIntent(intent)
buildProvider()
}
private fun onRestore(savedInstanceState: Bundle) {
@@ -252,6 +281,7 @@ class ReaderActivity : EhActivity() {
mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO)
mPage = savedInstanceState.getInt(KEY_PAGE, -1)
mCurrentIndex = savedInstanceState.getInt(KEY_CURRENT_INDEX)
buildProvider()
}
override fun onNewIntent(intent: Intent?) {
@@ -298,15 +328,16 @@ class ReaderActivity : EhActivity() {
} else {
onRestore(savedInstanceState)
}
buildProvider()
binding = ReaderActivityBinding.inflate(layoutInflater)
binding.dialogStub.setMD3Content {
dialogState.Intercept()
}
setContentView(binding.root)
builder = EditTextDialogBuilder(this, null, getString(R.string.archive_passwd))
builder.setTitle(getString(R.string.archive_need_passwd))
builder.setPositiveButton(getString(android.R.string.ok), null)
dialog = builder.create()
dialog.setCanceledOnTouchOutside(false)
mGalleryProvider.let {
if (it == null) {
finish()
return
}
lifecycleScope.launchIO {
@@ -321,6 +352,8 @@ class ReaderActivity : EhActivity() {
fun setGallery() {
if (mGalleryProvider?.isReady != true) return
// TODO: Not well place to call it
dialog.dismiss()
// Get start page
if (mCurrentIndex == 0) {
@@ -335,6 +368,7 @@ class ReaderActivity : EhActivity() {
updateViewerInset(ReaderPreferences.fullscreen().get())
binding.viewerContainer.removeAllViews()
setOrientation(ReaderPreferences.defaultOrientationType().get())
updateOrientationShortcut(ReaderPreferences.defaultOrientationType().get())
binding.viewerContainer.addView(viewer?.getView())
viewer?.setGalleryProvider(mGalleryProvider!!)
moveToPageIndex(mCurrentIndex)
@@ -345,6 +379,10 @@ class ReaderActivity : EhActivity() {
config = null
viewer?.destroy()
viewer = null
if (mGalleryProvider != null) {
mGalleryProvider!!.stop()
mGalleryProvider = null
}
readerSettingSheetDialog?.dismiss()
readerSettingSheetDialog = null
}
@@ -444,73 +482,78 @@ class ReaderActivity : EhActivity() {
}
fun saveImage(page: Int) {
mGalleryProvider ?: return
lifecycleScope.launchUI {
val granted = isAtLeastQ || requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (granted) {
val filename = mGalleryProvider!!.getImageFilenameWithExtension(page)
var mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
MimeTypeMap.getFileExtensionFromUrl(filename),
)
if (mimeType.isNullOrEmpty()) {
mimeType = "image/jpeg"
}
val realPath: String
val resolver = contentResolver
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
if (isAtLeastQ) {
values.put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME,
)
values.put(MediaStore.MediaColumns.IS_PENDING, 1)
realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME
} else {
val path = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
AppConfig.APP_DIRNAME,
)
realPath = path.toString()
if (!FileUtils.ensureDirectory(path)) {
Toast.makeText(this@ReaderActivity, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
return@launchUI
}
values.put(MediaStore.MediaColumns.DATA, path.toString() + File.separator + filename)
}
val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (imageUri == null) {
Toast.makeText(this@ReaderActivity, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
return@launchUI
}
if (!mGalleryProvider!!.save(page, UniFile.fromMediaUri(this@ReaderActivity, imageUri))) {
try {
resolver.delete(imageUri, null, null)
} catch (e: Exception) {
e.printStackTrace()
}
Toast.makeText(this@ReaderActivity, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
return@launchUI
} else if (isAtLeastQ) {
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
resolver.update(imageUri, contentValues, null, null)
}
Toast.makeText(
this@ReaderActivity,
getString(R.string.image_saved, realPath + File.separator + filename),
Toast.LENGTH_SHORT,
).show()
} else {
Toast.makeText(this@ReaderActivity, R.string.permission_denied, Toast.LENGTH_SHORT).show()
}
if (null == mGalleryProvider) {
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
) != PackageManager.PERMISSION_GRANTED
) {
mSavingPage = page
requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
return
}
val filename = mGalleryProvider!!.getImageFilenameWithExtension(page)
var mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
MimeTypeMap.getFileExtensionFromUrl(filename),
)
if (mimeType.isNullOrEmpty()) {
mimeType = "image/jpeg"
}
val realPath: String
val resolver = contentResolver
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME,
)
values.put(MediaStore.MediaColumns.IS_PENDING, 1)
realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME
} else {
val path = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
AppConfig.APP_DIRNAME,
)
realPath = path.toString()
if (!FileUtils.ensureDirectory(path)) {
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
return
}
values.put(MediaStore.MediaColumns.DATA, path.toString() + File.separator + filename)
}
val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (imageUri == null) {
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
return
}
if (!mGalleryProvider!!.save(page, UniFile.fromMediaUri(this, imageUri))) {
try {
resolver.delete(imageUri, null, null)
} catch (e: Exception) {
e.printStackTrace()
}
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()
return
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
resolver.update(imageUri, contentValues, null, null)
}
Toast.makeText(
this,
getString(R.string.image_saved, realPath + File.separator + filename),
Toast.LENGTH_SHORT,
).show()
}
fun saveImageTo(page: Int) {
@@ -813,13 +856,15 @@ class ReaderActivity : EhActivity() {
/**
* Forces the user preferred [orientation] on the activity.
* Returns true if a restart is required.
*/
private fun setOrientation(orientation: Int) {
private fun setOrientation(orientation: Int): Boolean {
val newOrientation = OrientationType.fromPreference(orientation)
if (newOrientation.flag != requestedOrientation) {
val restart = newOrientation.flag != requestedOrientation
if (restart) {
requestedOrientation = newOrientation.flag
}
updateOrientationShortcut(ReaderPreferences.defaultOrientationType().get())
return restart
}
/**
@@ -46,7 +46,7 @@ fun CombinedCircularProgressIndicator(progress: Float) {
label = "rotation",
)
CircularProgressIndicator(
progress = animatedProgress,
progress = { animatedProgress },
modifier = Modifier.rotate(rotation),
)
}
@@ -1,21 +0,0 @@
package rikka.core.res
import android.content.res.Resources
import android.graphics.drawable.Drawable
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
@ColorInt
fun Resources.Theme.resolveColor(@AttrRes attrId: Int): Int {
val a = obtainStyledAttributes(intArrayOf(attrId))
val res = a.getColor(0, 0)
a.recycle()
return res
}
fun Resources.Theme.resolveDrawable(@AttrRes attrId: Int): Drawable? {
val a = obtainStyledAttributes(intArrayOf(attrId))
val res = a.getDrawable(0)
a.recycle()
return res
}
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:tint="?colorTertiary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_big_history" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2022 Tarsin Norbin
~
~ This file is part of EhViewer
~
~ EhViewer is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published
~ by the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with EhViewer.
~ If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M2,17h2v0.5L3,17.5v1h1v0.5L2,19v1h3v-4L2,16v1zM3,8h1L4,4L2,4v1h1v3zM2,11h1.8L2,13.1v0.9h3v-1L3.2,13L5,10.9L5,10L2,10v1zM7,5v2h14L21,5L7,5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2z" />
</vector>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2019 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_eh_subscription" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2022 Tarsin Norbin
~
~ This file is part of EhViewer
~
~ EhViewer is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published
~ by the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with EhViewer.
~ If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_fire" />
</vector>
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2019 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_heart" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_history" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_homepage" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_sad_panda" />
</vector>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_settings" />
</vector>
+34 -4
View File
@@ -14,10 +14,40 @@
~ limitations under the License.
-->
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:id="@+id/draw_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
android:clipChildren="false"
app:edgeToEdge="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fitsSystemWindowsInsets="start|end">
<com.hippo.ehviewer.ui.legacy.AboveSnackerLayout
android:id="@+id/absnacker"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
</com.hippo.ehviewer.ui.legacy.AboveSnackerLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/nav_drawer_main" />
</androidx.drawerlayout.widget.DrawerLayout>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="160dp"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="centerCrop"
app:srcCompat="@drawable/sadpanda_low_poly" />
</FrameLayout>
@@ -5,11 +5,6 @@
android:layout_height="match_parent"
android:gravity="center">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_stub"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<FrameLayout
android:id="@+id/reader_container"
android:layout_width="match_parent"
+7 -7
View File
@@ -21,26 +21,25 @@
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/gallery_padding_top_search_bar"
android:paddingHorizontal="@dimen/gallery_list_margin_h"
android:paddingVertical="@dimen/gallery_list_margin_v" />
android:paddingVertical="@dimen/gallery_list_margin_v"
app:fitsSystemWindowsInsets="bottom" />
<com.hippo.ehviewer.ui.legacy.FastScroller
android:id="@+id/fast_scroller"
android:layout_width="30dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_marginTop="@dimen/gallery_padding_top_search_bar"
android:paddingStart="20dp"
android:paddingEnd="4dp"
android:paddingBottom="8dp" />
android:paddingBottom="8dp"
app:fitsSystemWindowsInsets="bottom" />
</FrameLayout>
@@ -59,7 +58,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingEnd="@dimen/corner_fab_margin"
android:paddingBottom="@dimen/corner_fab_margin">
android:paddingBottom="@dimen/corner_fab_margin"
app:fitsSystemWindowsInsets="bottom">
<com.google.android.material.floatingactionbutton.FloatingActionButton
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
+2 -1
View File
@@ -26,7 +26,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingEnd="@dimen/corner_fab_margin"
android:paddingBottom="@dimen/corner_fab_margin">
android:paddingBottom="@dimen/corner_fab_margin"
app:fitsSystemWindowsInsets="bottom">
<com.google.android.material.floatingactionbutton.FloatingActionButton
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
@@ -30,7 +30,8 @@
android:clipToPadding="false"
android:paddingLeft="@dimen/keyline_margin"
android:paddingRight="@dimen/keyline_margin"
android:paddingBottom="@dimen/fab_rv_padding_size" />
android:paddingBottom="@dimen/fab_rv_padding_size"
app:fitsSystemWindowsInsets="bottom" />
</androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout>
@@ -42,7 +43,8 @@
android:layout_gravity="center"
android:drawablePadding="8dp"
android:gravity="center_horizontal"
android:text="@string/no_one_comments_gallery" />
android:text="@string/no_one_comments_gallery"
app:fitsSystemWindowsInsets="bottom" />
<LinearLayout
android:id="@+id/edit_panel"
@@ -52,7 +54,8 @@
android:background="?attr/colorPrimaryContainer"
android:elevation="4dp"
android:orientation="horizontal"
android:visibility="invisible">
android:visibility="invisible"
app:fitsSystemWindowsInsets="bottom">
<EditText
android:id="@+id/edit_text"
@@ -87,7 +90,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingEnd="@dimen/corner_fab_margin"
android:paddingBottom="@dimen/corner_fab_margin">
android:paddingBottom="@dimen/corner_fab_margin"
app:fitsSystemWindowsInsets="bottom">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
+4 -19
View File
@@ -27,14 +27,16 @@
android:layout_height="match_parent"
android:layout_marginStart="@dimen/gallery_search_bar_margin_v"
android:layout_marginEnd="@dimen/gallery_search_bar_margin_v"
android:paddingTop="@dimen/gallery_padding_top_search_bar" />
android:paddingTop="80dp"
app:layout_fitsSystemWindowsInsets="top" />
<com.hippo.ehviewer.ui.legacy.FabLayout
android:id="@+id/fab_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingEnd="@dimen/corner_fab_margin"
android:paddingBottom="@dimen/corner_fab_margin">
android:paddingBottom="@dimen/corner_fab_margin"
app:layout_fitsSystemWindowsInsets="bottom">
<com.google.android.material.floatingactionbutton.FloatingActionButton
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
@@ -60,21 +62,4 @@
</com.hippo.ehviewer.ui.legacy.FabLayout>
<com.hippo.ehviewer.ui.legacy.FabLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingEnd="@dimen/corner_fab_margin"
android:paddingBottom="@dimen/corner_fab_margin">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/search_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="0"
android:scaleY="0"
android:visibility="invisible"
app:srcCompat="@drawable/v_magnify_x24" />
</com.hippo.ehviewer.ui.legacy.FabLayout>
</merge>
@@ -47,4 +47,22 @@
</com.google.android.material.search.SearchView>
<com.hippo.ehviewer.ui.legacy.FabLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingEnd="@dimen/corner_fab_margin"
android:paddingBottom="@dimen/corner_fab_margin"
app:fitsSystemWindowsInsets="top|bottom">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/search_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="0"
android:scaleY="0"
android:visibility="invisible"
app:srcCompat="@drawable/v_magnify_x24" />
</com.hippo.ehviewer.ui.legacy.FabLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -20,6 +20,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:paddingTop="16dp"
app:fitsSystemWindowsInsets="top"
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
<com.google.android.material.progressindicator.CircularProgressIndicator
@@ -52,7 +54,8 @@
android:clipChildren="false"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/gallery_list_margin_h"
android:paddingVertical="@dimen/gallery_list_margin_v" />
android:paddingVertical="@dimen/gallery_list_margin_v"
app:fitsSystemWindowsInsets="bottom" />
</androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout>
@@ -64,6 +67,7 @@
android:layout_marginTop="@dimen/gallery_padding_top_search_bar"
android:paddingStart="20dp"
android:paddingEnd="4dp"
android:paddingVertical="8dp" />
android:paddingVertical="8dp"
app:fitsSystemWindowsInsets="bottom" />
</FrameLayout>
+67
View File
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_homepage"
android:icon="@drawable/v_homepage_black_x24"
android:title="@string/homepage" />
<item
android:id="@+id/nav_subscription"
android:icon="@drawable/v_eh_subscription_black_x24"
android:title="@string/subscription" />
<item
android:id="@+id/nav_whats_hot"
android:icon="@drawable/v_fire_black_x24"
android:title="@string/whats_hot" />
<item
android:id="@+id/nav_toplist"
android:icon="@drawable/ic_baseline_format_list_numbered_24"
android:title="@string/toplist" />
<item
android:id="@+id/nav_favourite"
android:icon="@drawable/v_heart_x24"
android:title="@string/favourite" />
<item
android:id="@+id/nav_history"
android:icon="@drawable/v_history_black_x24"
android:title="@string/history" />
<item
android:id="@+id/nav_downloads"
android:icon="@drawable/v_download_x24"
android:title="@string/downloads" />
<item
android:id="@+id/nav_settings"
android:icon="@drawable/v_settings_black_x24"
android:title="@string/settings" />
<item
android:id="@+id/nav_stub"
android:title="@string/stub"
android:visible="false" />
</group>
</menu>
+2 -1
View File
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph">
android:id="@+id/nav_graph"
app:startDestination="@id/galleryListScene">
<fragment
android:id="@+id/galleryListScene"
+4 -3
View File
@@ -60,7 +60,7 @@
<string name="gallery_list_search_bar_hint_e_hentai">E-Hentai durchsuchen</string>
<string name="gallery_list_empty_hit">Die Welt ist so groß und der Panda sitzt ganz allein</string>
<string name="search">Suchen</string>
<string name="keyword_search">Suche nach dem Schlüsselwort</string>
<string name="keyword_search">"Suche nach dem Schlüsselwort "</string>
<string name="image_search">Suche nach einem Bild</string>
<string name="search_normal">Normale Suche</string>
<string name="search_advance">Erweiterte Optionen</string>
@@ -204,7 +204,7 @@
<string name="download_remove_dialog_check_text">Bilddateien löschen</string>
<string name="stat_download_action_stop_all">Alle stopppen</string>
<string name="default_download_label_name">Standard</string>
<string name="download_move_dialog_title">Verschieben</string>
<string name="download_move_dialog_title">"Verschieben "</string>
<string name="download_labels">Download-Etikette</string>
<string name="download_start_all">Alle starten</string>
<string name="download_stop_all">Alle stoppen</string>
@@ -303,6 +303,7 @@
<string name="default_favorites_collection">Standard-Favoriten</string>
<string name="let_me_select">Manuell auswählen</string>
<string name="collections">Kollektionen</string>
<string name="need_sign_in">Du musst dich zuerst anmelden</string>
<string name="error_something_wrong_happened">Ein Fehler ist aufgetreten</string>
<string name="from_the_future">Von der Zukunft</string>
<string name="just_now">Gerade</string>
@@ -311,6 +312,7 @@
<string name="no_archives">Keine Archivdatei</string>
<string name="download_archive_started">Archivdatei wird heruntergeladen</string>
<string name="download_archive_failure">Fehler beim Herunterladen der Archivdatei</string>
<string name="archive">Archivdateien</string>
<string name="download_archive_failure_no_hath">H@H-Klient ist für das Herunterladen der Archivdateien erforderlich</string>
<string name="settings_privacy">Datenschutzoptionen</string>
<string name="settings_privacy_secure">Screenshot verhindern</string>
@@ -347,6 +349,5 @@
<string name="settings_eh_identity_cookies_copy">Kopieren</string>
<string name="settings_eh_show_gallery_pages">Galerieseitenzahl anzeigen</string>
<string name="settings_eh_show_gallery_pages_summary">Die Galerieseitenzahle in der Galerieliste anzeigen</string>
<string name="dark_theme_follow_system">System folgen</string>
</resources>
+2 -1
View File
@@ -363,6 +363,7 @@
<string name="default_favorites_collection">Colección de favoritos por defecto</string>
<string name="let_me_select">Déjame elegir</string>
<string name="collections">Colección</string>
<string name="need_sign_in">Se necesita iniciar sesión</string>
<!-- Progress Scene -->
@@ -410,6 +411,7 @@
<string name="cancel_vote_down">Cancelar voto negativo</string>
<string name="clear_all">Limpiar todo</string>
<string name="comment_failed">Comentario fallido</string>
<string name="archive">Archivo</string>
<string name="delete">Eliminar</string>
<string name="download_state_failed">Descarga fallida</string>
<string name="download_state_none">Descarga en pausa</string>
@@ -433,6 +435,5 @@
<string name="settings_privacy_secure_summary">Si habilitas esta opción no podrás hacer captura de pantalla y el sistema no mostrará una previsualización en el multitareas.\n\nDeberás reiniciar la aplicación para activar esta función.</string>
<string name="u_config">Ajustes EHentai</string>
<string name="apply_tip">Toca la marca de verificación para guardar los cambios</string>
<string name="dark_theme_follow_system">Sigue el sistema</string>
</resources>
+2 -4
View File
@@ -402,6 +402,7 @@
<string name="default_favorites_collection">Collection favoris par défaut</string>
<string name="let_me_select">Sélectionner manuellement</string>
<string name="collections">Collections</string>
<string name="need_sign_in">Vous devez vous connecter</string>
<!-- Progress Scene -->
<string name="error_something_wrong_happened">Une erreur s\'est produite</string>
@@ -444,6 +445,7 @@
<string name="no_archives">Aucun fiche archives</string>
<string name="download_archive_started">Téléchargement du fiche archives a commencé</string>
<string name="download_archive_failure">Téléchargement du fiche archives a échoué</string>
<string name="archive">Fiches archives</string>
<string name="download_archive_failure_no_hath">Client H@H est nécessaire pour le téléchargement de fiches archives</string>
<string name="settings_privacy">Confidentialité</string>
<string name="settings_privacy_secure">Empêcher les captures d\'ecran</string>
@@ -455,9 +457,5 @@
<string name="settings_eh_identity_cookies_copy">Copier</string>
<string name="settings_eh_show_gallery_pages">Montrer les nombres des pages des galeries</string>
<string name="settings_eh_show_gallery_pages_summary">Montrer le nombre des pages des galerie dans la liste des galeries</string>
<string name="dark_theme">Thème sombre</string>
<string name="dark_theme_follow_system">Identique au système</string>
<string name="dark_theme_off">Toujours actif</string>
<string name="dark_theme_on">Toujours inactif</string>
</resources>
+2 -1
View File
@@ -399,6 +399,7 @@
<string name="default_favorites_collection">既定のお気に入りリスト</string>
<string name="let_me_select">手動で選択</string>
<string name="collections">お気に入りコレクション</string>
<string name="need_sign_in">ログインが必要</string>
<!-- Progress Scene -->
<string name="error_something_wrong_happened">エラーが発生しました</string>
@@ -435,6 +436,7 @@
<string name="no_archives">圧縮パッケージなし</string>
<string name="download_archive_started">圧縮パッケージのダウンロードを開始しました</string>
<string name="download_archive_failure">圧縮パッケージをダウンロードできませんでした</string>
<string name="archive">圧縮パッケージ</string>
<string name="download_archive_failure_no_hath">圧縮パッケージのダウンロードにはH@Hクライアントが必要です</string>
<string name="settings_privacy">プライバシー</string>
<string name="settings_privacy_secure">スクリーンキャプチャを防ぐ</string>
@@ -453,5 +455,4 @@
<string name="settings_eh_identity_cookies_copy">コピー</string>
<string name="settings_eh_show_gallery_pages">ギャラリーページ数を表示</string>
<string name="settings_eh_show_gallery_pages_summary">リストにギャラリーのページの数量を表示します</string>
<string name="dark_theme">暗いテーマ</string>
</resources>
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff. Show more