Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cde607e9a | ||
|
|
2f39359b47 | ||
|
|
d1e3965d62 | ||
|
|
10c7019aeb | ||
|
|
76aa401800 | ||
|
|
dab5878f74 |
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ android {
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 180045
|
||||
versionName = "1.8.11.1"
|
||||
versionName = "1.8.10.3"
|
||||
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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
+9
-5
@@ -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) {
|
||||
|
||||
-28
@@ -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,14 +43,17 @@ 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
|
||||
@@ -69,8 +73,8 @@ 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
|
||||
@@ -79,9 +83,7 @@ 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
|
||||
@@ -94,7 +96,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,14 +105,20 @@ 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() {
|
||||
@@ -132,7 +139,6 @@ class ReaderActivity : EhActivity() {
|
||||
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.
|
||||
@@ -170,6 +176,20 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
var mGalleryProvider by lazyMut { vm::galleryProvider }
|
||||
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() {
|
||||
@@ -204,27 +224,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()
|
||||
@@ -300,10 +336,13 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
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()
|
||||
@@ -321,6 +360,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) {
|
||||
@@ -444,73 +485,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) {
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -373,6 +373,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>
|
||||
@@ -408,6 +409,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>
|
||||
@@ -415,8 +417,4 @@
|
||||
|
||||
<string name="download_service">다운로드 서비스</string>
|
||||
<string name="favorite_name">즐겨찾기</string>
|
||||
<string name="dark_theme">어두운 테마</string>
|
||||
<string name="dark_theme_follow_system">시스템 설정</string>
|
||||
<string name="dark_theme_off">항상 꺼짐</string>
|
||||
<string name="dark_theme_on">항상 켜짐</string>
|
||||
</resources>
|
||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user